From 62bdd79b3473262dde5fb0a90eab34fe7bf344fd Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Sun, 17 May 2026 13:07:40 -0700 Subject: 'UI Shell and General Architecture' --- frontend/components/prism/AppShell.tsx | 20 ++++++ frontend/components/prism/ChartCard.tsx | 55 ++++++++++++++++ frontend/components/prism/KPIStrip.tsx | 16 +++++ frontend/components/prism/Sidebar.tsx | 101 +++++++++++++++++++++++++++++ frontend/components/prism/TickerHeader.tsx | 51 +++++++++++++++ frontend/components/prism/TopBar.tsx | 61 +++++++++++++++++ 6 files changed, 304 insertions(+) create mode 100644 frontend/components/prism/AppShell.tsx create mode 100644 frontend/components/prism/ChartCard.tsx create mode 100644 frontend/components/prism/KPIStrip.tsx create mode 100644 frontend/components/prism/Sidebar.tsx create mode 100644 frontend/components/prism/TickerHeader.tsx create mode 100644 frontend/components/prism/TopBar.tsx (limited to 'frontend/components/prism') diff --git a/frontend/components/prism/AppShell.tsx b/frontend/components/prism/AppShell.tsx new file mode 100644 index 0000000..63fd62b --- /dev/null +++ b/frontend/components/prism/AppShell.tsx @@ -0,0 +1,20 @@ +import type { ReactNode } from "react"; + +type Props = { + sidebar: ReactNode; + topbar: ReactNode; + children: ReactNode; +}; + +export function AppShell({ sidebar, topbar, children }: Props) { + return ( +
+ {sidebar} +
+ {topbar} +
{children}
+
+
+ ); +} + diff --git a/frontend/components/prism/ChartCard.tsx b/frontend/components/prism/ChartCard.tsx new file mode 100644 index 0000000..bc650d7 --- /dev/null +++ b/frontend/components/prism/ChartCard.tsx @@ -0,0 +1,55 @@ +import { PriceChart } from "@/components/PriceChart"; +import type { HistoryPoint } 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 Props = { + symbol: string; + period: string; + points: HistoryPoint[]; + chartState: "idle" | "loading" | "ready" | "error"; + chartError: string | null; + onChangePeriod: (period: string) => void; +}; + +export function ChartCard({ symbol, period, points, chartState, chartError, onChangePeriod }: Props) { + return ( +
+
+
+
Price History
+

{symbol}

+
+
+ {PERIODS.map((option) => ( + + ))} +
+
+ +

Chart loading is isolated from the rest of Overview. A history miss only affects this card.

+ +
+ {chartState === "loading" ?
Loading {period.toUpperCase()} history…
: null} + {chartState === "error" ?
{chartError || "Could not load chart history."}
: null} + {chartState === "ready" ? : null} +
+
+ ); +} + diff --git a/frontend/components/prism/KPIStrip.tsx b/frontend/components/prism/KPIStrip.tsx new file mode 100644 index 0000000..b3cd34c --- /dev/null +++ b/frontend/components/prism/KPIStrip.tsx @@ -0,0 +1,16 @@ +import type { KpiItem } from "@/lib/overview"; + +export function KPIStrip({ items }: { items: KpiItem[] }) { + return ( +
+ {items.map((item) => ( +
+ {item.key} + {item.value} + {item.sublabel} +
+ ))} +
+ ); +} + diff --git a/frontend/components/prism/Sidebar.tsx b/frontend/components/prism/Sidebar.tsx new file mode 100644 index 0000000..15a2947 --- /dev/null +++ b/frontend/components/prism/Sidebar.tsx @@ -0,0 +1,101 @@ +import Image from "next/image"; +import type { NavItem } from "@/lib/overview"; +import { deltaClass, fmtCurrency, fmtPct } from "@/lib/format"; +import { watchlistSubtitle } from "@/lib/overview"; +import type { WatchlistResponse } from "@/types/api"; + +type Props = { + navItems: NavItem[]; + selectedKey: string; + currentTicker: string; + watchlist: WatchlistResponse; + watchlistError: string | null; + onSelectTicker: (symbol: string) => void; + onRemoveTicker: (symbol: string) => void; +}; + +export function Sidebar({ + navItems, + selectedKey, + currentTicker, + watchlist, + watchlistError, + onSelectTicker, + onRemoveTicker +}: Props) { + return ( + + ); +} diff --git a/frontend/components/prism/TickerHeader.tsx b/frontend/components/prism/TickerHeader.tsx new file mode 100644 index 0000000..23254f8 --- /dev/null +++ b/frontend/components/prism/TickerHeader.tsx @@ -0,0 +1,51 @@ +import { deltaClass, fmtCurrency, fmtPct } from "@/lib/format"; +import { buildIdentityLine, rangePercent } from "@/lib/overview"; +import type { TickerOverview } from "@/types/api"; + +type Props = { + overview: TickerOverview; + onToggleWatchlist: () => void; + isSaved: boolean; +}; + +export function TickerHeader({ overview, onToggleWatchlist, isSaved }: Props) { + const pct = rangePercent(overview); + + return ( +
+
+ {overview.profile.symbol} +
+ {overview.profile.symbol} + {overview.profile.name || "Name unavailable"} + {overview.meta.is_partial ? Partial Data : null} +
+

{buildIdentityLine(overview)}

+
+ +
+
52 Week Range
+
+ {fmtCurrency(overview.range_52w.low)} + {fmtCurrency(overview.range_52w.price ?? overview.quote.price)} + {fmtCurrency(overview.range_52w.high)} +
+
+ {pct != null ? : null} +
+
+ +
+ {fmtCurrency(overview.quote.price)} + + {fmtCurrency(overview.quote.change)} · {fmtPct(overview.quote.change_pct, 2, true)} + + Prev close {fmtCurrency(overview.quote.prev_close)} + +
+
+ ); +} + diff --git a/frontend/components/prism/TopBar.tsx b/frontend/components/prism/TopBar.tsx new file mode 100644 index 0000000..38e8447 --- /dev/null +++ b/frontend/components/prism/TopBar.tsx @@ -0,0 +1,61 @@ +import type { FormEvent } from "react"; +import type { SearchResult } from "@/types/api"; + +type Props = { + query: string; + searching: boolean; + results: SearchResult[]; + marketStatus: { isOpen: boolean; status: string; time: string }; + onChangeQuery: (value: string) => void; + onSubmit: (event: FormEvent) => void; + onSelectTicker: (symbol: string) => void; +}; + +export function TopBar({ query, searching, results, marketStatus, onChangeQuery, onSubmit, onSelectTicker }: Props) { + const showDropdown = query.trim().length >= 2; + + return ( +
+
+
+ + onChangeQuery(event.target.value)} placeholder="Search ticker or company" aria-label="Search ticker" /> + Enter + + + {showDropdown ? ( +
+ {searching ?
Searching...
: null} + {!searching && results.length === 0 ?
No matches
: null} + {!searching + ? results.map((result) => ( + + )) + : null} +
+ ) : null} +
+ +
+
+ + {marketStatus.status} + {marketStatus.time} +
+ +
+ + T + Local Profile +
+
+
+ ); +} + -- cgit v1.3-2-g0d8e