Add @bytelyst/charts + @bytelyst/data-viz as workspace:* deps (minimal importer link entries in the lockfile). Replace the badge-pill breakdowns on /dashboard with KpiCards (total/open/in-progress/done) and a dynamically imported chart surface: Donut for By Status + By Type (centered total) and a BarChart for By Priority, coloured from the bridged --bl-chart-* palette. Pure transforms live in src/lib/overview-charts.ts (finite-coerced, no NaN reaches the SVG); the heavy chart surface is split into overview-charts.tsx and loaded via next/dynamic (ssr:false) to keep it out of the route bundle. Add overview-charts.test.tsx rendering the surface with mocked stats via react-dom/server (no NaN in paths) + transform unit tests; dedupe react in vitest so the SSR render uses a single React instance. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
79 lines
2.5 KiB
TypeScript
79 lines
2.5 KiB
TypeScript
'use client';
|
|
|
|
import { Donut, BarChart } from '@bytelyst/charts';
|
|
import { Card, CardHeader, CardTitle } from '@/components/ui/Primitives';
|
|
import { statusSlices, typeSlices, priorityBars } from '@/lib/overview-charts';
|
|
import type { TrackerStats } from '@/lib/tracker-client';
|
|
|
|
/**
|
|
* Heavy chart surface for the dashboard overview (UX-4.2). Imported via
|
|
* next/dynamic from the overview page so the SVG chart code stays out of the
|
|
* initial route bundle.
|
|
*/
|
|
export default function OverviewCharts({ stats }: { stats: TrackerStats }) {
|
|
const statuses = statusSlices(stats);
|
|
const types = typeSlices(stats);
|
|
const priorities = priorityBars(stats);
|
|
|
|
return (
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<Card className="flex flex-col items-center p-5">
|
|
<CardHeader className="w-full">
|
|
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
By Status
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<Donut
|
|
slices={statuses}
|
|
size={180}
|
|
ariaLabel="Items by status"
|
|
centerContent={<div className="text-2xl font-bold text-foreground">{stats.total}</div>}
|
|
/>
|
|
<ChartLegend slices={statuses} />
|
|
</Card>
|
|
|
|
<Card className="flex flex-col items-center p-5">
|
|
<CardHeader className="w-full">
|
|
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
By Type
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<Donut slices={types} size={180} ariaLabel="Items by type" />
|
|
<ChartLegend slices={types} />
|
|
</Card>
|
|
|
|
<Card className="p-5">
|
|
<CardHeader>
|
|
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
By Priority
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<BarChart
|
|
data={priorities}
|
|
width={360}
|
|
height={200}
|
|
ariaLabel="Items by priority"
|
|
className="w-full"
|
|
/>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ChartLegend({ slices }: { slices: { id: string; label?: string; color?: string }[] }) {
|
|
return (
|
|
<ul className="mt-4 flex w-full flex-wrap justify-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
|
{slices.map(s => (
|
|
<li key={s.id} className="flex items-center gap-1.5">
|
|
<span
|
|
aria-hidden="true"
|
|
className="h-2 w-2 rounded-full"
|
|
style={{ background: s.color }}
|
|
/>
|
|
{s.label ?? s.id}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|