Files
investor-flow/app/src/components/SymbolHeader.tsx
T
Investor Flow Build 55f07e6b42 fix: wire data adapters, fix OptionsAdapter v3, build Research pages
- 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
2026-07-05 08:50:12 -04:00

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>
);
}