diff options
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/app/page.tsx | 35 | ||||
| -rw-r--r-- | frontend/components/prism/ValuationOverviewCard.tsx | 44 |
2 files changed, 74 insertions, 5 deletions
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 150aaff..07bdfa4 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -17,10 +17,11 @@ import { VolumeCard } from "@/components/prism/VolumeCard"; import { ApiError, api } from "@/lib/api"; import { deltaClass, fmtNumber, fmtPct } from "@/lib/format"; import { buildKpis, limitIndices, marketClock, OVERVIEW_NAV_ITEMS, signalTone } from "@/lib/overview"; -import type { HistoryPoint, MarketIndex, SearchResult, TickerOverview, WatchlistResponse } from "@/types/api"; +import type { HistoryPoint, MarketIndex, SearchResult, TickerOverview, ValuationResponse, WatchlistResponse } from "@/types/api"; type LoadState = "idle" | "loading" | "ready" | "invalid" | "error"; type ChartState = "idle" | "loading" | "ready" | "error"; +type ValState = "idle" | "loading" | "ready" | "error"; export default function OverviewPage() { return ( @@ -49,6 +50,8 @@ function OverviewClient() { const [overviewError, setOverviewError] = useState<string | null>(null); const [chartState, setChartState] = useState<ChartState>("idle"); const [chartError, setChartError] = useState<string | null>(null); + const [valuation, setValuation] = useState<ValuationResponse | null>(null); + const [valState, setValState] = useState<ValState>("idle"); const [watchlistError, setWatchlistError] = useState<string | null>(null); const [clockSnapshot, setClockSnapshot] = useState(() => marketClock()); @@ -229,6 +232,34 @@ function OverviewClient() { }; }, [selectedTicker, period]); + useEffect(() => { + if (!selectedTicker) { + setValuation(null); + setValState("idle"); + return; + } + + let cancelled = false; + setValState("loading"); + setValuation(null); + + api + .valuation(selectedTicker) + .then((res) => { + if (!cancelled) { + setValuation(res); + setValState("ready"); + } + }) + .catch(() => { + if (!cancelled) setValState("error"); + }); + + return () => { + cancelled = true; + }; + }, [selectedTicker]); + async function onSearchSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); if (results[0]) { @@ -318,7 +349,7 @@ function OverviewClient() { <div className="psm-column"> <ProfileCard overview={overview} /> <ShortInterestCard overview={overview} /> - <ValuationOverviewCard overview={overview} /> + <ValuationOverviewCard overview={overview} valuation={valuation} valState={valState} /> <QualityCard overview={overview} /> </div> </div> diff --git a/frontend/components/prism/ValuationOverviewCard.tsx b/frontend/components/prism/ValuationOverviewCard.tsx index f85e443..9e59787 100644 --- a/frontend/components/prism/ValuationOverviewCard.tsx +++ b/frontend/components/prism/ValuationOverviewCard.tsx @@ -1,11 +1,15 @@ -import type { TickerOverview } from "@/types/api"; -import { fmtCurrency, fmtLarge, fmtNumber } from "@/lib/format"; +import type { TickerOverview, ValuationResponse } from "@/types/api"; +import { fmtCurrency, fmtLarge, fmtNumber, fmtPct } from "@/lib/format"; + +type ValState = "idle" | "loading" | "ready" | "error"; type Props = { overview: TickerOverview; + valuation: ValuationResponse | null; + valState: ValState; }; -export function ValuationOverviewCard({ overview }: Props) { +export function ValuationOverviewCard({ overview, valuation, valState }: Props) { const rows = [ { label: "Market Cap", value: fmtLarge(overview.stats.market_cap), missing: overview.stats.market_cap == null }, { label: "P/E TTM", value: overview.stats.trailing_pe == null ? "-" : `${fmtNumber(overview.stats.trailing_pe)}x`, missing: overview.stats.trailing_pe == null }, @@ -17,6 +21,8 @@ export function ValuationOverviewCard({ overview }: Props) { ]; const visible = rows.filter((r) => !r.missing); + const dcf = valuation?.dcf; + const showDcf = dcf?.available && dcf.intrinsic_value_per_share != null; return ( <section className="psm-card"> @@ -36,6 +42,38 @@ export function ValuationOverviewCard({ overview }: Props) { ))} </div> )} + {valState === "loading" && ( + <> + <hr className="psm-divider" /> + <div className="psm-stat-list"> + <div className="psm-stat-row"> + <span className="psm-stat-label psm-skeleton" style={{ width: "120px", height: "14px", display: "inline-block" }} /> + <span className="psm-stat-value psm-skeleton" style={{ width: "60px", height: "14px", display: "inline-block" }} /> + </div> + <div className="psm-stat-row"> + <span className="psm-stat-label psm-skeleton" style={{ width: "100px", height: "14px", display: "inline-block" }} /> + <span className="psm-stat-value psm-skeleton" style={{ width: "48px", height: "14px", display: "inline-block" }} /> + </div> + </div> + </> + )} + {valState === "ready" && showDcf && dcf && ( + <> + <hr className="psm-divider" /> + <div className="psm-stat-list"> + <div className="psm-stat-row"> + <span className="psm-stat-label">DCF Intrinsic Value</span> + <span className="psm-stat-value">{fmtCurrency(dcf.intrinsic_value_per_share)}</span> + </div> + {dcf.growth_rate_used != null && ( + <div className="psm-stat-row"> + <span className="psm-stat-label">FCF Growth Used</span> + <span className="psm-stat-value">{fmtPct(dcf.growth_rate_used)}</span> + </div> + )} + </div> + </> + )} </section> ); } |
