Admin dashboard & round management UX overhaul
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m43s

- Extract round detail monolith (2900→600 lines) into 13 standalone components
- Add shared round/status config (round-config.ts) replacing 4 local copies
- Delete 12 legacy competition-scoped pages, merge project pool into projects page
- Add round-type-specific dashboard stat panels (submission, mentoring, live final, deliberation, summary)
- Add contextual header quick actions based on active round type
- Improve pipeline visualization: progress bars, checkmarks, chevron connectors, overflow fix
- Add config tab completion dots (green/amber/red) and inline validation warnings
- Enhance juries page with round assignments, member avatars, and cap mode badges
- Add context-aware project list (recent submissions vs active evaluations)
- Move competition settings into Manage Editions page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 17:14:00 +01:00
parent f7bc3b4dd2
commit f26ee3f076
51 changed files with 4530 additions and 6276 deletions

View File

@@ -34,8 +34,8 @@ import {
} from '@/components/ui/dialog'
import { Textarea } from '@/components/ui/textarea'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import { Plus, Scale, Users, Loader2 } from 'lucide-react'
import { cn, formatEnumLabel } from '@/lib/utils'
import { Plus, Scale, Users, Loader2, ArrowRight, CircleDot } from 'lucide-react'
const capModeLabels = {
HARD: 'Hard Cap',
@@ -267,33 +267,82 @@ function CompetitionJuriesSection({ competition }: CompetitionJuriesSectionProps
No jury groups configured for this competition.
</p>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<div className="space-y-3">
{juryGroups.map((group) => (
<Link key={group.id} href={`/admin/juries/${group.id}` as Route}>
<Card className="transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md cursor-pointer">
<Card className="transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md cursor-pointer group">
<CardContent className="p-4">
<div className="space-y-2">
<div className="space-y-3">
{/* Header row */}
<div className="flex items-start justify-between gap-2">
<h3 className="font-semibold text-sm line-clamp-1">{group.name}</h3>
<Badge
variant="secondary"
className={cn('text-[10px] shrink-0', capModeColors[group.defaultCapMode as keyof typeof capModeColors])}
>
{capModeLabels[group.defaultCapMode as keyof typeof capModeLabels]}
</Badge>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<Users className="h-3 w-3" />
<span>{group._count.members} members</span>
<div className="flex items-center gap-2 min-w-0">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-brand-blue/10 shrink-0">
<Scale className="h-4 w-4 text-brand-blue" />
</div>
<div className="min-w-0">
<h3 className="font-semibold text-sm line-clamp-1 group-hover:text-brand-blue transition-colors">
{group.name}
</h3>
<p className="text-xs text-muted-foreground">
{group._count.members} member{group._count.members !== 1 ? 's' : ''}
{' · '}
{group._count.assignments} assignment{group._count.assignments !== 1 ? 's' : ''}
</p>
</div>
</div>
<div>
{group._count.assignments} assignments
<div className="flex items-center gap-2 shrink-0">
<Badge
variant="secondary"
className={cn('text-[10px]', capModeColors[group.defaultCapMode as keyof typeof capModeColors])}
>
{capModeLabels[group.defaultCapMode as keyof typeof capModeLabels]}
</Badge>
<ArrowRight className="h-4 w-4 text-muted-foreground/40 group-hover:text-brand-blue transition-colors" />
</div>
</div>
<div className="text-xs text-muted-foreground">
Default max: {group.defaultMaxAssignments}
</div>
{/* Round assignments */}
{(group as any).rounds?.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{(group as any).rounds.map((r: any) => (
<Badge
key={r.id}
variant="outline"
className={cn(
'text-[10px] gap-1',
r.status === 'ROUND_ACTIVE' && 'border-blue-300 bg-blue-50 text-blue-700',
r.status === 'ROUND_CLOSED' && 'border-emerald-300 bg-emerald-50 text-emerald-700',
r.status === 'ROUND_DRAFT' && 'border-slate-200 text-slate-500',
)}
>
<CircleDot className="h-2.5 w-2.5" />
{r.name}
</Badge>
))}
</div>
)}
{/* Member preview */}
{(group as any).members?.length > 0 && (
<div className="flex items-center gap-1.5">
<div className="flex -space-x-1.5">
{(group as any).members.slice(0, 5).map((m: any) => (
<div
key={m.id}
className="h-6 w-6 rounded-full bg-brand-blue/10 border-2 border-white flex items-center justify-center text-[9px] font-semibold text-brand-blue"
title={m.user?.name || m.user?.email}
>
{(m.user?.name || m.user?.email || '?').charAt(0).toUpperCase()}
</div>
))}
</div>
{group._count.members > 5 && (
<span className="text-[10px] text-muted-foreground">
+{group._count.members - 5} more
</span>
)}
</div>
)}
</div>
</CardContent>
</Card>