'use client'; import React, { useState, useCallback, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { ChevronDown, ChevronUp, TrendingUp, Landmark, UserCheck, ArrowUpRight, ArrowDownRight, RefreshCw, Settings, ExternalLink, } from 'lucide-react'; import type { DashboardData, StockTicker } from '@/types/dashboard'; import type { CongressTrade, InsiderTransaction } from '@/types/unusualWhales'; import { fetchUWStatus, fetchCongressTrades, fetchInsiderTransactions } from '@/lib/uwClient'; type Tab = 'tickers' | 'congress' | 'insider'; const TAB_CONFIG: { key: Tab; label: string; icon: React.ReactNode }[] = [ { key: 'tickers', label: 'TICKERS', icon: }, { key: 'congress', label: 'CONGRESS', icon: }, { key: 'insider', label: 'INSIDER', icon: }, ]; // ── Helpers ───────────────────────────────────────────────────────────────── function chamberBadge(chamber: string) { const c = chamber?.toLowerCase(); if (c === 'senator' || c === 'senate') return 'S'; if (c === 'representative' || c === 'house') return 'H'; return c?.charAt(0)?.toUpperCase() || '?'; } function txColor(tx: string) { const t = tx?.toLowerCase() || ''; if (t.includes('purchase') || t.includes('buy')) return 'text-green-400'; if (t.includes('sale') || t.includes('sell')) return 'text-red-400'; return 'text-yellow-400'; } function insiderCodeLabel(code: string) { const map: Record = { P: 'Purchase', S: 'Sale', A: 'Grant', M: 'Exercise', F: 'Tax', G: 'Gift', C: 'Conversion', X: 'Expiration', }; return map[code?.toUpperCase()] || code || '—'; } // ── Tab: Tickers ──────────────────────────────────────────────────────────── const CRYPTO_LABELS = new Set(['BTC', 'ETH']); function TickerRow({ ticker, info }: { ticker: string; info: StockTicker }) { return ( [{ticker}] ${(info.price ?? 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {info.up ? : } {Math.abs(info.change_percent ?? 0).toFixed(2)}% ); } function TickersTab({ stocks, oil }: { stocks: Record; oil: Record }) { const defenseEntries = Object.entries(stocks).filter(([k]) => !CRYPTO_LABELS.has(k)); const cryptoEntries = Object.entries(stocks).filter(([k]) => CRYPTO_LABELS.has(k)); const hasDefense = defenseEntries.length > 0; const hasCrypto = cryptoEntries.length > 0; const hasOil = Object.keys(oil).length > 0; if (!hasDefense && !hasCrypto && !hasOil) return Waiting for market data...; return ( {hasCrypto && ( CRYPTO {cryptoEntries.map(([ticker, info]) => ( ))} )} {hasDefense && ( DEFENSE SECTOR {defenseEntries.map(([ticker, info]) => ( ))} )} {hasOil && ( COMMODITIES {Object.entries(oil).map(([name, info]) => ( {name} ${(info.price ?? 0).toFixed(2)} {info.up ? : } {Math.abs(info.change_percent ?? 0).toFixed(2)}% ))} )} ); } // ── Tab: Congress ─────────────────────────────────────────────────────────── function CongressTab({ trades }: { trades: CongressTrade[] }) { if (!trades.length) return No recent congress trades; return ( {trades.slice(0, 20).map((t, i) => ( {chamberBadge(t.chamber)} {t.politician_name} {t.ticker && ( {t.ticker} )} {t.transaction_type || '—'} {t.amount_range && ( {t.amount_range} )} {t.filing_date && ( {t.filing_date} )} {t.asset_name && t.asset_name !== t.ticker && ( {t.asset_name} )} ))} ); } // ── Tab: Insider ──────────────────────────────────────────────────────────── function InsiderTab({ transactions }: { transactions: InsiderTransaction[] }) { if (!transactions.length) return No recent insider transactions; return ( {transactions.slice(0, 20).map((t, i) => { const isBuy = t.transaction_code === 'P'; const isSell = t.transaction_code === 'S'; return ( {t.name} {t.ticker} {insiderCodeLabel(t.transaction_code || '')} {t.change !== 0 && ( 0 ? 'text-green-400' : 'text-red-400'}`}> {t.change > 0 ? '+' : ''}{t.change.toLocaleString()} shares )} {t.transaction_price > 0 && ( ${t.transaction_price.toFixed(2)} )} {t.filing_date && ( {t.filing_date} )} ); })} ); } // ── Main Panel ────────────────────────────────────────────────────────────── interface MarketsPanelProps { data: DashboardData; focused?: boolean; onFocusChange?: (focused: boolean) => void; } const MarketsPanel = React.memo(function MarketsPanel({ data, focused, onFocusChange }: MarketsPanelProps) { const [isMinimized, setIsMinimized] = useState(true); const [activeTab, setActiveTab] = useState('tickers'); const [finnhubConfigured, setFinnhubConfigured] = useState(null); const [refreshing, setRefreshing] = useState(false); // Local overlay for on-demand fetches const [localCongress, setLocalCongress] = useState(null); const [localInsider, setLocalInsider] = useState(null); // Check Finnhub status useEffect(() => { fetchUWStatus() .then((s) => setFinnhubConfigured(s.configured)) .catch(() => setFinnhubConfigured(false)); }, []); // Data sources: background-polled + local overlay const stocks = data?.stocks || {}; const oil = data?.oil || {}; const uw = data?.unusual_whales; const congressTrades = localCongress ?? uw?.congress_trades ?? []; const insiderTxns = localInsider ?? uw?.insider_transactions ?? []; const handleRefresh = useCallback(async () => { if (refreshing) return; setRefreshing(true); try { const [c, ins] = await Promise.all([ fetchCongressTrades().catch(() => null), fetchInsiderTransactions().catch(() => null), ]); if (c?.trades) setLocalCongress(c.trades); if (ins?.transactions) setLocalInsider(ins.transactions); } finally { setRefreshing(false); } }, [refreshing]); // Determine if Finnhub tabs should show const hasFinnhub = finnhubConfigured === true; return ( {/* Header */} { const next = !isMinimized; setIsMinimized(next); onFocusChange?.(!next); }} > GLOBAL MARKETS {hasFinnhub && ( FINNHUB )} {isMinimized ? : } {!isMinimized && ( {hasFinnhub ? ( <> {/* Tab bar */} {TAB_CONFIG.map((tab) => ( setActiveTab(tab.key)} className={`flex-1 flex items-center justify-center gap-1 py-2 text-[9px] tracking-wider transition-colors ${ activeTab === tab.key ? 'text-cyan-400 border-b border-cyan-400 bg-cyan-950/20' : 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]' }`} > {tab.icon} {tab.label} ))} {/* Refresh bar (congress/insider tabs only) */} {activeTab !== 'tickers' && ( { e.stopPropagation(); handleRefresh(); }} disabled={refreshing} className="flex items-center gap-1 text-[9px] text-[var(--text-muted)] hover:text-cyan-400 transition-colors disabled:opacity-40" > {refreshing ? 'FETCHING...' : 'REFRESH'} )} {/* Tab content */} {activeTab === 'tickers' && } {activeTab === 'congress' && } {activeTab === 'insider' && } {/* Attribution */} Data from Finnhub > ) : ( /* No Finnhub key — show stocks/oil only (yfinance fallback) + setup hint */ {finnhubConfigured === false && ( Add FINNHUB_API_KEY for congress trades & insider data Free API Key )} )} )} ); }); export default MarketsPanel;
Data from Finnhub
Add FINNHUB_API_KEY for congress trades & insider data