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:
2026-02-11 13:20:52 +01:00
parent 98f4a957cc
commit ce4069bf92
59 changed files with 1949 additions and 913 deletions

View File

@@ -39,6 +39,7 @@ import {
Search,
} from 'lucide-react'
import { formatDateOnly } from '@/lib/utils'
import { AnimatedCard } from '@/components/shared/animated-container'
// Status badge colors
const statusColors: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
@@ -117,63 +118,72 @@ export default function MentorDashboard() {
{/* Stats */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Assigned Projects
</CardTitle>
<Briefcase className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{projects.length}</div>
<p className="text-xs text-muted-foreground">
Projects you are mentoring
</p>
</CardContent>
</Card>
<AnimatedCard index={0}>
<Card className="border-l-4 border-l-blue-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Assigned Projects</p>
<p className="text-2xl font-bold mt-1">{projects.length}</p>
<p className="text-xs text-muted-foreground mt-1">Projects you are mentoring</p>
</div>
<div className="rounded-xl bg-blue-50 p-3">
<Briefcase className="h-5 w-5 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
</AnimatedCard>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Completed
</CardTitle>
<CheckCircle2 className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{completedCount}</div>
<div className="flex items-center gap-2 mt-1">
{projects.length > 0 && (
<Progress
value={(completedCount / projects.length) * 100}
className="h-1.5 flex-1"
/>
)}
<span className="text-xs text-muted-foreground">
{projects.length > 0 ? Math.round((completedCount / projects.length) * 100) : 0}%
</span>
</div>
</CardContent>
</Card>
<AnimatedCard index={1}>
<Card className="border-l-4 border-l-emerald-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Completed</p>
<p className="text-2xl font-bold mt-1">{completedCount}</p>
<div className="flex items-center gap-2 mt-1">
{projects.length > 0 && (
<Progress
value={(completedCount / projects.length) * 100}
className="h-1.5 flex-1"
gradient
/>
)}
<span className="text-xs text-muted-foreground">
{projects.length > 0 ? Math.round((completedCount / projects.length) * 100) : 0}%
</span>
</div>
</div>
<div className="rounded-xl bg-emerald-50 p-3">
<CheckCircle2 className="h-5 w-5 text-emerald-600" />
</div>
</div>
</CardContent>
</Card>
</AnimatedCard>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Team Members
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{projects.reduce(
(acc, a) => acc + (a.project.teamMembers?.length || 0),
0
)}
</div>
<p className="text-xs text-muted-foreground">
Across all assigned projects
</p>
</CardContent>
</Card>
<AnimatedCard index={2}>
<Card className="border-l-4 border-l-violet-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Total Team Members</p>
<p className="text-2xl font-bold mt-1">
{projects.reduce(
(acc, a) => acc + (a.project.teamMembers?.length || 0),
0
)}
</p>
<p className="text-xs text-muted-foreground mt-1">Across all assigned projects</p>
</div>
<div className="rounded-xl bg-violet-50 p-3">
<Users className="h-5 w-5 text-violet-600" />
</div>
</div>
</CardContent>
</Card>
</AnimatedCard>
</div>
{/* Quick Actions */}
@@ -219,8 +229,8 @@ export default function MentorDashboard() {
{projects.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
<Users className="h-6 w-6 text-muted-foreground" />
<div className="rounded-2xl bg-brand-teal/10 p-4">
<Users className="h-8 w-8 text-brand-teal" />
</div>
<p className="mt-4 font-medium">No assigned projects yet</p>
<p className="text-sm text-muted-foreground mt-1">
@@ -248,7 +258,7 @@ export default function MentorDashboard() {
</Card>
) : (
<div className="grid gap-4">
{filteredProjects.map((assignment) => {
{filteredProjects.map((assignment, index) => {
const project = assignment.project
const teamLead = project.teamMembers?.find(
(m) => m.role === 'LEAD'
@@ -256,7 +266,8 @@ export default function MentorDashboard() {
const badge = completionBadge[assignment.completionStatus] || completionBadge.in_progress
return (
<Card key={assignment.id}>
<AnimatedCard key={assignment.id} index={index}>
<Card className="transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardHeader>
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
@@ -376,6 +387,7 @@ export default function MentorDashboard() {
</div>
</CardContent>
</Card>
</AnimatedCard>
)
})}
</div>

View File

@@ -28,6 +28,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { AnimatedCard } from '@/components/shared/animated-container'
import { FileViewer } from '@/components/shared/file-viewer'
import { MentorChat } from '@/components/shared/mentor-chat'
import { ProjectLogoWithUrl } from '@/components/shared/project-logo-with-url'
@@ -194,21 +195,31 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
{/* Milestones Section */}
{programId && mentorAssignmentId && (
<AnimatedCard index={0}>
<MilestonesSection
programId={programId}
mentorAssignmentId={mentorAssignmentId}
/>
</AnimatedCard>
)}
{/* Private Notes Section */}
{mentorAssignmentId && (
<NotesSection mentorAssignmentId={mentorAssignmentId} />
<AnimatedCard index={1}>
<NotesSection mentorAssignmentId={mentorAssignmentId} />
</AnimatedCard>
)}
{/* Project Info */}
<AnimatedCard index={2}>
<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 */}
@@ -299,12 +310,16 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
)}
</CardContent>
</Card>
</AnimatedCard>
{/* Team Members Section */}
<AnimatedCard index={3}>
<Card>
<CardHeader>
<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 || 0})
</CardTitle>
<CardDescription>
@@ -392,12 +407,16 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
)}
</CardContent>
</Card>
</AnimatedCard>
{/* Files Section */}
<AnimatedCard index={4}>
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<FileText 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">
<FileText className="h-4 w-4 text-rose-500" />
</div>
Project Files
</CardTitle>
<CardDescription>
@@ -426,12 +445,16 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
)}
</CardContent>
</Card>
</AnimatedCard>
{/* Messaging Section */}
<AnimatedCard index={5}>
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
<CardTitle className="flex items-center gap-2.5 text-lg">
<div className="rounded-lg bg-blue-500/10 p-1.5">
<MessageSquare className="h-4 w-4 text-blue-500" />
</div>
Messages
</CardTitle>
<CardDescription>
@@ -450,6 +473,7 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
/>
</CardContent>
</Card>
</AnimatedCard>
</div>
)
}
@@ -529,8 +553,10 @@ function MilestonesSection({
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<Target className="h-5 w-5" />
<CardTitle className="flex items-center gap-2.5 text-lg">
<div className="rounded-lg bg-amber-500/10 p-1.5">
<Target className="h-4 w-4 text-amber-500" />
</div>
Milestones
</CardTitle>
<Badge variant="secondary">
@@ -552,7 +578,7 @@ function MilestonesSection({
return (
<div
key={milestone.id}
className={`flex items-start gap-3 p-3 rounded-lg border transition-colors ${
className={`flex items-start gap-3 p-3 rounded-lg border transition-all duration-200 hover:-translate-y-0.5 hover:shadow-sm ${
isCompleted ? 'bg-green-50/50 border-green-200 dark:bg-green-950/20 dark:border-green-900' : ''
}`}
>
@@ -676,8 +702,10 @@ function NotesSection({ mentorAssignmentId }: { mentorAssignmentId: string }) {
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<StickyNote className="h-5 w-5" />
<CardTitle className="flex items-center gap-2.5 text-lg">
<div className="rounded-lg bg-amber-500/10 p-1.5">
<StickyNote className="h-4 w-4 text-amber-500" />
</div>
Private Notes
</CardTitle>
{!isAdding && !editingId && (