diff options
Diffstat (limited to 'frontend/components/prism/Sidebar.tsx')
| -rw-r--r-- | frontend/components/prism/Sidebar.tsx | 101 |
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> + ); +} |
