Files
MOPC-Portal/src/components/observer/dashboard/live-final-panel.tsx

165 lines
6.5 KiB
TypeScript
Raw Normal View History

'use client'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { Badge } from '@/components/ui/badge'
import { AnimatedCard } from '@/components/shared/animated-container'
import { Radio, Users, Trophy, Eye, EyeOff } from 'lucide-react'
import { cn } from '@/lib/utils'
const SESSION_STATUS_CONFIG: Record<string, { label: string; color: string; bg: string; pulse?: boolean }> = {
NOT_STARTED: { label: 'Not Started', color: 'text-slate-500', bg: 'bg-slate-100 dark:bg-slate-800' },
IN_PROGRESS: { label: 'In Progress', color: 'text-emerald-600', bg: 'bg-emerald-50 dark:bg-emerald-900/20', pulse: true },
PAUSED: { label: 'Paused', color: 'text-amber-600', bg: 'bg-amber-50 dark:bg-amber-900/20' },
COMPLETED: { label: 'Completed', color: 'text-blue-600', bg: 'bg-blue-50 dark:bg-blue-900/20' },
}
export function LiveFinalPanel({ roundId }: { roundId: string }) {
const { data: liveDash, isLoading } = trpc.analytics.getLiveFinalDashboard.useQuery(
{ roundId },
{ refetchInterval: 10_000 },
)
const { data: roundStats } = trpc.analytics.getRoundTypeStats.useQuery(
{ roundId },
{ refetchInterval: 30_000 },
)
const stats = roundStats?.stats as {
sessionStatus: string
voteCount: number
} | undefined
const sessionStatus = liveDash?.sessionStatus ?? stats?.sessionStatus ?? 'NOT_STARTED'
const statusConfig = SESSION_STATUS_CONFIG[sessionStatus] ?? SESSION_STATUS_CONFIG.NOT_STARTED
const jurors = liveDash?.jurors ?? []
const votedCount = jurors.filter((j: any) => j.hasVoted).length
const standings = liveDash?.standings ?? []
const visibility = liveDash?.observerScoreVisibility ?? 'after_completion'
const scoresVisible = visibility === 'realtime'
|| (visibility === 'after_completion' && sessionStatus === 'COMPLETED')
return (
<div className="space-y-4">
{/* Session Status Card */}
{isLoading ? (
<Skeleton className="h-24 rounded-lg" />
) : (
<Card className={cn('p-5', statusConfig.bg)}>
<div className="flex items-center gap-4">
<div className="relative">
<Radio className={cn('h-8 w-8', statusConfig.color)} />
{statusConfig.pulse && (
<span className="absolute -top-0.5 -right-0.5 h-3 w-3 rounded-full bg-emerald-500 animate-pulse" />
)}
</div>
<div>
<p className={cn('text-lg font-semibold', statusConfig.color)}>
{statusConfig.label}
</p>
<p className="text-sm text-muted-foreground">
{liveDash?.voteCount ?? stats?.voteCount ?? 0} votes cast
</p>
</div>
</div>
</Card>
)}
{/* Vote Count */}
<div className="grid grid-cols-2 gap-3">
<Card className="p-3 text-center">
<p className="text-2xl font-semibold tabular-nums">
{liveDash?.voteCount ?? stats?.voteCount ?? 0}
</p>
<p className="text-xs text-muted-foreground mt-0.5">Total Votes</p>
</Card>
<Card className="p-3 text-center">
<p className="text-2xl font-semibold tabular-nums">
{votedCount}/{jurors.length}
</p>
<p className="text-xs text-muted-foreground mt-0.5">Jurors Voted</p>
</Card>
</div>
{/* Juror Participation */}
{jurors.length > 0 && (
<AnimatedCard index={1}>
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm">
<Users className="h-4 w-4 text-violet-500" />
Juror Participation
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-1.5 max-h-[250px] overflow-y-auto">
{jurors.map((j: any) => (
<div key={j.id} className="flex items-center justify-between text-sm py-1">
<span className="truncate">{j.name}</span>
<Badge
variant={j.hasVoted ? 'default' : 'outline'}
className={cn(
'text-xs',
j.hasVoted && 'bg-emerald-500 hover:bg-emerald-600',
)}
>
{j.hasVoted ? 'Voted' : 'Pending'}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
</AnimatedCard>
)}
{/* Standings / Score Visibility */}
<AnimatedCard index={2}>
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm">
<Trophy className="h-4 w-4 text-amber-500" />
Standings
{scoresVisible ? (
<Eye className="h-3.5 w-3.5 text-emerald-500 ml-auto" />
) : (
<EyeOff className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
)}
</CardTitle>
</CardHeader>
<CardContent>
{scoresVisible && standings.length > 0 ? (
<div className="space-y-2">
{standings.map((s: any, i: number) => (
<div key={s.projectId} className="flex items-center justify-between text-sm py-1">
<div className="flex items-center gap-2 min-w-0">
<span className="text-muted-foreground tabular-nums font-medium w-5 text-right">
{i + 1}.
</span>
<span className="truncate">{s.projectTitle}</span>
</div>
<Badge variant="secondary" className="tabular-nums shrink-0">
{typeof s.score === 'number' ? s.score.toFixed(1) : s.score}
</Badge>
</div>
))}
</div>
) : (
<div className="text-center py-6">
<EyeOff className="h-8 w-8 text-muted-foreground/40 mx-auto mb-2" />
<p className="text-sm text-muted-foreground">
{sessionStatus === 'COMPLETED'
? 'Scores are hidden by admin configuration.'
: 'Scores will be revealed when voting completes.'}
</p>
</div>
)}
</CardContent>
</Card>
</AnimatedCard>
</div>
)
}