"use client"; import { FormEvent, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { BookmarkMinus, BookmarkPlus, Search } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { PriceChart } from "@/components/PriceChart"; import { ApiError, api } from "@/lib/api"; import { deltaClass, fmtCurrency, fmtLarge, fmtNumber, fmtPct } from "@/lib/format"; import type { HistoryPoint, MarketIndex, SearchResult, TickerOverview, WatchlistResponse } from "@/types/api"; const PERIODS = [ { key: "1m", label: "1M" }, { key: "3m", label: "3M" }, { key: "6m", label: "6M" }, { key: "1y", label: "1Y" }, { key: "5y", label: "5Y" } ]; type LoadState = "idle" | "loading" | "ready" | "invalid" | "error"; type ChartState = "idle" | "loading" | "ready" | "error"; export default function OverviewPage() { return (
Loading Prism...
}>
); } function OverviewClient() { const router = useRouter(); const searchParams = useSearchParams(); const selectedTicker = (searchParams.get("ticker") || "").toUpperCase(); const lastTickerRef = useRef(""); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const [market, setMarket] = useState([]); const [overview, setOverview] = useState(null); const [history, setHistory] = useState([]); const [watchlist, setWatchlist] = useState({ items: [], limit: 10 }); const [period, setPeriod] = useState("1y"); const [overviewState, setOverviewState] = useState("idle"); const [overviewError, setOverviewError] = useState(null); const [chartState, setChartState] = useState("idle"); const [chartError, setChartError] = useState(null); const [watchlistError, setWatchlistError] = useState(null); const watchlistSymbols = useMemo(() => new Set(watchlist.items.map((item) => item.symbol)), [watchlist]); const isSaved = selectedTicker ? watchlistSymbols.has(selectedTicker) : false; const selectTicker = useCallback( (symbol: string) => { const normalized = symbol.trim().toUpperCase(); if (!normalized) return; setResults([]); setQuery(""); setOverviewError(null); setChartError(null); setWatchlistError(null); setOverviewState("loading"); setChartState("loading"); setOverview(null); setHistory([]); router.push(`/?ticker=${encodeURIComponent(normalized)}`); }, [router] ); const clearTicker = useCallback(() => { setOverview(null); setHistory([]); setOverviewError(null); setChartError(null); setWatchlistError(null); setOverviewState("idle"); setChartState("idle"); router.push("/"); }, [router]); const refreshWatchlist = useCallback(async () => { try { setWatchlist(await api.watchlist()); } catch { setWatchlist({ items: [], limit: 10 }); } }, []); useEffect(() => { api.marketIndices().then(setMarket).catch(() => setMarket([])); refreshWatchlist(); }, [refreshWatchlist]); useEffect(() => { if (query.trim().length < 2) { setResults([]); return; } let cancelled = false; setSearching(true); const timer = window.setTimeout(() => { api .search(query) .then((rows) => { if (!cancelled) setResults(rows); }) .catch(() => { if (!cancelled) setResults([]); }) .finally(() => { if (!cancelled) setSearching(false); }); }, 250); return () => { cancelled = true; window.clearTimeout(timer); }; }, [query]); useEffect(() => { if (!selectedTicker) { lastTickerRef.current = ""; setOverview(null); setHistory([]); setOverviewError(null); setChartError(null); setWatchlistError(null); setOverviewState("idle"); setChartState("idle"); return; } if (lastTickerRef.current === selectedTicker) return; let cancelled = false; lastTickerRef.current = selectedTicker; setOverviewState("loading"); setOverviewError(null); setWatchlistError(null); setOverview(null); api .overview(selectedTicker) .then((overviewData) => { if (cancelled) return; setOverview(overviewData); setOverviewState("ready"); }) .catch((exc: Error) => { if (cancelled) return; setOverview(null); if (exc instanceof ApiError && exc.status === 404) { setOverviewState("invalid"); setOverviewError("Ticker not found"); setChartState("idle"); setHistory([]); return; } setOverviewState("error"); setOverviewError(exc.message || "Ticker data unavailable"); }); return () => { cancelled = true; }; }, [selectedTicker]); useEffect(() => { if (!selectedTicker) return; let cancelled = false; setChartState("loading"); setChartError(null); setHistory([]); api .history(selectedTicker, period) .then((historyData) => { if (cancelled) return; setHistory(historyData); setChartState("ready"); }) .catch((exc: Error) => { if (cancelled) return; setHistory([]); setChartState("error"); setChartError(exc.message || "Could not load chart history"); }); return () => { cancelled = true; }; }, [selectedTicker, period]); async function onSearchSubmit(event: FormEvent) { event.preventDefault(); if (results[0]) selectTicker(results[0].symbol); else if (query.trim()) selectTicker(query); } async function toggleWatchlist() { if (!selectedTicker) return; try { const next = isSaved ? await api.removeWatchlist(selectedTicker) : await api.addWatchlist(selectedTicker); setWatchlist(next); setWatchlistError(null); } catch (exc) { setWatchlistError(exc instanceof Error ? exc.message : "Could not update watchlist"); } } const summaryTicker = overview?.profile.symbol || selectedTicker; const summaryName = overviewState === "invalid" ? "Ticker not found" : overview?.profile.name || (selectedTicker ? "Loading ticker..." : "No ticker selected"); return (
{!selectedTicker && } {selectedTicker && overviewState === "loading" &&
Loading {selectedTicker}...
} {selectedTicker && overviewState === "invalid" && } {selectedTicker && overviewState === "error" &&
{overviewError || "Ticker data unavailable"}
} {overview && overviewState === "ready" && (
{overview.profile.exchange || "Ticker"}

{overview.profile.name}

{overview.meta.is_partial && Partial Data}

{identityLine(overview)}

{fmtCurrency(overview.quote.price)} {fmtPct(overview.quote.change_pct, 2, true)} Prev close {fmtCurrency(overview.quote.prev_close)}
{overview.meta.is_partial &&
Some fields are unavailable for this ticker. Available data is still shown below.
}
{overview.signals.map((signal) => (
{signal.key} {signal.value} {signal.description}
))}
Price History
{overview.profile.symbol}
{PERIODS.map((option) => ( ))}
{chartState === "loading" &&
Loading {period.toUpperCase()} history...
} {chartState === "error" &&
{chartError || "Could not load chart history"}
} {chartState === "ready" && }
)}
); } function MarketBar({ indices }: { indices: MarketIndex[] }) { if (!indices.length) return
Market data unavailable
; return (
{indices.map((index) => (
{index.name} {fmtNumber(index.price)} {fmtPct(index.change_pct, 2, true)}
))}
); } function EmptyState({ watchlist, onSelect }: { watchlist: WatchlistResponse; onSelect: (symbol: string) => void }) { return (

Overview

{watchlist.items.length ? ( <>

Select a saved ticker to load its company profile, quote, stats, and chart.

{watchlist.items.map((item) => ( ))}
) : (

Search for a ticker to begin.

)}
); } function InvalidTickerState({ symbol, onClear }: { symbol: string; onClear: () => void }) { return (

{symbol}

This ticker could not be resolved to usable market data.

); } function Stat({ label, value, missing = false }: { label: string; value: string; missing?: boolean }) { return (
{label} {missing ? "Unavailable" : value}
); } function RangeCard({ overview }: { overview: TickerOverview }) { const low = overview.range_52w.low; const high = overview.range_52w.high; const price = overview.range_52w.price; const pct = low != null && high != null && price != null && high > low ? Math.max(0, Math.min(100, ((price - low) / (high - low)) * 100)) : null; return (
52 Week Range
{pct === null ? (
Range unavailable
) : ( <>
{fmtCurrency(low)} {fmtCurrency(price)} {fmtCurrency(high)}
)}
); } function ShortInterest({ overview }: { overview: TickerOverview }) { const short = overview.short_interest; return (
Short Interest
); } function ProfileCard({ overview }: { overview: TickerOverview }) { return (
Company Profile
Sector {overview.profile.sector || "Unavailable"}
Industry {overview.profile.industry || "Unavailable"}
Website {overview.profile.website ? ( {overview.profile.website} ) : ( Unavailable )}

{overview.profile.summary || "Business summary unavailable."}

); } function SourceCard({ overview }: { overview: TickerOverview }) { const fields = Object.entries(overview.meta.sources).slice(0, 6); return (
Data Quality
Status {overview.meta.status}
{fields.length ? ( fields.map(([field, source]) => (
{field} {source}
)) ) : (
Sources Unavailable
)}
); } function identityLine(overview: TickerOverview) { const parts = [overview.profile.symbol, overview.profile.sector, overview.profile.industry].filter(Boolean); return parts.length ? parts.join(" ยท ") : overview.profile.symbol; }