149 lines
6.2 KiB
TypeScript
149 lines
6.2 KiB
TypeScript
|
|
'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>
|
||
|
|
)
|
||
|
|
}
|