Files
MOPC-Portal/src/components/observer/dashboard/previous-round-section.tsx

149 lines
6.2 KiB
TypeScript
Raw Normal View History

'use client'
import { useState } from 'react'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { AnimatedCard } from '@/components/shared/animated-container'
import { ArrowDown, ChevronDown, ChevronUp, TrendingDown } from 'lucide-react'
import { cn } from '@/lib/utils'
export function PreviousRoundSection({ currentRoundId }: { currentRoundId: string }) {
const [collapsed, setCollapsed] = useState(false)
const { data, isLoading } = trpc.analytics.getPreviousRoundComparison.useQuery(
{ currentRoundId },
{ refetchInterval: 60_000 },
)
if (isLoading) {
return <Skeleton className="h-40 w-full rounded-lg" />
}
if (!data || !data.hasPrevious) {
return null
}
const { previousRound, currentRound, eliminated, categoryBreakdown, countryAttrition } = data
return (
<AnimatedCard index={5}>
<Card>
<CardHeader className="pb-2">
<button
type="button"
className="flex items-center justify-between w-full text-left"
onClick={() => setCollapsed(!collapsed)}
>
<CardTitle className="flex items-center gap-2 text-base">
<div className="rounded-lg bg-rose-500/10 p-1.5">
<TrendingDown className="h-4 w-4 text-rose-500" />
</div>
Compared to Previous Round: {previousRound.name}
</CardTitle>
{collapsed
? <ChevronDown className="h-4 w-4 text-muted-foreground" />
: <ChevronUp className="h-4 w-4 text-muted-foreground" />}
</button>
</CardHeader>
{!collapsed && (
<CardContent className="space-y-4">
{/* Headline Stat */}
<div className="flex items-center gap-3 rounded-lg bg-rose-50 dark:bg-rose-950/20 p-4">
<ArrowDown className="h-6 w-6 text-rose-500 shrink-0" />
<div>
<p className="text-lg font-semibold">
{eliminated} project{eliminated !== 1 ? 's' : ''} eliminated
</p>
<p className="text-sm text-muted-foreground">
{previousRound.projectCount} {currentRound.projectCount}
</p>
</div>
</div>
{/* Category Survival Bars */}
{categoryBreakdown && categoryBreakdown.length > 0 && (
<div className="space-y-3">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
By Category
</p>
{categoryBreakdown.map((cat: any) => {
const maxVal = Math.max(cat.previous, 1)
const prevPct = 100
const currPct = (cat.current / maxVal) * 100
return (
<div key={cat.category} className="space-y-1">
<div className="flex items-center justify-between text-sm">
<span className="font-medium truncate">{cat.category}</span>
<span className="text-xs text-muted-foreground tabular-nums">
{cat.previous} {cat.current}
<span className="text-rose-500 ml-1">(-{cat.eliminated})</span>
</span>
</div>
<div className="relative h-2.5 rounded-full bg-muted overflow-hidden">
<div
className="absolute inset-y-0 left-0 rounded-full bg-slate-300 dark:bg-slate-600 transition-all"
style={{ width: `${prevPct}%` }}
/>
<div
className="absolute inset-y-0 left-0 rounded-full bg-brand-teal transition-all"
style={{ width: `${currPct}%` }}
/>
</div>
</div>
)
})}
</div>
)}
{/* Country Attrition */}
{countryAttrition && countryAttrition.length > 0 && (
<div>
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
Country Attrition (Top 10)
</p>
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
{countryAttrition.map((c: any) => (
<div key={c.country} className="flex items-center justify-between text-sm py-0.5">
<span className="truncate">{c.country}</span>
<Badge variant="destructive" className="tabular-nums text-xs">
-{c.lost}
</Badge>
</div>
))}
</div>
</div>
)}
{/* Score Comparison */}
{previousRound.avgScore != null && currentRound.avgScore != null && (
<div className="grid grid-cols-2 gap-3">
<Card className="p-3 text-center border-muted">
<p className="text-xs text-muted-foreground mb-1">{previousRound.name}</p>
<p className="text-lg font-semibold tabular-nums">
{typeof previousRound.avgScore === 'number'
? previousRound.avgScore.toFixed(1)
: previousRound.avgScore}
</p>
<p className="text-[10px] text-muted-foreground">Avg Score</p>
</Card>
<Card className="p-3 text-center border-brand-teal/30">
<p className="text-xs text-muted-foreground mb-1">{currentRound.name}</p>
<p className="text-lg font-semibold tabular-nums">
{typeof currentRound.avgScore === 'number'
? currentRound.avgScore.toFixed(1)
: currentRound.avgScore}
</p>
<p className="text-[10px] text-muted-foreground">Avg Score</p>
</Card>
</div>
)}
</CardContent>
)}
</Card>
</AnimatedCard>
)
}