From 49907ef9991d2f13d53bb02fa648c8a06fbfe80a Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 18 May 2026 01:35:04 -0700 Subject: feat: add ValuationCard component and psm-val-* CSS --- frontend/components/prism/ValuationCard.tsx | 190 ++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 frontend/components/prism/ValuationCard.tsx (limited to 'frontend/components/prism/ValuationCard.tsx') diff --git a/frontend/components/prism/ValuationCard.tsx b/frontend/components/prism/ValuationCard.tsx new file mode 100644 index 0000000..7329ff5 --- /dev/null +++ b/frontend/components/prism/ValuationCard.tsx @@ -0,0 +1,190 @@ +"use client"; +import type { ValuationResponse } from "@/types/api"; +import { deltaClass, fmtCurrency, fmtLarge, fmtPct } from "@/lib/format"; + +type Props = { data: ValuationResponse }; + +function pctVsCurrent(implied?: number | null, current?: number | null): number | null { + if (implied == null || current == null || current === 0) return null; + return (implied - current) / current; +} + +function SummaryChip({ + label, + price, + current, + accent = false, +}: { + label: string; + price?: number | null; + current?: number | null; + accent?: boolean; +}) { + const pct = pctVsCurrent(price, current); + return ( +
+ {label} + {price != null ? fmtCurrency(price) : "—"} + {pct != null && ( + {fmtPct(pct, 1, true)} + )} +
+ ); +} + +function MultipleRow({ + label, + multiple, + price, + current, +}: { + label: string; + multiple?: number | null; + price?: number | null; + current?: number | null; +}) { + const pct = pctVsCurrent(price, current); + return ( +
+ {label} + {multiple != null ? `${multiple.toFixed(1)}×` : "—"} + {price != null ? fmtCurrency(price) : "—"} + + {pct != null ? fmtPct(pct, 1, true) : "—"} + +
+ ); +} + +export function ValuationCard({ data }: Props) { + const { dcf, ev_ebitda, ev_revenue, price_to_book, current_price } = data; + const dcfPrice = dcf.available && !dcf.error ? dcf.intrinsic_value_per_share : null; + const hasMultiples = ev_ebitda.available || ev_revenue.available || price_to_book.available; + + return ( +
+ {/* Summary strip */} +
+
+ Market Price + + {current_price != null ? fmtCurrency(current_price) : "—"} + +
+ + + + +
+ + {/* DCF detail */} +
+
+ Discounted Cash Flow + + WACC {(dcf.wacc * 100).toFixed(1)}% · Terminal {(dcf.terminal_growth * 100).toFixed(1)}% + +
+ + {!dcf.available && ( +

Insufficient free cash flow history for DCF.

+ )} + {dcf.available && dcf.error && ( +

{dcf.error}

+ )} + {dcf.available && !dcf.error && ( +
+
+
+ Base FCF (TTM) + {dcf.base_fcf != null ? fmtLarge(dcf.base_fcf) : "—"} +
+
+ Historical growth + + {dcf.growth_rate_used != null ? fmtPct(dcf.growth_rate_used) : "—"} + +
+
+ Enterprise Value + + {dcf.enterprise_value != null ? fmtLarge(dcf.enterprise_value) : "—"} + +
+
+ Net Debt + + {dcf.net_debt != null ? fmtLarge(dcf.net_debt) : "—"} + +
+
+ Equity Value + + {dcf.equity_value != null ? fmtLarge(dcf.equity_value) : "—"} + +
+
+
+ Intrinsic Value + + {dcf.intrinsic_value_per_share != null ? fmtCurrency(dcf.intrinsic_value_per_share) : "—"} + + {data.shares_outstanding != null && ( + + {fmtLarge(data.shares_outstanding)} shares + + )} +
+
+ )} +
+ + {/* Multiples */} + {hasMultiples && ( +
+
+ Multiples — at current market multiple +
+
+ {ev_ebitda.available && ( + + )} + {ev_revenue.available && ( + + )} + {price_to_book.available && ( + + )} +
+
+ )} +
+ ); +} -- cgit v1.3-2-g0d8e