Files
MOPC-Portal/src/app/(admin)/admin/rounds/page.tsx
Matt f26ee3f076
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m43s
Admin dashboard & round management UX overhaul
- 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>
2026-02-22 17:14:00 +01:00

745 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'
import {
roundTypeConfig,
roundStatusConfig,
awardStatusConfig,
ROUND_TYPE_OPTIONS,
} from '@/lib/round-config'
// ─── Constants (derived from shared config) ──────────────────────────────────
const ROUND_TYPES = ROUND_TYPE_OPTIONS
const ROUND_TYPE_COLORS: Record<string, { dot: string; bg: string; text: string; border: string }> = Object.fromEntries(
Object.entries(roundTypeConfig).map(([k, v]) => [k, { dot: v.dotColor, bg: v.cardBg, text: v.cardText, border: v.cardBorder }])
)
const ROUND_STATUS_STYLES: Record<string, { color: string; label: string; pulse?: boolean }> = Object.fromEntries(
Object.entries(roundStatusConfig).map(([k, v]) => [k, { color: v.dotColor, label: v.label, pulse: v.pulse }])
)
const AWARD_STATUS_COLORS: Record<string, string> = Object.fromEntries(
Object.entries(awardStatusConfig).map(([k, v]) => [k, v.color])
)
// ─── Types ───────────────────────────────────────────────────────────────────
type RoundWithStats = {
id: string
name: string
slug: string
roundType: string
status: string
sortOrder: number
windowOpenAt: string | null
windowCloseAt: string | null
specialAwardId: 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 [selectedCompId, setSelectedCompId] = useState<string | null>(null)
const { data: competitions, isLoading } = trpc.competition.list.useQuery(
{ programId: programId! },
{ enabled: !!programId, refetchInterval: 30_000 }
)
// Auto-select first competition, or use the user's selection
const comp = competitions?.find((c: any) => c.id === selectedCompId) ?? competitions?.[0]
const { data: compDetail, isLoading: isLoadingDetail } = trpc.competition.getById.useQuery(
{ id: comp?.id! },
{ enabled: !!comp?.id, refetchInterval: 30_000 }
)
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 = (compDetail?.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 program edition to start building the evaluation pipeline.
</p>
<Link href={'/admin/programs' as Route}>
<Button className="bg-[#de0f1e] hover:bg-[#de0f1e]/90">
<Plus className="h-4 w-4 mr-2" />
Manage Editions
</Button>
</Link>
</div>
</div>
)
}
// ─── Main Render ─────────────────────────────────────────────────────────
const activeFilter = filterType !== 'all'
const totalProjects = (compDetail as any)?.distinctProjectCount ?? 0
const allRounds = (compDetail?.rounds ?? []) as RoundWithStats[]
const totalAssignments = allRounds.reduce((s, r) => s + r._count.assignments, 0)
const activeRound = allRounds.find((r) => r.status === 'ROUND_ACTIVE')
return (
<TooltipProvider delayDuration={200}>
<div className="space-y-5">
{/* Competition selector (when multiple exist) */}
{competitions && competitions.length > 1 && (
<Select value={comp.id} onValueChange={setSelectedCompId}>
<SelectTrigger className="w-[280px] mb-4">
<SelectValue placeholder="Select competition" />
</SelectTrigger>
<SelectContent>
{competitions.map((c: any) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{/* ── 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>{allRounds.filter((r) => !r.specialAwardId).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} asgn</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>
)
}