"use client"; import { FormEvent, Suspense, startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react"; import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import { AppShell } from "@/components/prism/AppShell"; import { ChartCard } from "@/components/prism/ChartCard"; import { KPIStrip } from "@/components/prism/KPIStrip"; import { Sidebar } from "@/components/prism/Sidebar"; import { TickerHeader } from "@/components/prism/TickerHeader"; import { TopBar } from "@/components/prism/TopBar"; import { ApiError, api } from "@/lib/api"; import { deltaClass, fmtCurrency, fmtLarge, fmtNumber, fmtPct } from "@/lib/format"; import { availableFieldSummary, buildKpis, marketClock, OVERVIEW_NAV_ITEMS, signalTone, sortIndices, unavailableFields } from "@/lib/overview"; import type { HistoryPoint, MarketIndex, SearchResult, TickerOverview, WatchlistResponse } from "@/types/api"; type LoadState = "idle" | "loading" | "ready" | "invalid" | "error"; type ChartState = "idle" | "loading" | "ready" | "error"; export default function OverviewPage() { return ( }> ); } 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 [clockSnapshot, setClockSnapshot] = useState(() => marketClock()); const watchlistSymbols = useMemo(() => new Set(watchlist.items.map((item) => item.symbol)), [watchlist]); const isSaved = selectedTicker ? watchlistSymbols.has(selectedTicker) : false; const marketCards = useMemo(() => sortIndices(market), [market]); const kpis = useMemo(() => (overview ? buildKpis(overview) : []), [overview]); const missingFields = useMemo(() => (overview ? unavailableFields(overview) : []), [overview]); useEffect(() => { const timer = window.setInterval(() => setClockSnapshot(marketClock()), 60_000); return () => window.clearInterval(timer); }, []); const navigateToTicker = useCallback( (symbol: string) => { const normalized = symbol.trim().toUpperCase(); if (!normalized) return; setResults([]); setQuery(""); setOverview(null); setHistory([]); setOverviewError(null); setChartError(null); setWatchlistError(null); setOverviewState("loading"); setChartState("loading"); startTransition(() => { router.push(`/?ticker=${encodeURIComponent(normalized)}`); }); }, [router] ); const clearTicker = useCallback(() => { setOverview(null); setHistory([]); setOverviewError(null); setChartError(null); setWatchlistError(null); setOverviewState("idle"); setChartState("idle"); startTransition(() => { 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([]); setSearching(false); 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); setOverviewState("idle"); setChartState("idle"); return; } if (lastTickerRef.current === selectedTicker) return; let cancelled = false; lastTickerRef.current = selectedTicker; setOverviewState("loading"); setOverviewError(null); setOverview(null); api .overview(selectedTicker) .then((overviewData) => { if (cancelled) return; setOverview(overviewData); setOverviewState("ready"); }) .catch((exc: Error) => { if (cancelled) return; setOverview(null); setChartState("idle"); setHistory([]); if (exc instanceof ApiError && exc.status === 404) { setOverviewState("invalid"); setOverviewError("Ticker not found"); 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]) { navigateToTicker(results[0].symbol); return; } if (query.trim()) navigateToTicker(query); } async function addOrRemoveCurrentTicker() { 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"); } } async function removeFromWatchlist(symbol: string) { try { const next = await api.removeWatchlist(symbol); setWatchlist(next); setWatchlistError(null); } catch (exc) { setWatchlistError(exc instanceof Error ? exc.message : "Could not update watchlist"); } } const shell = ( } topbar={ } > {!selectedTicker ? : null} {selectedTicker && overviewState === "loading" ? : null} {selectedTicker && overviewState === "invalid" ? : null} {selectedTicker && overviewState === "error" ? : null} {overview && overviewState === "ready" ? ( <>
) : null}
); return shell; } function MarketStrip({ indices }: { indices: MarketIndex[] }) { if (!indices.length) { return
Market data is temporarily unavailable.
; } return (
{indices.map((index) => (
{index.name} {fmtNumber(index.price)} {fmtPct(index.change_pct, 2, true)}
))}
); } function EmptyOverviewState({ watchlist, onSelectTicker }: { watchlist: WatchlistResponse; onSelectTicker: (symbol: string) => void }) { return (
Overview

Choose a ticker to enter the workbench.

Search from the top bar or jump into one of your saved symbols from the sidebar watchlist.

{watchlist.items.length ? (
{watchlist.items.map((item) => ( ))}
) : null}
); } function LoadingOverviewState({ symbol }: { symbol: string }) { return (
Loading

{symbol}

Fetching quote, profile, signals, and supporting metrics.

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

{symbol}

This symbol could not be resolved into usable market data. Try another search or return to the empty workspace.

); } function ErrorOverviewState({ message }: { message: string }) { return (
Data Error

Overview unavailable

{message}

); } function SignalCard({ overview }: { overview: TickerOverview }) { return (
Signals

Readthrough

{overview.signals.map((signal) => (
{signal.key} {signal.value} {signal.description}
))}
); } function DataStatusCard({ overview, missingFields }: { overview: TickerOverview; missingFields: string[] }) { const entries = Object.entries(overview.meta.sources).slice(0, 6); return (
Data Quality

Coverage

{overview.meta.status}

{availableFieldSummary(overview)}

{overview.meta.is_partial ? (
{missingFields.length ? missingFields.slice(0, 8).map((field) => {field}) : null}
) : null}
{entries.length ? ( entries.map(([field, source]) => (
{field} {source}
)) ) : (
Sources Unavailable
)}
); } function ProfileCard({ overview }: { overview: TickerOverview }) { return (
Company Profile

Context

Sector {overview.profile.sector || "Unavailable"}
Industry {overview.profile.industry || "Unavailable"}
Exchange {overview.profile.exchange || "Unavailable"}
Website {overview.profile.website ? ( {overview.profile.website} ) : ( "Unavailable" )}

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

); } function ShortInterestCard({ overview }: { overview: TickerOverview }) { const short = overview.short_interest; return (
Short Interest

Pressure

); } function StatsCard({ overview }: { overview: TickerOverview }) { return (
Overview Stats

Reference

); } function DetailItem({ label, value, missing = false }: { label: string; value: string; missing?: boolean }) { return (
{label} {missing ? "Unavailable" : value}
); } function StatRow({ label, value, missing = false }: { label: string; value: string; missing?: boolean }) { return (
{label} {missing ? "Unavailable" : value}
); } function LoadingShell() { return (
Prism
Loading
} topbar={
Loading Prism…
} >
); }