MatchProgressChart component showing cumulative match status counts over time.

export const MatchProgressChart: FC<MatchProgressChartProps> = React.memo(
function MatchProgressChartMemo({
// eslint-disable-next-line react/prop-types
matchResults,
// eslint-disable-next-line react/prop-types
className,
}) {
const { data, totalMatches, startLabel, endLabel } = useMemo(
() => buildTimeline(matchResults),
[matchResults],
);

const subtitle = useMemo(() => {
if (!data.length || !startLabel || !endLabel) {
return "Historical view of match completion by status.";
}
if (startLabel === endLabel) {
return `Activity recorded on ${startLabel}.`;
}
return `Activity from ${startLabel} to ${endLabel}.`;
}, [data.length, startLabel, endLabel]);

return (
<section
aria-label="Match progress timeline"
className={cn(
"rounded-2xl border border-slate-200 bg-white/90 p-6 shadow-sm backdrop-blur-md dark:border-slate-800 dark:bg-slate-900/90",
className,
)}
>
<header className="mb-4 flex flex-wrap items-start justify-between gap-3">
<div>
<div className="flex items-center gap-2">
<span className="bg-linear-to-r inline-flex h-9 min-h-9 w-9 min-w-9 items-center justify-center rounded-full from-emerald-500/15 via-blue-500/15 to-purple-500/15 text-emerald-500 dark:text-emerald-300">
<TrendingUp className="h-4 w-4" aria-hidden="true" />
</span>
<h2 className="text-foreground text-lg font-semibold">
Match Progress Timeline
</h2>
</div>
<p className="text-muted-foreground mt-1 text-sm">{subtitle}</p>
</div>
{totalMatches > 0 && (
<div className="text-muted-foreground flex items-center gap-2 text-sm">
<Calendar className="h-4 w-4" aria-hidden="true" />
<span>{totalMatches.toLocaleString()} total matches</span>
</div>
)}
</header>

{data.length === 0 ? (
<div className="text-muted-foreground flex flex-col items-center justify-center rounded-xl border border-dashed border-slate-200 bg-slate-50/60 p-8 text-center dark:border-slate-800 dark:bg-slate-900/60">
<AlertCircle className="mb-3 h-6 w-6" aria-hidden="true" />
<p className="font-medium">No match history available</p>
<p className="mt-1 text-sm">
Once you review matches, your progress timeline will appear here.
</p>
</div>
) : (
<figure className="h-[350px] w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
margin={{ top: 20, right: 30, left: 10, bottom: 10 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis
dataKey="isoDate"
tickFormatter={(value) => {
const date = new Date(value);
return Number.isNaN(date.getTime())
? value
: DATE_FORMATTER.format(date);
}}
tickLine={false}
axisLine={false}
tick={{ fontSize: 12 }}
/>
<YAxis
tickLine={false}
axisLine={false}
tick={{ fontSize: 12 }}
allowDecimals={false}
/>
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--muted))",
borderRadius: "calc(var(--radius) - 4px)",
padding: "0.5rem 0.75rem",
border: "none",
}}
labelStyle={{ color: "hsl(var(--muted-foreground))" }}
labelFormatter={(value) => {
const date = new Date(value);
return Number.isNaN(date.getTime())
? value
: FULL_DATE_FORMATTER.format(date);
}}
/>
<Legend iconType="circle" verticalAlign="top" height={32} />
<Area
type="monotone"
dataKey="matched"
stackId="1"
stroke="#059669"
fill="#10b981"
fillOpacity={0.4}
name="Matched"
/>
<Area
type="monotone"
dataKey="manual"
stackId="1"
stroke="#2563eb"
fill="#3b82f6"
fillOpacity={0.35}
name="Manual"
/>
<Area
type="monotone"
dataKey="pending"
stackId="1"
stroke="#d97706"
fill="#f59e0b"
fillOpacity={0.3}
name="Pending"
/>
<Area
type="monotone"
dataKey="skipped"
stackId="1"
stroke="#dc2626"
fill="#ef4444"
fillOpacity={0.25}
name="Skipped"
/>
</AreaChart>
</ResponsiveContainer>
</figure>
)}
</section>
);
},
);