Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
Replace Pipeline/Stage system with Competition/Round architecture. New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy, ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow. New services: round-engine, round-assignment, deliberation, result-lock, submission-manager, competition-context, ai-prompt-guard. Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with structured prompts, retry logic, and injection detection. All legacy pipeline/stage code removed. 4 new migrations + seed aligned. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
156
src/components/admin/competition/competition-timeline.tsx
Normal file
156
src/components/admin/competition/competition-timeline.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import type { Route } from 'next'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { format } from 'date-fns'
|
||||
import { CheckCircle2, Circle, Clock } from 'lucide-react'
|
||||
|
||||
const roundTypeColors: Record<string, string> = {
|
||||
INTAKE: 'bg-gray-100 text-gray-700 border-gray-300',
|
||||
FILTERING: 'bg-amber-100 text-amber-700 border-amber-300',
|
||||
EVALUATION: 'bg-blue-100 text-blue-700 border-blue-300',
|
||||
SUBMISSION: 'bg-purple-100 text-purple-700 border-purple-300',
|
||||
MENTORING: 'bg-teal-100 text-teal-700 border-teal-300',
|
||||
LIVE_FINAL: 'bg-red-100 text-red-700 border-red-300',
|
||||
DELIBERATION: 'bg-indigo-100 text-indigo-700 border-indigo-300',
|
||||
}
|
||||
|
||||
const roundStatusConfig: Record<string, { icon: typeof Circle; color: string }> = {
|
||||
ROUND_DRAFT: { icon: Circle, color: 'text-gray-400' },
|
||||
ROUND_ACTIVE: { icon: Clock, color: 'text-emerald-500' },
|
||||
ROUND_CLOSED: { icon: CheckCircle2, color: 'text-blue-500' },
|
||||
ROUND_ARCHIVED: { icon: CheckCircle2, color: 'text-gray-400' },
|
||||
}
|
||||
|
||||
type RoundSummary = {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
roundType: string
|
||||
status: string
|
||||
sortOrder: number
|
||||
windowOpenAt: Date | string | null
|
||||
windowCloseAt: Date | string | null
|
||||
}
|
||||
|
||||
export function CompetitionTimeline({
|
||||
competitionId,
|
||||
rounds,
|
||||
}: {
|
||||
competitionId: string
|
||||
rounds: RoundSummary[]
|
||||
}) {
|
||||
if (rounds.length === 0) {
|
||||
return (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
No rounds configured yet. Add rounds to see the competition timeline.
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">Round Timeline</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Desktop: horizontal timeline */}
|
||||
<div className="hidden md:block overflow-x-auto pb-2">
|
||||
<div className="flex items-start gap-0 min-w-max">
|
||||
{rounds.map((round, index) => {
|
||||
const statusCfg = roundStatusConfig[round.status] ?? roundStatusConfig.ROUND_DRAFT
|
||||
const StatusIcon = statusCfg.icon
|
||||
const isLast = index === rounds.length - 1
|
||||
|
||||
return (
|
||||
<div key={round.id} className="flex items-start">
|
||||
<Link
|
||||
href={`/admin/competitions/${competitionId}/rounds/${round.id}` as Route}
|
||||
className="group flex flex-col items-center text-center w-32 shrink-0"
|
||||
>
|
||||
<div className="relative">
|
||||
<StatusIcon className={cn('h-6 w-6', statusCfg.color)} />
|
||||
</div>
|
||||
<p className="mt-2 text-xs font-medium group-hover:text-primary transition-colors line-clamp-2">
|
||||
{round.name}
|
||||
</p>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
'mt-1 text-[9px]',
|
||||
roundTypeColors[round.roundType] ?? ''
|
||||
)}
|
||||
>
|
||||
{round.roundType.replace('_', ' ')}
|
||||
</Badge>
|
||||
{round.windowOpenAt && (
|
||||
<p className="mt-1 text-[10px] text-muted-foreground">
|
||||
{format(new Date(round.windowOpenAt), 'MMM d')}
|
||||
{round.windowCloseAt && (
|
||||
<> - {format(new Date(round.windowCloseAt), 'MMM d')}</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</Link>
|
||||
{!isLast && (
|
||||
<div className="mt-3 h-px w-8 bg-border shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile: vertical timeline */}
|
||||
<div className="md:hidden space-y-0">
|
||||
{rounds.map((round, index) => {
|
||||
const statusCfg = roundStatusConfig[round.status] ?? roundStatusConfig.ROUND_DRAFT
|
||||
const StatusIcon = statusCfg.icon
|
||||
const isLast = index === rounds.length - 1
|
||||
|
||||
return (
|
||||
<div key={round.id}>
|
||||
<Link
|
||||
href={`/admin/competitions/${competitionId}/rounds/${round.id}` as Route}
|
||||
className="flex items-start gap-3 py-2 hover:bg-muted/50 rounded-md px-2 -mx-2 transition-colors"
|
||||
>
|
||||
<div className="flex flex-col items-center shrink-0">
|
||||
<StatusIcon className={cn('h-5 w-5', statusCfg.color)} />
|
||||
{!isLast && <div className="w-px flex-1 bg-border mt-1 min-h-[16px]" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium truncate">{round.name}</p>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
'text-[9px] shrink-0',
|
||||
roundTypeColors[round.roundType] ?? ''
|
||||
)}
|
||||
>
|
||||
{round.roundType.replace('_', ' ')}
|
||||
</Badge>
|
||||
</div>
|
||||
{round.windowOpenAt && (
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">
|
||||
{format(new Date(round.windowOpenAt), 'MMM d, yyyy')}
|
||||
{round.windowCloseAt && (
|
||||
<> - {format(new Date(round.windowCloseAt), 'MMM d, yyyy')}</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user