Admin dashboard & round management UX overhaul
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m43s
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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user