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

@@ -1,6 +1,7 @@
'use client'
import Link from 'next/link'
import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
@@ -9,6 +10,17 @@ import {
AlertTriangle,
Upload,
UserPlus,
Settings,
ClipboardCheck,
Users,
Send,
FileDown,
Calendar,
Eye,
Presentation,
Vote,
Play,
Lock,
} from 'lucide-react'
import { GeographicSummaryCard } from '@/components/charts'
import { AnimatedCard } from '@/components/shared/animated-container'
@@ -29,6 +41,77 @@ type DashboardContentProps = {
sessionName: string
}
type QuickAction = {
label: string
href: string
icon: React.ElementType
}
function getContextualActions(
activeRound: { id: string; roundType: string } | null
): QuickAction[] {
if (!activeRound) {
return [
{ label: 'Rounds', href: '/admin/rounds', icon: CircleDot },
{ label: 'Import', href: '/admin/projects/new', icon: Upload },
{ label: 'Invite', href: '/admin/members', icon: UserPlus },
]
}
const roundHref = `/admin/rounds/${activeRound.id}`
switch (activeRound.roundType) {
case 'INTAKE':
return [
{ label: 'Import Projects', href: '/admin/projects/new', icon: Upload },
{ label: 'Review', href: roundHref, icon: ClipboardCheck },
{ label: 'Configure', href: `${roundHref}?tab=config`, icon: Settings },
]
case 'FILTERING':
return [
{ label: 'Run Screening', href: roundHref, icon: ClipboardCheck },
{ label: 'Review Results', href: `${roundHref}?tab=filtering`, icon: Eye },
{ label: 'Configure', href: `${roundHref}?tab=config`, icon: Settings },
]
case 'EVALUATION':
return [
{ label: 'Assignments', href: `${roundHref}?tab=assignments`, icon: Users },
{ label: 'Send Reminders', href: `${roundHref}?tab=assignments`, icon: Send },
{ label: 'Export', href: roundHref, icon: FileDown },
]
case 'SUBMISSION':
return [
{ label: 'Submissions', href: roundHref, icon: ClipboardCheck },
{ label: 'Deadlines', href: `${roundHref}?tab=config`, icon: Calendar },
{ label: 'Status', href: `${roundHref}?tab=projects`, icon: Eye },
]
case 'MENTORING':
return [
{ label: 'Mentors', href: `${roundHref}?tab=projects`, icon: Users },
{ label: 'Progress', href: roundHref, icon: Eye },
{ label: 'Configure', href: `${roundHref}?tab=config`, icon: Settings },
]
case 'LIVE_FINAL':
return [
{ label: 'Live Control', href: roundHref, icon: Presentation },
{ label: 'Results', href: `${roundHref}?tab=projects`, icon: Vote },
{ label: 'Configure', href: `${roundHref}?tab=config`, icon: Settings },
]
case 'DELIBERATION':
return [
{ label: 'Sessions', href: roundHref, icon: Play },
{ label: 'Results', href: `${roundHref}?tab=projects`, icon: Eye },
{ label: 'Lock Results', href: roundHref, icon: Lock },
]
default:
return [
{ label: 'Rounds', href: '/admin/rounds', icon: CircleDot },
{ label: 'Import', href: '/admin/projects/new', icon: Upload },
{ label: 'Invite', href: '/admin/members', icon: UserPlus },
]
}
}
export function DashboardContent({ editionId, sessionName }: DashboardContentProps) {
const { data, isLoading, error } = trpc.dashboard.getStats.useQuery(
{ editionId },
@@ -83,6 +166,7 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
evaluationStats,
totalAssignments,
latestProjects,
recentlyActiveProjects,
categoryBreakdown,
oceanIssueBreakdown,
recentActivity,
@@ -92,6 +176,17 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
? pipelineRounds.find((r) => r.id === activeRoundId) ?? null
: null
// Find next draft round for summary panel
const lastActiveSortOrder = Math.max(
...pipelineRounds.filter((r) => r.status === 'ROUND_ACTIVE').map((r) => r.sortOrder),
-1
)
const nextDraftRound = pipelineRounds.find(
(r) => r.status === 'ROUND_DRAFT' && r.sortOrder > lastActiveSortOrder
) ?? null
const quickActions = getContextualActions(activeRound)
return (
<>
{/* Page Header */}
@@ -109,25 +204,15 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
Welcome back, {sessionName}
</p>
</div>
<div className="flex gap-2">
<Link href="/admin/rounds">
<Button size="sm" variant="outline">
<CircleDot className="mr-1.5 h-3.5 w-3.5" />
Rounds
</Button>
</Link>
<Link href="/admin/projects/new">
<Button size="sm" variant="outline">
<Upload className="mr-1.5 h-3.5 w-3.5" />
Import
</Button>
</Link>
<Link href="/admin/members">
<Button size="sm" variant="outline">
<UserPlus className="mr-1.5 h-3.5 w-3.5" />
Invite
</Button>
</Link>
<div className="flex flex-wrap gap-2">
{quickActions.map((action) => (
<Link key={action.label} href={action.href as Route}>
<Button size="sm" variant="outline">
<action.icon className="mr-1.5 h-3.5 w-3.5" />
{action.label}
</Button>
</Link>
))}
</div>
</motion.div>
@@ -147,6 +232,7 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
totalAssignments={totalAssignments}
evaluationStats={evaluationStats}
actionsCount={nextActions.length}
nextDraftRound={nextDraftRound ? { name: nextDraftRound.name, roundType: nextDraftRound.roundType } : null}
/>
</AnimatedCard>
@@ -161,7 +247,11 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
)}
<AnimatedCard index={3}>
<ProjectListCompact projects={latestProjects} />
<ProjectListCompact
projects={latestProjects}
activeProjects={recentlyActiveProjects}
mode={activeRound && activeRound.roundType !== 'INTAKE' ? 'active' : 'recent'}
/>
</AnimatedCard>
{recentEvals && recentEvals.length > 0 && (