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:
@@ -5,14 +5,9 @@ import type { Route } from 'next'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { motion } from 'motion/react'
|
||||
import {
|
||||
Upload,
|
||||
Filter,
|
||||
ClipboardCheck,
|
||||
FileUp,
|
||||
GraduationCap,
|
||||
Radio,
|
||||
Scale,
|
||||
} from 'lucide-react'
|
||||
roundTypeConfig as sharedRoundTypeConfig,
|
||||
roundStatusConfig as sharedRoundStatusConfig,
|
||||
} from '@/lib/round-config'
|
||||
|
||||
type PipelineRound = {
|
||||
id: string
|
||||
@@ -55,66 +50,11 @@ type PipelineRound = {
|
||||
deliberationCount: number
|
||||
}
|
||||
|
||||
const roundTypeConfig: Record<
|
||||
string,
|
||||
{ icon: typeof Upload; iconColor: string; iconBg: string }
|
||||
> = {
|
||||
INTAKE: { icon: Upload, iconColor: 'text-sky-600', iconBg: 'bg-sky-100' },
|
||||
FILTERING: {
|
||||
icon: Filter,
|
||||
iconColor: 'text-amber-600',
|
||||
iconBg: 'bg-amber-100',
|
||||
},
|
||||
EVALUATION: {
|
||||
icon: ClipboardCheck,
|
||||
iconColor: 'text-violet-600',
|
||||
iconBg: 'bg-violet-100',
|
||||
},
|
||||
SUBMISSION: {
|
||||
icon: FileUp,
|
||||
iconColor: 'text-blue-600',
|
||||
iconBg: 'bg-blue-100',
|
||||
},
|
||||
MENTORING: {
|
||||
icon: GraduationCap,
|
||||
iconColor: 'text-teal-600',
|
||||
iconBg: 'bg-teal-100',
|
||||
},
|
||||
LIVE_FINAL: {
|
||||
icon: Radio,
|
||||
iconColor: 'text-red-600',
|
||||
iconBg: 'bg-red-100',
|
||||
},
|
||||
DELIBERATION: {
|
||||
icon: Scale,
|
||||
iconColor: 'text-indigo-600',
|
||||
iconBg: 'bg-indigo-100',
|
||||
},
|
||||
}
|
||||
const roundTypeConfig = sharedRoundTypeConfig
|
||||
|
||||
const statusStyles: Record<
|
||||
string,
|
||||
{ container: string; label: string }
|
||||
> = {
|
||||
ROUND_DRAFT: {
|
||||
container:
|
||||
'bg-slate-50 border-slate-200 text-slate-400 border-dashed',
|
||||
label: 'Draft',
|
||||
},
|
||||
ROUND_ACTIVE: {
|
||||
container:
|
||||
'bg-blue-50 border-blue-300 text-blue-700 ring-2 ring-blue-400/30 shadow-lg shadow-blue-500/10',
|
||||
label: 'Active',
|
||||
},
|
||||
ROUND_CLOSED: {
|
||||
container: 'bg-emerald-50 border-emerald-200 text-emerald-600',
|
||||
label: 'Closed',
|
||||
},
|
||||
ROUND_ARCHIVED: {
|
||||
container: 'bg-slate-50/50 border-slate-100 text-slate-300',
|
||||
label: 'Archived',
|
||||
},
|
||||
}
|
||||
const statusStyles: Record<string, { container: string; label: string }> = Object.fromEntries(
|
||||
Object.entries(sharedRoundStatusConfig).map(([k, v]) => [k, { container: v.pipelineContainer, label: v.label }])
|
||||
)
|
||||
|
||||
function getMetric(round: PipelineRound): string {
|
||||
const { roundType, projectStates, filteringTotal, filteringPassed, evalTotal, evalSubmitted, assignmentCount, liveSessionStatus, deliberationCount } = round
|
||||
@@ -147,6 +87,30 @@ function getMetric(round: PipelineRound): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getProgressPct(round: PipelineRound): number | null {
|
||||
if (round.status !== 'ROUND_ACTIVE') return null
|
||||
|
||||
switch (round.roundType) {
|
||||
case 'FILTERING': {
|
||||
const processed = round.filteringPassed + round.filteringRejected + round.filteringFlagged
|
||||
const total = round.projectStates.total || round.filteringTotal
|
||||
return total > 0 ? Math.round((processed / total) * 100) : 0
|
||||
}
|
||||
case 'EVALUATION':
|
||||
return round.evalTotal > 0 ? Math.round((round.evalSubmitted / round.evalTotal) * 100) : 0
|
||||
case 'SUBMISSION': {
|
||||
const total = round.projectStates.total
|
||||
return total > 0 ? Math.round((round.projectStates.COMPLETED / total) * 100) : 0
|
||||
}
|
||||
case 'MENTORING': {
|
||||
const total = round.projectStates.total
|
||||
return total > 0 ? Math.round((round.projectStates.COMPLETED / total) * 100) : 0
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function PipelineRoundNode({
|
||||
round,
|
||||
index,
|
||||
@@ -158,7 +122,9 @@ export function PipelineRoundNode({
|
||||
const Icon = typeConfig.icon
|
||||
const status = statusStyles[round.status] ?? statusStyles.ROUND_DRAFT
|
||||
const isActive = round.status === 'ROUND_ACTIVE'
|
||||
const isCompleted = round.status === 'ROUND_CLOSED' || round.status === 'ROUND_ARCHIVED'
|
||||
const metric = getMetric(round)
|
||||
const progressPct = getProgressPct(round)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -172,8 +138,8 @@ export function PipelineRoundNode({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-col items-center rounded-xl border-2 p-3 transition-all hover:-translate-y-0.5 hover:shadow-md',
|
||||
isActive ? 'w-44' : 'w-36',
|
||||
'relative flex flex-col items-center rounded-xl border-2 transition-all hover:-translate-y-0.5 hover:shadow-md',
|
||||
isActive ? 'w-48 px-4 py-4' : 'w-40 px-3 py-3.5',
|
||||
status.container
|
||||
)}
|
||||
>
|
||||
@@ -185,30 +151,64 @@ export function PipelineRoundNode({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Completed check */}
|
||||
{isCompleted && (
|
||||
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-emerald-500 text-white">
|
||||
<svg className="h-2.5 w-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg',
|
||||
'flex items-center justify-center rounded-lg',
|
||||
isActive ? 'h-10 w-10' : 'h-9 w-9',
|
||||
typeConfig.iconBg
|
||||
)}
|
||||
>
|
||||
<Icon className={cn('h-4 w-4', typeConfig.iconColor)} />
|
||||
<Icon className={cn(isActive ? 'h-5 w-5' : 'h-4 w-4', typeConfig.iconColor)} />
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<p className="mt-2 text-center text-xs font-semibold leading-tight line-clamp-2 group-hover:text-foreground transition-colors">
|
||||
<p className="mt-2.5 text-center text-xs font-semibold leading-tight line-clamp-2 group-hover:text-foreground transition-colors">
|
||||
{round.name}
|
||||
</p>
|
||||
|
||||
{/* Type label */}
|
||||
<span className="mt-1 text-[10px] font-medium text-muted-foreground/70">
|
||||
{typeConfig.label}
|
||||
</span>
|
||||
|
||||
{/* Status label */}
|
||||
<span className="mt-1.5 text-[10px] font-medium uppercase tracking-wider opacity-70">
|
||||
<span className="mt-1 text-[10px] font-semibold uppercase tracking-wider opacity-70">
|
||||
{status.label}
|
||||
</span>
|
||||
|
||||
{/* Progress bar for active rounds */}
|
||||
{progressPct !== null && (
|
||||
<div className="mt-2 w-full">
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-black/5">
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-blue-500"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progressPct}%` }}
|
||||
transition={{ duration: 0.8, ease: 'easeOut' }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-0.5 text-center text-[10px] font-medium tabular-nums text-blue-600">
|
||||
{progressPct}%
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metric */}
|
||||
<p className="mt-1 text-[11px] font-medium tabular-nums opacity-80">
|
||||
{metric}
|
||||
</p>
|
||||
{progressPct === null && (
|
||||
<p className="mt-1.5 text-[11px] font-medium tabular-nums opacity-80">
|
||||
{metric}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user