frontend: scaffold Vite + React 19 + TS + Tailwind v4 + TanStack Query
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
Bare-minimum SPA that wires up to the FastAPI backend: - Vite 6 + React 19 + TS strict, alias @/* to src/* - Tailwind v4 via @tailwindcss/vite (no postcss) - TanStack Query v5 with sane defaults (30s staleTime, no auto-refetch) - React Router 7 for routing - ECharts + Recharts available (charts land in Phase 1a) - Vitest + @testing-library/react for tests - Dev proxy /api → http://localhost:8080 (FastAPI) Pages: - Dashboard — pulls /networth, shows total + per-account cards. No chart yet (Phase 1a). Empty/error states for "no data" cases point users to the ingest CLI. Header shows live API health (queue depth from /healthz). 274 KB JS gzipped to 87 KB. typecheck + build pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ee6ed1d3c4
commit
f4539f9e6d
16 changed files with 6145 additions and 0 deletions
75
frontend/src/pages/Dashboard.tsx
Normal file
75
frontend/src/pages/Dashboard.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Dashboard — landing page. Wire up gets fleshed out in Phase 1a.
|
||||
* For now: confirm we can reach /networth and render today's NW.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { api } from '@/api/client';
|
||||
import { gbp } from '@/lib/format';
|
||||
|
||||
export function Dashboard() {
|
||||
const nw = useQuery({ queryKey: ['networth', 'current'], queryFn: api.networth.current });
|
||||
|
||||
if (nw.isLoading) {
|
||||
return <p className="text-slate-500">Loading net worth…</p>;
|
||||
}
|
||||
|
||||
if (nw.isError) {
|
||||
return (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 p-4 text-amber-800">
|
||||
<p className="font-medium">Couldn't reach the API.</p>
|
||||
<p className="text-sm mt-1">
|
||||
Make sure FastAPI is running on :8080 and Wealthfolio ingest has populated
|
||||
<code className="px-1">account_snapshot</code>.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const data = nw.data!;
|
||||
if (data.accounts.length === 0) {
|
||||
return (
|
||||
<div className="rounded-md border border-slate-200 bg-white p-6">
|
||||
<p className="font-medium">No snapshots yet.</p>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Run <code className="px-1">python -m fire_planner ingest</code> to pull from the
|
||||
wealthfolio_sync mirror.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-6">
|
||||
<header>
|
||||
<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{data.accounts.map((a) => (
|
||||
<div
|
||||
key={a.account_id}
|
||||
className="rounded-lg border border-slate-200 bg-white p-4 flex flex-col gap-1"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium truncate">{a.account_name}</span>
|
||||
<span className="text-xs uppercase tracking-wide text-slate-500">
|
||||
{a.account_type}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-semibold tabular-nums">{gbp(a.market_value_gbp)}</div>
|
||||
{a.currency !== 'GBP' && (
|
||||
<div className="text-xs text-slate-500">
|
||||
{gbp(a.market_value, true)} {a.currency}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue