summaryrefslogtreecommitdiff
path: root/frontend/components/prism/Sidebar.tsx
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-17 13:07:40 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-17 13:07:40 -0700
commit62bdd79b3473262dde5fb0a90eab34fe7bf344fd (patch)
tree84f75baf7503e1df77c8335750650a72b088468a /frontend/components/prism/Sidebar.tsx
parent1482422f2f5b236cdcdff4429ae06bb55dca4083 (diff)
'UI Shell and General Architecture'
Diffstat (limited to 'frontend/components/prism/Sidebar.tsx')
-rw-r--r--frontend/components/prism/Sidebar.tsx101
1 files changed, 101 insertions, 0 deletions
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 (
+ <aside className="psm-side">
+ <div className="psm-brand">
+ <Image className="psm-brand-mark" src="/design-system/logo-monogram.svg" alt="" width={34} height={34} />
+ <div className="psm-brand-copy">
+ <div className="psm-brand-name">Prism</div>
+ <div className="psm-brand-sub">Market Workbench</div>
+ </div>
+ </div>
+
+ <div className="psm-side-section">
+ <div className="psm-side-label">Workspace</div>
+ </div>
+
+ <nav className="psm-nav" aria-label="Primary">
+ {navItems.map((item) => {
+ const active = item.key === selectedKey;
+ return (
+ <button
+ key={item.key}
+ type="button"
+ className={`psm-nav-item${active ? " active" : ""}${item.disabled ? " disabled" : ""}`}
+ aria-disabled={item.disabled ? "true" : undefined}
+ >
+ <span className={`psm-icon icon-${item.icon}`} aria-hidden />
+ <span className="psm-nav-copy">
+ <span>{item.label}</span>
+ {item.disabled ? <span className="psm-nav-coming">Soon</span> : null}
+ </span>
+ </button>
+ );
+ })}
+ </nav>
+
+ <div className="psm-side-section">
+ <div className="psm-side-label">Watchlist</div>
+ </div>
+
+ <div className="psm-watch">
+ <div className="psm-watch-toolbar">
+ <div className="psm-watch-limit">
+ {watchlist.items.length}/{watchlist.limit}
+ </div>
+ </div>
+
+ {watchlist.items.length === 0 ? <div className="psm-watch-empty">Saved tickers will appear here.</div> : null}
+
+ {watchlist.items.map((item) => {
+ const active = item.symbol === currentTicker;
+ return (
+ <div key={item.symbol} className={`psm-watch-row${active ? " active" : ""}`}>
+ <button type="button" className="psm-watch-select" onClick={() => onSelectTicker(item.symbol)}>
+ <span className="psm-watch-main">
+ <span className="psm-watch-symbol">{item.symbol}</span>
+ <span className="psm-watch-date">{watchlistSubtitle(item)}</span>
+ </span>
+ <span className="psm-watch-price">{fmtCurrency(item.quote?.price)}</span>
+ <span className={`psm-watch-change ${deltaClass(item.quote?.change_pct)}`}>{fmtPct(item.quote?.change_pct, 2, true)}</span>
+ </button>
+ <button
+ type="button"
+ aria-label={`Remove ${item.symbol} from watchlist`}
+ className="psm-watch-remove"
+ onClick={() => onRemoveTicker(item.symbol)}
+ >
+ ×
+ </button>
+ </div>
+ );
+ })}
+
+ {watchlistError ? <p className="psm-muted-copy psm-error-copy">{watchlistError}</p> : null}
+ </div>
+ </aside>
+ );
+}