Platform-wide visual overhaul, team invites, analytics improvements, and deployment hardening
UI overhaul applying jury dashboard design patterns across all pages: - Stat cards with border-l-4 accent + icon pills on admin, observer, mentor, applicant dashboards and reports - Card section headers with color-coded icon pills throughout - Hover lift effects (translate-y + shadow) on cards and list items - Gradient progress bars (brand-teal to brand-blue) platform-wide - AnimatedCard stagger animations on all dashboard sections - Auth pages with gradient accent strip and polished icon containers - EmptyState component upgraded with rounded icon pill containers - Replaced AI-looking icons (Brain/Sparkles/Bot/Wand2/Cpu) with descriptive alternatives across 12 files - Removed gradient overlay from jury dashboard header - Quick actions restyled as card links with group hover effects Backend improvements: - Team member invite emails with account setup flow and notification logging - Analytics routers accept edition-wide queries (programId) in addition to roundId - Round detail endpoint returns inline progress data (eliminates extra getProgress call) - Award voting endpoints parallelized with Promise.all - Bulk invite supports optional sendInvitation flag - AwardVote composite index migration for query performance Infrastructure: - Docker entrypoint with migration retry loop (configurable retries/delay) - docker-compose pull_policy: always for automatic image refresh - Simplified deploy/update scripts using docker compose up -d --pull always - Updated deployment documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,10 +19,10 @@ import { Progress } from '@/components/ui/progress'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
Sparkles,
|
||||
Users,
|
||||
User,
|
||||
Check,
|
||||
Wand2,
|
||||
RefreshCw,
|
||||
} from 'lucide-react'
|
||||
import { getInitials } from '@/lib/utils'
|
||||
|
||||
@@ -199,7 +199,7 @@ function MentorAssignmentContent({ projectId }: { projectId: string }) {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-primary" />
|
||||
<Users className="h-5 w-5 text-primary" />
|
||||
AI-Suggested Mentors
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -225,7 +225,7 @@ function MentorAssignmentContent({ projectId }: { projectId: string }) {
|
||||
{autoAssignMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Wand2 className="mr-2 h-4 w-4" />
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Auto-Assign Best Match
|
||||
</Button>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { FileUpload } from '@/components/shared/file-upload'
|
||||
import { ProjectLogoWithUrl } from '@/components/shared/project-logo-with-url'
|
||||
import { UserAvatar } from '@/components/shared/user-avatar'
|
||||
import { EvaluationSummaryCard } from '@/components/admin/evaluation-summary-card'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Edit,
|
||||
@@ -184,13 +185,16 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
|
||||
{/* Stats Grid */}
|
||||
{stats && (
|
||||
<AnimatedCard index={0}>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Card>
|
||||
<Card className="transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Average Score
|
||||
</CardTitle>
|
||||
<BarChart3 className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="rounded-lg bg-brand-teal/10 p-1.5">
|
||||
<BarChart3 className="h-4 w-4 text-brand-teal" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
@@ -202,12 +206,14 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Recommendations
|
||||
</CardTitle>
|
||||
<ThumbsUp className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="rounded-lg bg-emerald-500/10 p-1.5">
|
||||
<ThumbsUp className="h-4 w-4 text-emerald-500" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
@@ -219,12 +225,19 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AnimatedCard>
|
||||
)}
|
||||
|
||||
{/* Project Info */}
|
||||
<AnimatedCard index={1}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Project Information</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2.5 text-lg">
|
||||
<div className="rounded-lg bg-emerald-500/10 p-1.5">
|
||||
<FileText className="h-4 w-4 text-emerald-500" />
|
||||
</div>
|
||||
Project Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Category & Ocean Issue badges */}
|
||||
@@ -393,14 +406,18 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Team Members Section */}
|
||||
{project.teamMembers && project.teamMembers.length > 0 && (
|
||||
<AnimatedCard index={2}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
<CardTitle className="flex items-center gap-2.5 text-lg">
|
||||
<div className="rounded-lg bg-violet-500/10 p-1.5">
|
||||
<Users className="h-4 w-4 text-violet-500" />
|
||||
</div>
|
||||
Team Members ({project.teamMembers.length})
|
||||
</CardTitle>
|
||||
</div>
|
||||
@@ -437,15 +454,19 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
)}
|
||||
|
||||
{/* Mentor Assignment Section */}
|
||||
{project.wantsMentorship && (
|
||||
<AnimatedCard index={3}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Heart className="h-5 w-5" />
|
||||
<CardTitle className="flex items-center gap-2.5 text-lg">
|
||||
<div className="rounded-lg bg-rose-500/10 p-1.5">
|
||||
<Heart className="h-4 w-4 text-rose-500" />
|
||||
</div>
|
||||
Mentor Assignment
|
||||
</CardTitle>
|
||||
{!project.mentorAssignment && (
|
||||
@@ -487,12 +508,19 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
)}
|
||||
|
||||
{/* Files Section */}
|
||||
<AnimatedCard index={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Files</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2.5 text-lg">
|
||||
<div className="rounded-lg bg-rose-500/10 p-1.5">
|
||||
<FileText className="h-4 w-4 text-rose-500" />
|
||||
</div>
|
||||
Files
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Project documents and materials
|
||||
</CardDescription>
|
||||
@@ -535,14 +563,21 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Assignments Section */}
|
||||
{assignments && assignments.length > 0 && (
|
||||
<AnimatedCard index={5}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Jury Assignments</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2.5 text-lg">
|
||||
<div className="rounded-lg bg-violet-500/10 p-1.5">
|
||||
<Users className="h-4 w-4 text-violet-500" />
|
||||
</div>
|
||||
Jury Assignments
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{assignments.filter((a) => a.evaluation?.status === 'SUBMITTED')
|
||||
.length}{' '}
|
||||
@@ -649,6 +684,7 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
)}
|
||||
|
||||
{/* AI Evaluation Summary */}
|
||||
|
||||
@@ -60,7 +60,6 @@ import {
|
||||
Search,
|
||||
Trash2,
|
||||
Loader2,
|
||||
Sparkles,
|
||||
Tags,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
@@ -98,6 +97,7 @@ import {
|
||||
ProjectFiltersBar,
|
||||
type ProjectFilters,
|
||||
} from './project-filters'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
|
||||
const statusColors: Record<
|
||||
string,
|
||||
@@ -584,7 +584,7 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" onClick={() => setAiTagDialogOpen(true)}>
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
<Tags className="mr-2 h-4 w-4" />
|
||||
AI Tags
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
@@ -983,7 +983,7 @@ export default function ProjectsPage() {
|
||||
/>
|
||||
</div>
|
||||
<Link href={`/admin/projects/${project.id}`} className="block">
|
||||
<Card className="transition-colors hover:bg-muted/50">
|
||||
<Card className="transition-all duration-200 hover:bg-muted/50 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start gap-3 pl-8">
|
||||
<ProjectLogo project={project} size="md" fallback="initials" />
|
||||
@@ -1051,7 +1051,7 @@ export default function ProjectsPage() {
|
||||
/>
|
||||
</div>
|
||||
<Link href={`/admin/projects/${project.id}`} className="block">
|
||||
<Card className={`transition-colors hover:bg-muted/50 h-full ${isEliminated ? 'opacity-60 bg-destructive/5' : ''}`}>
|
||||
<Card className={`transition-all duration-200 hover:bg-muted/50 hover:-translate-y-0.5 hover:shadow-md h-full ${isEliminated ? 'opacity-60 bg-destructive/5' : ''}`}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start gap-3 pl-7">
|
||||
<ProjectLogo project={project} size="lg" fallback="initials" />
|
||||
@@ -1483,7 +1483,7 @@ export default function ProjectsPage() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-amber-400 to-orange-500">
|
||||
<Sparkles className="h-5 w-5 text-white" />
|
||||
<Tags className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<span>AI Tag Generator</span>
|
||||
@@ -1723,7 +1723,7 @@ export default function ProjectsPage() {
|
||||
{taggingInProgress ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
<Tags className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{taggingInProgress ? 'Processing...' : 'Generate Tags'}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user