- Fix OptionsAdapter for yahoo-finance2 v3 API ({ date: expiry } instead of positional arg)
- Fix institutional.flow and insiderStream endpoints to query correct DB tables
- Fix anonymous user INSERT (pw_hash column, not password_hash)
- Create secDataFetcher module for 13F/Form 4 data population
- Add institutional and marketOutlook endpoints to frontend trpc client
- Build out Chart Lab, Institutional, Market Outlook pages with real components
- Add focus-visible rings, aria-labels, hex tokens, shape icons across all panels
- Integrate EmotionLogger, MobileTabNav, useKeyboardShortcuts
- Add localStorage persistence for emotion logs
- Normalize spacing, type scale, hover effects across all components
120 lines
4.7 KiB
TypeScript
120 lines
4.7 KiB
TypeScript
"use client";
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { useActiveSymbol } from "@/stores/active-symbol-store";
|
|
import { api } from "@/lib/trpc";
|
|
import { UI_STRINGS } from "@/lib/strings";
|
|
|
|
/**
|
|
* Persistent top bar showing the active symbol, price, change, and search.
|
|
* This is the "central nervous system" — switching symbols updates everything globally.
|
|
*/
|
|
export function SymbolHeader() {
|
|
const symbol = useActiveSymbol((s) => s.activeSymbol);
|
|
const setActive = useActiveSymbol((s) => s.setActive);
|
|
const [draft, setDraft] = useState(symbol);
|
|
const [showSearch, setShowSearch] = useState(false);
|
|
const [snap, setSnap] = useState<ReturnType<typeof api.market.snapshot> | null>(null);
|
|
|
|
// Fetch current price for display
|
|
useEffect(() => {
|
|
const sym = draft || symbol;
|
|
api.market.snapshot(sym).then(setSnap).catch(() => {});
|
|
}, [draft, symbol]);
|
|
|
|
const quote = snap?.quote ?? null;
|
|
const sector = snap?.sector ?? null;
|
|
|
|
const up = (quote?.change ?? 0) >= 0;
|
|
const priceLabel = quote ? `$${quote.price?.toFixed(2)}` : "";
|
|
const changeLabel = quote && quote.change !== undefined
|
|
? `${up ? "▲" : "▼"}${up ? "+" : ""}${quote.change.toFixed(2)} (${up ? "+" : ""}${(quote.changePercent ?? 0).toFixed(2)}%)`
|
|
: "";
|
|
const sectorLabel = sector ? `${sector.sector} · ${sector.tickerKind}` : "";
|
|
|
|
const handleSubmit = useCallback((e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const sym = draft.trim().toUpperCase() || symbol;
|
|
setActive(sym);
|
|
setShowSearch(false);
|
|
}, [draft, symbol, setActive]);
|
|
|
|
// Keyboard shortcut: Ctrl+K to toggle search
|
|
useEffect(() => {
|
|
const handler = (e: KeyboardEvent) => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
e.preventDefault();
|
|
setShowSearch(prev => !prev);
|
|
}
|
|
if (e.key === "Escape" && showSearch) {
|
|
setShowSearch(false);
|
|
}
|
|
};
|
|
window.addEventListener("keydown", handler);
|
|
return () => window.removeEventListener("keydown", handler);
|
|
}, [showSearch]);
|
|
|
|
// Sync draft when active symbol changes externally
|
|
useEffect(() => {
|
|
setDraft(symbol);
|
|
}, [symbol]);
|
|
|
|
return (
|
|
<header className="sticky top-0 z-50 border-b border-[#2a2b3a] bg-[#121318]/95 backdrop-blur-sm">
|
|
<div className="flex items-center justify-between px-4 py-3">
|
|
{/* Left: Logo mark */}
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
<div className="w-7 h-7 rounded-lg bg-[#4f8cff] flex items-center justify-center">
|
|
<span className="text-xs font-bold text-[#121318]">IF</span>
|
|
</div>
|
|
<h1 className="hidden sm:block text-sm font-semibold text-[#e6e7ec] tracking-tight">
|
|
Investor Flow
|
|
</h1>
|
|
</div>
|
|
|
|
{/* Right: Ticker info + Search grouped together */}
|
|
<div className="flex items-center gap-4">
|
|
{/* Active symbol — compact, next to search */}
|
|
<div className="hidden sm:flex items-baseline gap-2 text-xs">
|
|
<span className="font-bold text-[#e6e7ec]">{symbol}</span>
|
|
{priceLabel && (
|
|
<span className="font-mono text-[#8a8b9a]">{priceLabel}</span>
|
|
)}
|
|
{changeLabel && (
|
|
<span className={`font-mono ${up ? "text-[#34d399]" : "text-[#fbbf24]"}`}>
|
|
{changeLabel}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Mobile: just ticker */}
|
|
<div className="sm:hidden flex items-center gap-1.5 text-xs">
|
|
<span className="font-bold text-[#e6e7ec]">{symbol}</span>
|
|
{priceLabel && (
|
|
<span className="font-mono text-[#8a8b9a]">{priceLabel}</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Search */}
|
|
<form onSubmit={handleSubmit} className="flex items-center">
|
|
<label className="sr-only" htmlFor="symbol-search">Search symbol</label>
|
|
<div className={`relative transition-all duration-200 ${showSearch ? "w-56" : "w-40"}`}>
|
|
<input
|
|
id="symbol-search"
|
|
value={draft}
|
|
onChange={(e) => setDraft(e.target.value)}
|
|
onFocus={() => setShowSearch(true)}
|
|
onBlur={() => setTimeout(() => setShowSearch(false), 200)}
|
|
placeholder="Search…"
|
|
className="w-full bg-[#1a1b24] border border-[#2a2b3a] rounded-lg px-3 py-1.5 text-xs text-[#e6e7ec] placeholder:text-[#5a5b6a] focus:outline-none focus:border-[#4f8cff] focus:ring-1 focus:ring-[#4f8cff]"
|
|
/>
|
|
<kbd className="absolute right-2 top-1/2 -translate-y-1/2 text-[9px] text-[#5a5b6a] bg-[#0a0b0f] border border-[#2a2b3a] rounded px-1.5">
|
|
⌘K
|
|
</kbd>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|