From 257c25be1d5b6cba422ffe4f8bc2a31e76e1c68e Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 18 May 2026 22:14:42 -0700 Subject: feat: add RatiosCard component with hero KPIs, sparklines, and detail rows --- frontend/components/prism/RatiosCard.tsx | 212 +++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 frontend/components/prism/RatiosCard.tsx (limited to 'frontend/components/prism') diff --git a/frontend/components/prism/RatiosCard.tsx b/frontend/components/prism/RatiosCard.tsx new file mode 100644 index 0000000..1a00829 --- /dev/null +++ b/frontend/components/prism/RatiosCard.tsx @@ -0,0 +1,212 @@ +"use client"; + +import type { RatioPoint, RatiosResponse } from "@/types/api"; +import { fmtNumber, fmtPct } from "@/lib/format"; + +const BRASS = "#C2AA7A"; +const GAIN = "#4F8C5E"; + +type Props = { + data: RatiosResponse; +}; + +type ValueKind = "multiple" | "percent" | "coverage"; + +function buildLine(values: (number | null)[], width: number, height: number): string { + const numeric = values.filter((value): value is number => value != null && Number.isFinite(value)); + if (numeric.length === 0) return ""; + if (numeric.length === 1) { + const y = height / 2; + return `0,${y} ${width},${y}`; + } + + const min = Math.min(...numeric); + const max = Math.max(...numeric); + const range = max - min || 1; + + return numeric + .map((value, index) => { + const x = (index / (numeric.length - 1)) * width; + const y = max === min ? height / 2 : height - ((value - min) / range) * height; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }) + .join(" "); +} + +function fmtMultiple(value?: number | null): string { + if (value == null || Number.isNaN(value)) return "—"; + return `${fmtNumber(value)}x`; +} + +function fmtCoverage(value?: number | null): string { + if (value == null || Number.isNaN(value)) return "—"; + return `${fmtNumber(value)}x`; +} + +function formatValue(value: number | null, kind: ValueKind): string { + if (kind === "percent") return fmtPct(value); + if (kind === "coverage") return fmtCoverage(value); + return fmtMultiple(value); +} + +function MiniSpark({ values, color }: { values: (number | null)[]; color: string }) { + const points = buildLine(values, 88, 24); + if (!points) { + return ; + } + + return ( + + ); +} + +function HeroSpark({ values, color }: { values: (number | null)[]; color: string }) { + const points = buildLine(values, 196, 52); + if (!points) { + return No trend; + } + + return ( + + ); +} + +function HeroCard({ + label, + point, + kind, + color, +}: { + label: string; + point: RatioPoint; + kind: ValueKind; + color: string; +}) { + return ( +
+
+ {label} + + Sector {formatValue(point.vs_sector, kind)} + +
+
+ {formatValue(point.value, kind)} +
+ +
+ ); +} + +function DetailRow({ + label, + point, + kind, + color, +}: { + label: string; + point: RatioPoint; + kind: ValueKind; + color: string; +}) { + return ( +
+ {label} + {formatValue(point.value, kind)} + {formatValue(point.vs_sector, kind)} + +
+ ); +} + +function GroupHeader({ label }: { label: string }) { + return ( +
+ {label} + Current + Sector + Trend +
+ ); +} + +export function RatiosCard({ data }: Props) { + const showDividends = data.dividend_yield.value != null || data.dividend_payout.value != null; + + return ( +
+
+
+
Key Ratios
+

Key Ratios

+
+
+ +
+ + + + +
+ +
+
+ + + + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + + +
+ + {showDividends && ( +
+ + + +
+ )} +
+
+ ); +} -- cgit v1.3-2-g0d8e From 566f59cb00958caa073279a7bb698e1ede48c0d4 Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 18 May 2026 22:15:42 -0700 Subject: feat: add RatiosPage data-fetch wrapper --- frontend/components/prism/RatiosPage.tsx | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 frontend/components/prism/RatiosPage.tsx (limited to 'frontend/components/prism') diff --git a/frontend/components/prism/RatiosPage.tsx b/frontend/components/prism/RatiosPage.tsx new file mode 100644 index 0000000..26868f8 --- /dev/null +++ b/frontend/components/prism/RatiosPage.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { api } from "@/lib/api"; +import { RatiosCard } from "@/components/prism/RatiosCard"; +import type { RatiosResponse } from "@/types/api"; + +type RatiosState = "loading" | "ready" | "error"; + +type Props = { + ticker: string; +}; + +export function RatiosPage({ ticker }: Props) { + const [data, setData] = useState(null); + const [ratiosState, setRatiosState] = useState("loading"); + + useEffect(() => { + let cancelled = false; + setRatiosState("loading"); + setData(null); + + api + .ratios(ticker) + .then((res) => { + if (!cancelled) { + setData(res); + setRatiosState("ready"); + } + }) + .catch(() => { + if (!cancelled) setRatiosState("error"); + }); + + return () => { + cancelled = true; + }; + }, [ticker]); + + if (ratiosState === "loading") { + return
; + } + + if (ratiosState === "error") { + return ( +
+

Ratio data unavailable for {ticker}.

+
+ ); + } + + if (ratiosState === "ready" && data) { + return ; + } + + return null; +} -- cgit v1.3-2-g0d8e From 16d9eb4f864fe8c29a9dee57ec47f77b34ae0df4 Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 18 May 2026 22:18:20 -0700 Subject: feat: wire Ratios subtab into FinancialsPage, move tab strip up from FinancialsCard --- frontend/app/prism-shell.css | 6 +++ frontend/components/prism/FinancialsCard.tsx | 20 -------- frontend/components/prism/FinancialsPage.tsx | 68 ++++++++++++++++++++-------- 3 files changed, 56 insertions(+), 38 deletions(-) (limited to 'frontend/components/prism') diff --git a/frontend/app/prism-shell.css b/frontend/app/prism-shell.css index 424ebb3..4e65ced 100644 --- a/frontend/app/prism-shell.css +++ b/frontend/app/prism-shell.css @@ -1116,6 +1116,11 @@ overflow: hidden; } +.psm-fin-tab-bar { + border-bottom: 1px solid var(--line-1); + margin-bottom: 0; +} + .psm-fin-header { display: flex; align-items: stretch; @@ -1156,6 +1161,7 @@ display: flex; align-items: center; gap: var(--sp-1); + margin-left: auto; } .psm-fin-period-btn { diff --git a/frontend/components/prism/FinancialsCard.tsx b/frontend/components/prism/FinancialsCard.tsx index 94a6618..43a2dc2 100644 --- a/frontend/components/prism/FinancialsCard.tsx +++ b/frontend/components/prism/FinancialsCard.tsx @@ -9,16 +9,9 @@ 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)}%`; @@ -70,7 +63,6 @@ export function FinancialsCard({ data, statement, period, - onChangeStatement, onChangePeriod, }: Props) { const stmt = data[statement]; @@ -79,18 +71,6 @@ export function FinancialsCard({ return (
-
- {(["income", "balance", "cash_flow"] as StatementKey[]).map((key) => ( - - ))} -
{(["annual", "quarterly"] as PeriodKey[]).map((p) => ( + ))} +
+
+ {statement === "ratios" ? ( + + ) : ( + <> + {finState === "loading" && ( +
+ )} + {finState === "error" && ( +
+

Financial statements unavailable for {ticker}.

+
+ )} + {finState === "ready" && data && ( + + )} + )} ); -- cgit v1.3-2-g0d8e