From b04f744e931c518fe342aa5e43530925bbead4ab Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 18 May 2026 00:19:54 -0700 Subject: feat: add FinancialsCard component with statement tabs and period toggle Renders income/balance/cash_flow statements with annual/quarterly toggle, section headers, indent levels, total rows, margin rows, and negative-value coloring via --negative. Co-Authored-By: Claude Sonnet 4.6 --- frontend/components/prism/FinancialsCard.tsx | 136 +++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 frontend/components/prism/FinancialsCard.tsx (limited to 'frontend/components/prism') diff --git a/frontend/components/prism/FinancialsCard.tsx b/frontend/components/prism/FinancialsCard.tsx new file mode 100644 index 0000000..94a6618 --- /dev/null +++ b/frontend/components/prism/FinancialsCard.tsx @@ -0,0 +1,136 @@ +"use client"; +import type { FinancialRow, FinancialsResponse } from "@/types/api"; +import { fmtLarge } from "@/lib/format"; + +type StatementKey = "income" | "balance" | "cash_flow"; +type PeriodKey = "annual" | "quarterly"; + +type Props = { + data: FinancialsResponse; + statement: StatementKey; + period: PeriodKey; + onChangeStatement: (s: StatementKey) => void; + onChangePeriod: (p: PeriodKey) => void; +}; + +const STMT_LABELS: Record = { + income: "INCOME", + balance: "BALANCE", + cash_flow: "CASH FLOW", +}; + +function fmtFinVal(val: number | null | undefined, isMargin: boolean): string { + if (val === null || val === undefined) return "—"; + if (isMargin) return `${(val * 100).toFixed(1)}%`; + return fmtLarge(val); +} + +function FinRow({ row, lastColIdx }: { row: FinancialRow; lastColIdx: number }) { + if (row.is_section) { + return ( + + + {row.label} + + + ); + } + + const cls = [ + "psm-fin-row", + row.is_total ? "is-total" : "", + row.is_margin ? "is-margin" : "", + row.indent === 1 ? "is-indent" : "", + ] + .filter(Boolean) + .join(" "); + + return ( + + {row.label} + {row.values.map((val, i) => ( + + {fmtFinVal(val, row.is_margin)} + + ))} + + ); +} + +export function FinancialsCard({ + data, + statement, + period, + onChangeStatement, + onChangePeriod, +}: Props) { + const stmt = data[statement]; + const lastColIdx = stmt.columns.length - 1; + + return ( +
+
+
+ {(["income", "balance", "cash_flow"] as StatementKey[]).map((key) => ( + + ))} +
+
+ {(["annual", "quarterly"] as PeriodKey[]).map((p) => ( + + ))} +
+
+ + {stmt.columns.length === 0 ? ( +

Statement data unavailable.

+ ) : ( +
+ + + + + {stmt.columns.map((col, i) => ( + + ))} + + + + {stmt.rows.map((row, i) => ( + + ))} + +
USD (millions) + {col} +
+
+ )} +
+ ); +} -- cgit v1.3-2-g0d8e From fecdd44d856cdff8c8746551a3ae4e862a8752dc Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 18 May 2026 00:31:37 -0700 Subject: feat: add FinancialsPage data-fetching wrapper --- frontend/components/prism/FinancialsPage.tsx | 73 ++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 frontend/components/prism/FinancialsPage.tsx (limited to 'frontend/components/prism') diff --git a/frontend/components/prism/FinancialsPage.tsx b/frontend/components/prism/FinancialsPage.tsx new file mode 100644 index 0000000..fcd2763 --- /dev/null +++ b/frontend/components/prism/FinancialsPage.tsx @@ -0,0 +1,73 @@ +"use client"; +import { useEffect, useState } from "react"; +import { api } from "@/lib/api"; +import { buildKpis } from "@/lib/overview"; +import { FinancialsCard } from "@/components/prism/FinancialsCard"; +import { KPIStrip } from "@/components/prism/KPIStrip"; +import { TickerHeader } from "@/components/prism/TickerHeader"; +import type { FinancialsResponse, TickerOverview } from "@/types/api"; + +type StatementKey = "income" | "balance" | "cash_flow"; +type PeriodKey = "annual" | "quarterly"; +type FinState = "loading" | "ready" | "error"; + +type Props = { + ticker: string; + overview: TickerOverview; + isSaved: boolean; + onToggleWatchlist: () => void; +}; + +export function FinancialsPage({ ticker, overview, isSaved, onToggleWatchlist }: Props) { + const [statement, setStatement] = useState("income"); + const [period, setPeriod] = useState("annual"); + const [data, setData] = useState(null); + const [finState, setFinState] = useState("loading"); + const kpis = buildKpis(overview); + + useEffect(() => { + let cancelled = false; + setFinState("loading"); + setData(null); + + api + .financials(ticker, period) + .then((res) => { + if (!cancelled) { + setData(res); + setFinState("ready"); + } + }) + .catch(() => { + if (!cancelled) setFinState("error"); + }); + + return () => { + cancelled = true; + }; + }, [ticker, period]); + + return ( + <> + + + {finState === "loading" && ( +
+ )} + {finState === "error" && ( +
+

Financial statements unavailable for {ticker}.

+
+ )} + {finState === "ready" && data && ( + + )} + + ); +} -- cgit v1.3-2-g0d8e From 7cb2492c748556af99f2b155a434b92f19461095 Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 18 May 2026 00:33:39 -0700 Subject: feat: wire financials tab routing in Sidebar and page.tsx Co-Authored-By: Claude Sonnet 4.6 --- frontend/app/page.tsx | 63 +++++++++++++++++++++++++---------- frontend/components/prism/Sidebar.tsx | 5 ++- 2 files changed, 49 insertions(+), 19 deletions(-) (limited to 'frontend/components/prism') diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 013b93d..6f7e5b6 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -4,6 +4,7 @@ import { FormEvent, Suspense, startTransition, useCallback, useEffect, useMemo, import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import { AppShell } from "@/components/prism/AppShell"; +import { FinancialsPage } from "@/components/prism/FinancialsPage"; import { ChartCard } from "@/components/prism/ChartCard"; import { KPIStrip } from "@/components/prism/KPIStrip"; import { Sidebar } from "@/components/prism/Sidebar"; @@ -29,6 +30,7 @@ function OverviewClient() { const router = useRouter(); const searchParams = useSearchParams(); const selectedTicker = (searchParams.get("ticker") || "").toUpperCase(); + const tab = searchParams.get("tab") || "overview"; const lastTickerRef = useRef(""); const [query, setQuery] = useState(""); @@ -72,11 +74,26 @@ function OverviewClient() { setOverviewState("loading"); setChartState("loading"); + const params = new URLSearchParams(); + params.set("ticker", normalized); + if (tab !== "overview") params.set("tab", tab); startTransition(() => { - router.push(`/?ticker=${encodeURIComponent(normalized)}`); + router.push(`/?${params.toString()}`); }); }, - [router] + [router, tab] + ); + + const navigateToTab = useCallback( + (key: string) => { + const params = new URLSearchParams(); + if (selectedTicker) params.set("ticker", selectedTicker); + if (key !== "overview") params.set("tab", key); + startTransition(() => { + router.push(`/?${params.toString()}`); + }); + }, + [router, selectedTicker] ); const clearTicker = useCallback(() => { @@ -245,12 +262,13 @@ function OverviewClient() { sidebar={ } topbar={ @@ -271,22 +289,31 @@ function OverviewClient() { {selectedTicker && overviewState === "invalid" ? : null} {selectedTicker && overviewState === "error" ? : null} {overview && overviewState === "ready" ? ( - <> - - -
-
- - -
-
- - - - + tab === "financials" ? ( + + ) : ( + <> + + +
+
+ + +
+
+ + + + +
-
- + + ) ) : null} ); diff --git a/frontend/components/prism/Sidebar.tsx b/frontend/components/prism/Sidebar.tsx index 7f106d8..fb8ebcf 100644 --- a/frontend/components/prism/Sidebar.tsx +++ b/frontend/components/prism/Sidebar.tsx @@ -12,6 +12,7 @@ type Props = { watchlistError: string | null; onSelectTicker: (symbol: string) => void; onRemoveTicker: (symbol: string) => void; + onSelectTab: (key: string) => void; }; export function Sidebar({ @@ -21,7 +22,8 @@ export function Sidebar({ watchlist, watchlistError, onSelectTicker, - onRemoveTicker + onRemoveTicker, + onSelectTab }: Props) { return (