'use client' import { useState } from 'react' import { useParams } from 'next/navigation' import Link from 'next/link' import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle, } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { toast } from 'sonner' import { cn } from '@/lib/utils' import { ArrowLeft, ChevronDown, Layers, Users, FolderKanban, ClipboardList, Settings, MoreHorizontal, Archive, Loader2, Plus, CalendarDays, } from 'lucide-react' import { CompetitionTimeline } from '@/components/admin/competition/competition-timeline' const ROUND_TYPES = [ { value: 'INTAKE', label: 'Intake' }, { value: 'FILTERING', label: 'Filtering' }, { value: 'EVALUATION', label: 'Evaluation' }, { value: 'SUBMISSION', label: 'Submission' }, { value: 'MENTORING', label: 'Mentoring' }, { value: 'LIVE_FINAL', label: 'Live Final' }, { value: 'DELIBERATION', label: 'Deliberation' }, ] as const const statusConfig = { DRAFT: { label: 'Draft', bgClass: 'bg-gray-100 text-gray-700', dotClass: 'bg-gray-500', }, ACTIVE: { label: 'Active', bgClass: 'bg-emerald-100 text-emerald-700', dotClass: 'bg-emerald-500', }, CLOSED: { label: 'Closed', bgClass: 'bg-blue-100 text-blue-700', dotClass: 'bg-blue-500', }, ARCHIVED: { label: 'Archived', bgClass: 'bg-muted text-muted-foreground', dotClass: 'bg-muted-foreground', }, } as const const roundTypeColors: Record = { INTAKE: 'bg-gray-100 text-gray-700', FILTERING: 'bg-amber-100 text-amber-700', EVALUATION: 'bg-blue-100 text-blue-700', SUBMISSION: 'bg-purple-100 text-purple-700', MENTORING: 'bg-teal-100 text-teal-700', LIVE_FINAL: 'bg-red-100 text-red-700', DELIBERATION: 'bg-indigo-100 text-indigo-700', } export default function CompetitionDetailPage() { const params = useParams() const competitionId = params.competitionId as string const utils = trpc.useUtils() const [addRoundOpen, setAddRoundOpen] = useState(false) const [roundForm, setRoundForm] = useState({ name: '', roundType: '' as string, }) const { data: competition, isLoading } = trpc.competition.getById.useQuery( { id: competitionId }, { refetchInterval: 30_000 } ) const updateMutation = trpc.competition.update.useMutation({ onSuccess: () => { utils.competition.getById.invalidate({ id: competitionId }) toast.success('Competition updated') }, onError: (err) => toast.error(err.message), }) const createRoundMutation = trpc.round.create.useMutation({ onSuccess: () => { utils.competition.getById.invalidate({ id: competitionId }) toast.success('Round created') setAddRoundOpen(false) setRoundForm({ name: '', roundType: '' }) }, onError: (err) => toast.error(err.message), }) const handleStatusChange = (newStatus: 'DRAFT' | 'ACTIVE' | 'CLOSED' | 'ARCHIVED') => { updateMutation.mutate({ id: competitionId, status: newStatus }) } const handleCreateRound = () => { if (!roundForm.name.trim() || !roundForm.roundType) { toast.error('Name and type are required') return } const slug = roundForm.name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') const nextOrder = competition?.rounds.length ?? 0 createRoundMutation.mutate({ competitionId, name: roundForm.name.trim(), slug, roundType: roundForm.roundType as any, sortOrder: nextOrder, }) } if (isLoading) { return (
) } if (!competition) { return (

Competition Not Found

The requested competition does not exist

) } const status = competition.status as keyof typeof statusConfig const config = statusConfig[status] || statusConfig.DRAFT return (
{/* Header */}

{competition.name}

{(['DRAFT', 'ACTIVE', 'CLOSED'] as const).map((s) => ( handleStatusChange(s)} disabled={competition.status === s || updateMutation.isPending} > {s.charAt(0) + s.slice(1).toLowerCase()} ))} handleStatusChange('ARCHIVED')} disabled={competition.status === 'ARCHIVED' || updateMutation.isPending} > Archive

{competition.slug}

Assignments Deliberation handleStatusChange('ARCHIVED')} disabled={updateMutation.isPending} > {updateMutation.isPending ? ( ) : ( )} Archive
{/* Stats Cards */}
Rounds

{competition.rounds.filter((r: any) => !r.specialAwardId).length}

Juries

{competition.juryGroups.length}

Projects

{(competition as any).distinctProjectCount ?? 0}

Category

{competition.categoryMode}

{/* Tabs */} Overview Rounds Juries Settings {/* Overview Tab */} !r.specialAwardId)} /> {/* Rounds Tab */}

Rounds ({competition.rounds.filter((r: any) => !r.specialAwardId).length})

{competition.rounds.filter((r: any) => !r.specialAwardId).length === 0 ? ( No rounds configured. Add rounds to define the competition flow. ) : (
{competition.rounds.filter((r: any) => !r.specialAwardId).map((round: any, index: number) => { const projectCount = round._count?.projectRoundStates ?? 0 const assignmentCount = round._count?.assignments ?? 0 const statusLabel = round.status.replace('ROUND_', '') const statusColors: Record = { DRAFT: 'bg-gray-100 text-gray-600', ACTIVE: 'bg-emerald-100 text-emerald-700', CLOSED: 'bg-blue-100 text-blue-700', ARCHIVED: 'bg-muted text-muted-foreground', } return ( {/* Top: number + name + badges */}
{index + 1}

{round.name}

{round.roundType.replace(/_/g, ' ')} {statusLabel}
{/* Stats row */}
{projectCount} project{projectCount !== 1 ? 's' : ''}
{(round.roundType === 'EVALUATION' || round.roundType === 'FILTERING') && (
{assignmentCount} assignment{assignmentCount !== 1 ? 's' : ''}
)}
{/* Dates */} {(round.windowOpenAt || round.windowCloseAt) && (
{round.windowOpenAt ? new Date(round.windowOpenAt).toLocaleDateString() : '?'} {' \u2014 '} {round.windowCloseAt ? new Date(round.windowCloseAt).toLocaleDateString() : '?'}
)} {/* Jury group */} {round.juryGroup && (
{round.juryGroup.name}
)}
) })}
)}
{/* Juries Tab */}

Jury Groups ({competition.juryGroups.length})

{competition.juryGroups.length === 0 ? ( No jury groups configured. Create jury groups to assign evaluators. ) : (
{competition.juryGroups.map((group) => ( {group.name}
{group._count.members} members Cap: {group.defaultCapMode}
))}
)}
{/* Settings Tab */} Competition Settings

{competition.categoryMode}

{competition.startupFinalistCount}

{competition.conceptFinalistCount}

{competition.notifyOnDeadlineApproach && ( Deadline Approach )}
{competition.deadlineReminderDays && (

{(competition.deadlineReminderDays as number[]).join(', ')} days before deadline

)}
{/* Add Round Dialog */} Add Round Add a new round to this competition. It will be appended to the current round sequence.
setRoundForm({ ...roundForm, name: e.target.value })} />
) }