Files
investor-flow/app/src/components/SymbolHeader.tsx
T

106 lines
4.1 KiB
TypeScript
Raw Normal View History

"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);
// Fetch current price for display
const { data: snap } = api.market.snapshot.useQuery(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 gap-4 px-4 py-2.5">
{/* Symbol Info */}
<div className="flex items-center gap-3 flex-shrink-0">
<h1 className="text-base font-bold text-[#4f8cff]">Investor Flow</h1>
<div className="hidden sm:flex items-baseline gap-2">
<span className="text-lg font-bold text-[#e6e7ec]">{symbol}</span>
{priceLabel && <span className="text-sm font-mono text-[#e6e7ec]">{priceLabel}</span>}
{changeLabel && (
<span className={`text-xs font-mono ${up ? "text-[#34d399]" : "text-[#fbbf24]"}`}>
{changeLabel}
</span>
)}
</div>
</div>
{/* Search Input */}
<form onSubmit={handleSubmit} className="flex-1 max-w-xs">
<div className={`relative transition-all duration-200 ${showSearch ? "w-full" : ""}`}>
<input
value={draft}
onChange={(e) => setDraft(e.target.value)}
onFocus={() => setShowSearch(true)}
onBlur={() => setTimeout(() => setShowSearch(false), 200)}
placeholder="Search symbol (Ctrl+K)"
className="w-full bg-[#1a1b24] border border-[#2a2b3a] rounded-lg px-3 py-1.5 text-sm text-[#e6e7ec] placeholder:text-[#5a5b6a] focus:outline-none focus:border-[#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>
{/* Sector info */}
{sectorLabel && (
<span className="hidden md:block text-xs text-[#5a5b6a]">{sectorLabel}</span>
)}
{/* Mobile symbol display */}
<div className="sm:hidden flex items-center gap-2">
<span className="text-sm font-bold text-[#e6e7ec]">{symbol}</span>
{priceLabel && (
<span className="text-xs font-mono text-[#e6e7ec]">{priceLabel}</span>
)}
</div>
</div>
</header>
);
}