Files
MOPC-Portal/src/app/(admin)/admin/rounds/page.tsx
Matt de73a6f080
All checks were successful
Build and Push Docker Image / build (push) Successful in 12m44s
Rounds page: flat pipeline view with awards branching visualization
Redesign the rounds page from a card-per-competition layout to a flat
pipeline/timeline view. Rounds display as compact rows connected by a
vertical track with status dots (pulsing green for active). Special
awards branch off to the right from their linked evaluation round with
connector lines and tooltip details. Competition settings moved to a
dialog behind a gear icon. Filter pills replace the dropdown selector.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:19:50 +01:00

741 lines
32 KiB
TypeScript

'use client'
import { useState, useMemo } from 'react'
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 { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import {
Plus,
Calendar,
Settings,
Users,
FileBox,
Save,
Loader2,
Award,
Trophy,
ArrowRight,
} from 'lucide-react'
import { useEdition } from '@/contexts/edition-context'
// ─── Constants ───────────────────────────────────────────────────────────────
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 ROUND_TYPE_COLORS: Record<string, { dot: string; bg: string; text: string; border: string }> = {
INTAKE: { dot: '#9ca3af', bg: 'bg-gray-50', text: 'text-gray-600', border: 'border-gray-300' },
FILTERING: { dot: '#f59e0b', bg: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-300' },
EVALUATION: { dot: '#3b82f6', bg: 'bg-blue-50', text: 'text-blue-700', border: 'border-blue-300' },
SUBMISSION: { dot: '#8b5cf6', bg: 'bg-purple-50', text: 'text-purple-700', border: 'border-purple-300' },
MENTORING: { dot: '#557f8c', bg: 'bg-teal-50', text: 'text-teal-700', border: 'border-teal-300' },
LIVE_FINAL: { dot: '#de0f1e', bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-300' },
DELIBERATION: { dot: '#6366f1', bg: 'bg-indigo-50', text: 'text-indigo-700', border: 'border-indigo-300' },
}
const ROUND_STATUS_STYLES: Record<string, { color: string; label: string; pulse?: boolean }> = {
ROUND_DRAFT: { color: '#9ca3af', label: 'Draft' },
ROUND_ACTIVE: { color: '#10b981', label: 'Active', pulse: true },
ROUND_CLOSED: { color: '#3b82f6', label: 'Closed' },
ROUND_ARCHIVED: { color: '#6b7280', label: 'Archived' },
}
const AWARD_STATUS_COLORS: Record<string, string> = {
DRAFT: 'text-gray-500',
NOMINATIONS_OPEN: 'text-amber-600',
VOTING_OPEN: 'text-emerald-600',
CLOSED: 'text-blue-600',
ARCHIVED: 'text-gray-400',
}
// ─── Types ───────────────────────────────────────────────────────────────────
type RoundWithStats = {
id: string
name: string
slug: string
roundType: string
status: string
sortOrder: number
windowOpenAt: string | null
windowCloseAt: string | null
juryGroup: { id: string; name: string } | null
_count: { projectRoundStates: number; assignments: number }
}
type SpecialAwardItem = {
id: string
name: string
status: string
evaluationRoundId: string | null
eligibilityMode: string
_count: { eligibilities: number; jurors: number; votes: number }
winnerProject: { id: string; title: string; teamName: string | null } | null
}
// ─── Main Page ───────────────────────────────────────────────────────────────
export default function RoundsPage() {
const { currentEdition } = useEdition()
const programId = currentEdition?.id
const utils = trpc.useUtils()
const [addRoundOpen, setAddRoundOpen] = useState(false)
const [roundForm, setRoundForm] = useState({ name: '', roundType: '', competitionId: '' })
const [settingsOpen, setSettingsOpen] = useState(false)
const [competitionEdits, setCompetitionEdits] = useState<Record<string, unknown>>({})
const [editingCompId, setEditingCompId] = useState<string | null>(null)
const [filterType, setFilterType] = useState<string>('all')
const { data: competitions, isLoading } = trpc.competition.list.useQuery(
{ programId: programId! },
{ enabled: !!programId }
)
// Use the first (and usually only) competition
const comp = competitions?.[0]
const { data: compDetail, isLoading: isLoadingDetail } = trpc.competition.getById.useQuery(
{ id: comp?.id! },
{ enabled: !!comp?.id }
)
const { data: awards } = trpc.specialAward.list.useQuery(
{ programId: programId! },
{ enabled: !!programId }
)
const createRoundMutation = trpc.round.create.useMutation({
onSuccess: () => {
utils.competition.list.invalidate()
utils.competition.getById.invalidate()
toast.success('Round created')
setAddRoundOpen(false)
setRoundForm({ name: '', roundType: '', competitionId: '' })
},
onError: (err) => toast.error(err.message),
})
const updateCompMutation = trpc.competition.update.useMutation({
onSuccess: () => {
utils.competition.list.invalidate()
utils.competition.getById.invalidate()
toast.success('Settings saved')
setEditingCompId(null)
setCompetitionEdits({})
setSettingsOpen(false)
},
onError: (err) => toast.error(err.message),
})
const rounds = useMemo(() => {
const all = (compDetail?.rounds ?? []) as RoundWithStats[]
return filterType === 'all' ? all : all.filter((r) => r.roundType === filterType)
}, [compDetail?.rounds, filterType])
// Group awards by their evaluationRoundId
const awardsByRound = useMemo(() => {
const map = new Map<string, SpecialAwardItem[]>()
for (const award of (awards ?? []) as SpecialAwardItem[]) {
if (award.evaluationRoundId) {
const existing = map.get(award.evaluationRoundId) ?? []
existing.push(award)
map.set(award.evaluationRoundId, existing)
}
}
return map
}, [awards])
const floatingAwards = useMemo(() => {
return ((awards ?? []) as SpecialAwardItem[]).filter((a) => !a.evaluationRoundId)
}, [awards])
const handleCreateRound = () => {
if (!roundForm.name.trim() || !roundForm.roundType || !comp) {
toast.error('All fields are required')
return
}
const slug = roundForm.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
const nextOrder = rounds.length
createRoundMutation.mutate({
competitionId: comp.id,
name: roundForm.name.trim(),
slug,
roundType: roundForm.roundType as any,
sortOrder: nextOrder,
})
}
const startEditSettings = () => {
if (!comp) return
setEditingCompId(comp.id)
setCompetitionEdits({
name: comp.name,
categoryMode: (comp as any).categoryMode,
startupFinalistCount: (comp as any).startupFinalistCount,
conceptFinalistCount: (comp as any).conceptFinalistCount,
notifyOnDeadlineApproach: (comp as any).notifyOnDeadlineApproach,
})
setSettingsOpen(true)
}
const saveSettings = () => {
if (!editingCompId) return
updateCompMutation.mutate({ id: editingCompId, ...competitionEdits } as any)
}
// ─── No edition ──────────────────────────────────────────────────────────
if (!programId) {
return (
<div className="space-y-6">
<h1 className="text-xl font-bold">Rounds</h1>
<div className="flex flex-col items-center justify-center py-16 text-center border-2 border-dashed rounded-lg">
<Calendar className="h-10 w-10 text-muted-foreground/40 mb-3" />
<p className="font-medium text-sm">No Edition Selected</p>
<p className="text-xs text-muted-foreground">Select an edition from the sidebar</p>
</div>
</div>
)
}
// ─── Loading ─────────────────────────────────────────────────────────────
if (isLoading || isLoadingDetail) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-9 w-28" />
</div>
<Skeleton className="h-10 w-full" />
<div className="space-y-1">
{[1, 2, 3, 4, 5].map((i) => (
<Skeleton key={i} className="h-14 w-full" />
))}
</div>
</div>
)
}
// ─── No competition ──────────────────────────────────────────────────────
if (!comp) {
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold tracking-tight text-[#053d57]">Competition Pipeline</h1>
<div className="flex flex-col items-center justify-center py-20 text-center border-2 border-dashed rounded-lg">
<div className="rounded-full bg-[#053d57]/5 p-5 mb-4">
<Trophy className="h-10 w-10 text-[#053d57]/60" />
</div>
<h3 className="text-lg font-semibold text-[#053d57] mb-1">No Competition Configured</h3>
<p className="text-sm text-muted-foreground max-w-sm mb-5">
Create a competition to start building the evaluation pipeline.
</p>
<Link href={`/admin/competitions/new?programId=${programId}` as Route}>
<Button className="bg-[#de0f1e] hover:bg-[#de0f1e]/90">
<Plus className="h-4 w-4 mr-2" />
Create Competition
</Button>
</Link>
</div>
</div>
)
}
// ─── Main Render ─────────────────────────────────────────────────────────
const activeFilter = filterType !== 'all'
const totalProjects = rounds.reduce((s, r) => s + r._count.projectRoundStates, 0)
const totalAssignments = rounds.reduce((s, r) => s + r._count.assignments, 0)
const activeRound = rounds.find((r) => r.status === 'ROUND_ACTIVE')
return (
<TooltipProvider delayDuration={200}>
<div className="space-y-5">
{/* ── Header Bar ──────────────────────────────────────────────── */}
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold tracking-tight text-[#053d57] truncate">
{comp.name}
</h1>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={startEditSettings}
className="shrink-0 p-1.5 rounded-md hover:bg-muted transition-colors text-muted-foreground hover:text-[#053d57]"
>
<Settings className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent>Competition settings</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center gap-4 mt-1 text-sm text-muted-foreground">
<span>{rounds.length} rounds</span>
<span className="text-muted-foreground/30">|</span>
<span>{totalProjects} projects</span>
<span className="text-muted-foreground/30">|</span>
<span>{totalAssignments} assignments</span>
{activeRound && (
<>
<span className="text-muted-foreground/30">|</span>
<span className="flex items-center gap-1.5 text-emerald-600 font-medium">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
{activeRound.name}
</span>
</>
)}
{awards && awards.length > 0 && (
<>
<span className="text-muted-foreground/30">|</span>
<span className="flex items-center gap-1">
<Award className="h-3.5 w-3.5" />
{awards.length} awards
</span>
</>
)}
</div>
</div>
<Button
size="sm"
onClick={() => setAddRoundOpen(true)}
className="bg-[#de0f1e] hover:bg-[#de0f1e]/90 shrink-0"
>
<Plus className="h-3.5 w-3.5 mr-1.5" />
Add Round
</Button>
</div>
{/* ── Filter Pills ────────────────────────────────────────────── */}
<div className="flex items-center gap-1.5 flex-wrap">
<button
onClick={() => setFilterType('all')}
className={cn(
'px-3 py-1 text-xs font-medium rounded-full border transition-colors',
filterType === 'all'
? 'bg-[#053d57] text-white border-[#053d57]'
: 'bg-white text-muted-foreground border-border hover:border-[#053d57]/30'
)}
>
All
</button>
{ROUND_TYPES.map((rt) => {
const colors = ROUND_TYPE_COLORS[rt.value]
const isActive = filterType === rt.value
return (
<button
key={rt.value}
onClick={() => setFilterType(isActive ? 'all' : rt.value)}
className={cn(
'px-3 py-1 text-xs font-medium rounded-full border transition-colors flex items-center gap-1.5',
isActive
? `${colors.bg} ${colors.text} ${colors.border}`
: 'bg-white text-muted-foreground border-border hover:border-[#053d57]/30'
)}
>
<span
className="h-2 w-2 rounded-full shrink-0"
style={{ backgroundColor: colors.dot }}
/>
{rt.label}
</button>
)
})}
</div>
{/* ── Pipeline View ───────────────────────────────────────────── */}
{rounds.length === 0 ? (
<div className="py-16 text-center border-2 border-dashed rounded-lg">
<FileBox className="h-8 w-8 text-muted-foreground/40 mx-auto mb-2" />
<p className="text-sm text-muted-foreground">
{activeFilter ? 'No rounds match this filter.' : 'No rounds yet. Add one to start building the pipeline.'}
</p>
</div>
) : (
<div className="relative">
{/* Main pipeline track */}
{rounds.map((round, index) => {
const isLast = index === rounds.length - 1
const typeColors = ROUND_TYPE_COLORS[round.roundType] ?? ROUND_TYPE_COLORS.INTAKE
const statusStyle = ROUND_STATUS_STYLES[round.status] ?? ROUND_STATUS_STYLES.ROUND_DRAFT
const projectCount = round._count.projectRoundStates
const assignmentCount = round._count.assignments
const roundAwards = awardsByRound.get(round.id) ?? []
return (
<div key={round.id} className="relative">
{/* Round row with pipeline connector */}
<div className="flex">
{/* Left: pipeline track */}
<div className="flex flex-col items-center shrink-0 w-10">
{/* Status dot */}
<Tooltip>
<TooltipTrigger asChild>
<div className="relative z-10 flex items-center justify-center">
<div
className="h-3.5 w-3.5 rounded-full border-2 border-white shadow-sm"
style={{ backgroundColor: statusStyle.color }}
/>
{statusStyle.pulse && (
<div
className="absolute h-3.5 w-3.5 rounded-full animate-ping opacity-40"
style={{ backgroundColor: statusStyle.color }}
/>
)}
</div>
</TooltipTrigger>
<TooltipContent side="left" className="text-xs">
{statusStyle.label}
</TooltipContent>
</Tooltip>
{/* Connector line */}
{!isLast && (
<div className="w-px flex-1 min-h-[8px] bg-border" />
)}
</div>
{/* Right: round content + awards */}
<div className="flex-1 min-w-0 pb-2">
<div className="flex items-stretch gap-3">
{/* Round row */}
<Link
href={`/admin/rounds/${round.id}` as Route}
className="flex-1 min-w-0"
>
<div
className={cn(
'group flex items-center gap-3 px-3 py-2.5 rounded-md border-l-[3px] cursor-pointer transition-all',
'bg-white hover:bg-gray-50/80 hover:shadow-sm',
)}
style={{ borderLeftColor: typeColors.dot }}
>
{/* Round type indicator */}
<span className={cn(
'text-[10px] font-semibold uppercase tracking-wider shrink-0 w-[70px]',
typeColors.text
)}>
{round.roundType.replace('_', ' ')}
</span>
{/* Round name */}
<span className="text-sm font-semibold text-[#053d57] truncate group-hover:text-[#de0f1e] transition-colors min-w-0 flex-1">
{round.name}
</span>
{/* Stats cluster */}
<div className="hidden sm:flex items-center gap-3 text-xs text-muted-foreground shrink-0">
{round.juryGroup && (
<span className="flex items-center gap-1 max-w-[120px]">
<Users className="h-3 w-3 shrink-0" />
<span className="truncate">{round.juryGroup.name}</span>
</span>
)}
<span className="flex items-center gap-1">
<FileBox className="h-3 w-3 shrink-0" />
{projectCount}
</span>
{assignmentCount > 0 && (
<span className="tabular-nums">{assignmentCount} eval</span>
)}
{(round.windowOpenAt || round.windowCloseAt) && (
<span className="flex items-center gap-1 tabular-nums">
<Calendar className="h-3 w-3 shrink-0" />
{round.windowOpenAt
? new Date(round.windowOpenAt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' })
: ''}
{round.windowOpenAt && round.windowCloseAt ? ' \u2013 ' : ''}
{round.windowCloseAt
? new Date(round.windowCloseAt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' })
: ''}
</span>
)}
</div>
{/* Status badge (compact) */}
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0 h-5 font-medium shrink-0 hidden md:inline-flex"
style={{ color: statusStyle.color, borderColor: statusStyle.color + '40' }}
>
{statusStyle.label}
</Badge>
{/* Arrow */}
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground/30 group-hover:text-[#de0f1e]/60 transition-colors shrink-0" />
</div>
</Link>
{/* Awards branching off this round */}
{roundAwards.length > 0 && (
<div className="flex items-center gap-2 shrink-0">
{/* Connector dash */}
<div className="w-4 h-px bg-amber-300" />
{/* Award nodes */}
<div className="flex flex-col gap-1">
{roundAwards.map((award) => (
<AwardNode key={award.id} award={award} />
))}
</div>
</div>
)}
</div>
</div>
</div>
</div>
)
})}
{/* Floating awards (no evaluationRoundId) */}
{floatingAwards.length > 0 && (
<div className="mt-4 pt-4 border-t border-dashed">
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-2 pl-10">
Unlinked Awards
</p>
<div className="flex flex-wrap gap-2 pl-10">
{floatingAwards.map((award) => (
<AwardNode key={award.id} award={award} />
))}
</div>
</div>
)}
</div>
)}
{/* ── Settings Panel (Collapsible) ─────────────────────────── */}
<Dialog open={settingsOpen} onOpenChange={setSettingsOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle className="text-[#053d57]">Competition Settings</DialogTitle>
<DialogDescription>
Configure competition parameters for {comp.name}.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5 sm:col-span-2">
<Label className="text-xs font-medium">Competition Name</Label>
<Input
value={(competitionEdits.name as string) ?? ''}
onChange={(e) => setCompetitionEdits({ ...competitionEdits, name: e.target.value })}
className="h-9"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-medium">Category Mode</Label>
<Input
value={(competitionEdits.categoryMode as string) ?? ''}
onChange={(e) => setCompetitionEdits({ ...competitionEdits, categoryMode: e.target.value })}
className="h-9"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-medium">Startup Finalists</Label>
<Input
type="number"
min={1}
className="h-9"
value={(competitionEdits.startupFinalistCount as number) ?? 10}
onChange={(e) => setCompetitionEdits({ ...competitionEdits, startupFinalistCount: parseInt(e.target.value, 10) || 10 })}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-medium">Concept Finalists</Label>
<Input
type="number"
min={1}
className="h-9"
value={(competitionEdits.conceptFinalistCount as number) ?? 10}
onChange={(e) => setCompetitionEdits({ ...competitionEdits, conceptFinalistCount: parseInt(e.target.value, 10) || 10 })}
/>
</div>
<div className="flex items-center gap-3 sm:col-span-2 pt-1">
<Switch
checked={(competitionEdits.notifyOnDeadlineApproach as boolean) ?? false}
onCheckedChange={(v) => setCompetitionEdits({ ...competitionEdits, notifyOnDeadlineApproach: v })}
/>
<Label className="text-xs font-medium">Deadline Reminders</Label>
</div>
</div>
<DialogFooter className="gap-2">
<Button variant="outline" size="sm" onClick={() => setSettingsOpen(false)}>
Cancel
</Button>
<Button
size="sm"
onClick={saveSettings}
disabled={updateCompMutation.isPending}
className="bg-[#de0f1e] hover:bg-[#de0f1e]/90"
>
{updateCompMutation.isPending ? (
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
) : (
<Save className="h-3.5 w-3.5 mr-1.5" />
)}
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* ── Add Round Dialog ─────────────────────────────────────── */}
<Dialog open={addRoundOpen} onOpenChange={setAddRoundOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Add Round</DialogTitle>
<DialogDescription>
Add a new round to the pipeline.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-1.5">
<Label className="text-xs font-medium">Round Name *</Label>
<Input
placeholder="e.g. Initial Screening"
value={roundForm.name}
onChange={(e) => setRoundForm({ ...roundForm, name: e.target.value })}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-medium">Round Type *</Label>
<Select
value={roundForm.roundType}
onValueChange={(v) => setRoundForm({ ...roundForm, roundType: v })}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
{ROUND_TYPES.map((rt) => {
const colors = ROUND_TYPE_COLORS[rt.value]
return (
<SelectItem key={rt.value} value={rt.value}>
<span className="flex items-center gap-2">
<span
className="h-2 w-2 rounded-full shrink-0"
style={{ backgroundColor: colors.dot }}
/>
{rt.label}
</span>
</SelectItem>
)
})}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter className="gap-2">
<Button variant="outline" size="sm" onClick={() => setAddRoundOpen(false)}>Cancel</Button>
<Button
size="sm"
onClick={handleCreateRound}
disabled={createRoundMutation.isPending}
className="bg-[#de0f1e] hover:bg-[#de0f1e]/90"
>
{createRoundMutation.isPending ? 'Creating...' : 'Create Round'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</TooltipProvider>
)
}
// ─── Award Node ──────────────────────────────────────────────────────────────
function AwardNode({ award }: { award: SpecialAwardItem }) {
const statusColor = AWARD_STATUS_COLORS[award.status] ?? 'text-gray-500'
const isExclusive = award.eligibilityMode === 'SEPARATE_POOL'
const eligible = award._count.eligibilities
const hasWinner = !!award.winnerProject
return (
<TooltipProvider delayDuration={150}>
<Tooltip>
<TooltipTrigger asChild>
<Link href={`/admin/awards/${award.id}` as Route}>
<div className={cn(
'flex items-center gap-1.5 px-2.5 py-1.5 rounded-md border text-xs cursor-pointer transition-all',
'bg-amber-50/60 border-amber-200/80 hover:border-amber-400 hover:shadow-sm',
hasWinner && 'bg-emerald-50/60 border-emerald-200/80 hover:border-emerald-400',
)}>
<Trophy className={cn('h-3 w-3 shrink-0', hasWinner ? 'text-emerald-600' : 'text-amber-500')} />
<span className="font-medium text-[#053d57] truncate max-w-[140px]">
{award.name}
</span>
{eligible > 0 && (
<span className="text-muted-foreground tabular-nums">{eligible}</span>
)}
<span className={cn(
'text-[9px] font-semibold uppercase tracking-wide px-1 py-px rounded',
isExclusive
? 'bg-red-100 text-red-600'
: 'bg-blue-100 text-blue-600'
)}>
{isExclusive ? 'Excl' : 'Par'}
</span>
</div>
</Link>
</TooltipTrigger>
<TooltipContent className="max-w-xs text-xs">
<p className="font-semibold">{award.name}</p>
<p className="text-muted-foreground mt-0.5">
{isExclusive ? 'Exclusive pool (projects leave main track)' : 'Parallel (projects stay in main track)'}
</p>
<p className="mt-1">
{eligible} eligible &middot; {award._count.jurors} jurors &middot; {award._count.votes} votes
</p>
{hasWinner && (
<p className="text-emerald-600 font-medium mt-1">
Winner: {award.winnerProject!.title}
</p>
)}
<p className={cn('mt-1', statusColor)}>
Status: {award.status.replace('_', ' ')}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}