frontend: stacked-area NW history chart on the dashboard
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

First end-to-end view: ECharts stacked area of net worth by account
over the last 365 days, fed from GET /networth/history.

- NetWorthChart component with empty-state fallback
- Mocked ReactECharts in tests so they run without a DOM canvas
- Dashboard now: headline NW + history chart + per-account cards

Bundle grew to 467 KB gzipped — ECharts is heavy by design. Will
tree-shake via echarts/core imports once the chart surface stabilises.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-05-09 21:55:30 +00:00
parent f4539f9e6d
commit 5d2b9e931a
3 changed files with 141 additions and 2 deletions

View file

@ -1,14 +1,23 @@
/**
* Dashboard landing page. Wire up gets fleshed out in Phase 1a.
* For now: confirm we can reach /networth and render today's NW.
* Dashboard net worth at a glance.
*
* Three sections, top to bottom:
* 1. Headline NW total (latest snapshot)
* 2. Stacked-area history chart (per-account, /networth/history)
* 3. Per-account cards (latest values, /networth)
*/
import { useQuery } from '@tanstack/react-query';
import { api } from '@/api/client';
import { NetWorthChart } from '@/components/NetWorthChart';
import { gbp } from '@/lib/format';
export function Dashboard() {
const nw = useQuery({ queryKey: ['networth', 'current'], queryFn: api.networth.current });
const history = useQuery({
queryKey: ['networth', 'history', 365],
queryFn: () => api.networth.history(365),
});
if (nw.isLoading) {
return <p className="text-slate-500">Loading net worth</p>;
@ -45,10 +54,23 @@ export function Dashboard() {
<h1 className="text-3xl font-semibold tracking-tight">Net worth</h1>
<p className="text-sm text-slate-500">As of {data.snapshot_date}</p>
</header>
<div className="rounded-lg border border-slate-200 bg-white p-6">
<div className="text-4xl font-semibold tabular-nums">{gbp(data.total_gbp)}</div>
<p className="text-sm text-slate-500 mt-1">{data.accounts.length} accounts</p>
</div>
<div className="rounded-lg border border-slate-200 bg-white p-6">
<h2 className="text-lg font-semibold mb-4">Last 12 months</h2>
{history.isLoading ? (
<p className="text-sm text-slate-500">Loading</p>
) : history.isError || !history.data ? (
<p className="text-sm text-slate-500">History unavailable.</p>
) : (
<NetWorthChart history={history.data} />
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.accounts.map((a) => (
<div