import { useState, useEffect } from 'react'; const API_BASE = import.meta.env.VITE_API_URL || ''; function fmt(n, decimals = 2, prefix = '') { if (n == null || isNaN(n)) return '—'; return `${prefix}${n.toFixed(decimals)}`; } function fmtPct(n) { if (n == null) return '—'; const val = (n * 100).toFixed(1); return `${val}%`; } function fmtLarge(n) { if (n == null) return '—'; if (Math.abs(n) >= 1e12) return `$${(n / 1e12).toFixed(2)}T`; if (Math.abs(n) >= 1e9) return `$${(n / 1e9).toFixed(2)}B`; if (Math.abs(n) >= 1e6) return `$${(n / 1e6).toFixed(2)}M`; if (Math.abs(n) >= 1e3) return `$${(n / 1e3).toFixed(1)}K`; return `$${n.toFixed(0)}`; } function MetricRow({ label, value, highlight }) { return ( {label} {value} ); } function SentimentBadge({ sentiment }) { const styles = { BULLISH: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', BEARISH: 'bg-red-500/20 text-red-400 border-red-500/30', NEUTRAL: 'bg-slate-700/50 text-slate-400 border-slate-600', }; const icons = { BULLISH: '▲', BEARISH: '▼', NEUTRAL: '●' }; return ( {icons[sentiment]} {sentiment} ); } function DirectionArrow({ dir }) { if (dir === 'INCREASED') return ; if (dir === 'DECREASED') return ; return ; } export default function StockDetailPanel({ symbol, onClose }) { const [activeTab, setActiveTab] = useState('overview'); const [stockData, setStockData] = useState(null); const [news, setNews] = useState([]); const [holders, setHolders] = useState(null); const [loading, setLoading] = useState(true); const [newsLoading, setNewsLoading] = useState(false); const [holdersLoading, setHoldersLoading] = useState(false); useEffect(() => { if (!symbol) return; setLoading(true); setStockData(null); setNews([]); setHolders(null); fetch(`${API_BASE}/api/market/stock/${symbol}`) .then(r => r.json()) .then(d => { if (d.success) setStockData(d.data); }) .catch(console.error) .finally(() => setLoading(false)); }, [symbol]); useEffect(() => { if (!symbol || activeTab !== 'news') return; if (news.length > 0) return; setNewsLoading(true); fetch(`${API_BASE}/api/market/stock/${symbol}/news`) .then(r => r.json()) .then(d => { if (d.success) setNews(d.data || []); }) .catch(console.error) .finally(() => setNewsLoading(false)); }, [symbol, activeTab]); useEffect(() => { if (!symbol || activeTab !== 'holders') return; if (holders) return; setHoldersLoading(true); fetch(`${API_BASE}/api/market/stock/${symbol}/holders`) .then(r => r.json()) .then(d => { if (d.success) setHolders(d.data); }) .catch(console.error) .finally(() => setHoldersLoading(false)); }, [symbol, activeTab]); if (!symbol) return null; const q = stockData?.quote; const f = stockData?.fundamentals; const t = stockData?.technicals; const meta = stockData?.meta; const TABS = [ { id: 'overview', label: '📋 Overview' }, { id: 'fundamentals', label: '📊 Fundamentals' }, { id: 'technicals', label: '📈 Technicals' }, { id: 'holders', label: '🏛️ Institutional' }, { id: 'news', label: '📰 News' }, ]; return (
{/* Header */}

{symbol}

{stockData?.capTier && ( {stockData.capTier} CAP )}
{meta?.name} · {meta?.sector}
{q && (
${q.price?.toFixed(2) ?? '—'}
= 0 ? 'text-emerald-400' : 'text-red-400' }`}> {q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) : '—'} {' '}({q.change_pct != null ? (q.change_pct >= 0 ? '+' : '') + q.change_pct.toFixed(2) + '%' : '—'})
)}
{/* Tabs */}
{TABS.map(tab => ( ))}
{/* Content */}
{loading ? (
{Array.from({ length: 8 }).map((_, i) => (
))}
) : ( <> {activeTab === 'overview' && } {activeTab === 'fundamentals' && } {activeTab === 'technicals' && } {activeTab === 'holders' && } {activeTab === 'news' && } )}
); } function OverviewTab({ q, f, t }) { return (
Price Snapshot
Key Metrics
0.15 ? 'negative' : undefined} />
); } function FundamentalsTab({ f }) { if (!f) return
No fundamental data available
; const sections = [ { title: '💰 Valuation', metrics: [ { label: 'P/E Ratio (TTM)', value: fmt(f.pe_ttm, 1) }, { label: 'P/E Ratio (Fwd)', value: fmt(f.pe_forward, 1) }, { label: 'Price / Book', value: fmt(f.price_to_book, 2) }, { label: 'Price / Sales', value: fmt(f.price_to_sales, 2) }, { label: 'EV / EBITDA', value: fmt(f.ev_ebitda, 1) }, { label: 'Market Cap', value: fmtLarge(f.market_cap) }, ] }, { title: '📈 Growth', metrics: [ { label: 'EPS (TTM)', value: f.eps_ttm != null ? `$${f.eps_ttm.toFixed(2)}` : '—' }, { label: 'EPS (Forward)', value: f.eps_forward != null ? `$${f.eps_forward.toFixed(2)}` : '—' }, { label: 'Earnings Growth', value: fmtPct(f.earnings_growth), h: f.earnings_growth > 0 ? 'positive' : f.earnings_growth < 0 ? 'negative' : undefined }, { label: 'Revenue Growth', value: fmtPct(f.revenue_growth), h: f.revenue_growth > 0 ? 'positive' : f.revenue_growth < 0 ? 'negative' : undefined }, { label: 'Total Revenue', value: fmtLarge(f.total_revenue) }, { label: 'Free Cash Flow', value: fmtLarge(f.free_cash_flow), h: f.free_cash_flow > 0 ? 'positive' : f.free_cash_flow < 0 ? 'negative' : undefined }, ] }, { title: '📊 Margins', metrics: [ { label: 'Gross Margin', value: fmtPct(f.gross_margins) }, { label: 'EBITDA Margin', value: fmtPct(f.ebitda_margins) }, { label: 'Operating Margin', value: fmtPct(f.operating_margins) }, { label: 'Profit Margin', value: fmtPct(f.profit_margins) }, { label: 'Return on Equity', value: fmtPct(f.return_on_equity), h: f.return_on_equity > 0.15 ? 'positive' : f.return_on_equity < 0 ? 'negative' : undefined }, { label: 'Return on Assets', value: fmtPct(f.return_on_assets) }, ] }, { title: '🏦 Balance Sheet', metrics: [ { label: 'Debt / Equity', value: fmt(f.debt_to_equity, 2), h: f.debt_to_equity > 2 ? 'negative' : f.debt_to_equity < 0.5 ? 'positive' : undefined }, { label: 'Current Ratio', value: fmt(f.current_ratio, 2), h: f.current_ratio > 2 ? 'positive' : f.current_ratio < 1 ? 'negative' : undefined }, { label: 'Quick Ratio', value: fmt(f.quick_ratio, 2) }, { label: 'Total Debt', value: fmtLarge(f.total_debt) }, { label: 'Book Value / Share', value: f.book_value != null ? `$${f.book_value.toFixed(2)}` : '—' }, { label: 'Float Shares', value: f.float_shares != null ? (f.float_shares / 1e6).toFixed(1) + 'M' : '—' }, ] }, { title: '🎯 Analyst Consensus', metrics: [ { label: 'Recommendation', value: f.recommendation?.toUpperCase() ?? '—', h: f.recommendation?.includes('buy') ? 'positive' : f.recommendation?.includes('sell') ? 'negative' : undefined }, { label: 'Target (Mean)', value: f.target_mean != null ? `$${f.target_mean.toFixed(2)}` : '—' }, { label: 'Target (High)', value: f.target_high != null ? `$${f.target_high.toFixed(2)}` : '—' }, { label: 'Target (Low)', value: f.target_low != null ? `$${f.target_low.toFixed(2)}` : '—' }, { label: '# Analysts', value: f.analyst_count ?? '—' }, { label: 'Strong Buy / Buy', value: `${f.rec_strong_buy ?? 0} / ${f.rec_buy ?? 0}`, h: 'positive' }, { label: 'Hold', value: `${f.rec_hold ?? 0}` }, { label: 'Sell / Strong Sell', value: `${f.rec_sell ?? 0} / ${f.rec_strong_sell ?? 0}`, h: (f.rec_sell + f.rec_strong_sell) > 0 ? 'negative' : undefined }, ] }, { title: '📉 Short Interest', metrics: [ { label: 'Short % of Float', value: fmtPct(f.short_pct_float), h: f.short_pct_float > 0.20 ? 'negative' : f.short_pct_float < 0.05 ? 'positive' : undefined }, { label: 'Shares Short', value: f.shares_short != null ? (f.shares_short / 1e6).toFixed(1) + 'M' : '—' }, { label: 'Inst. Ownership', value: fmtPct(f.held_pct_institutions) }, { label: 'Insider Ownership', value: fmtPct(f.held_pct_insiders) }, { label: 'Dividend Yield', value: fmtPct(f.dividend_yield), h: f.dividend_yield > 0 ? 'positive' : undefined }, { label: 'Payout Ratio', value: fmtPct(f.payout_ratio) }, ] }, ]; return (
{sections.map(section => (
{section.title}
{section.metrics.map(m => ( ))}
))}
); } function TechnicalsTab({ t, q }) { if (!t) return
Insufficient price history for technical calculations
; function rsiColor(v) { if (v == null) return 'text-white'; if (v >= 70) return 'text-red-400'; if (v <= 30) return 'text-emerald-400'; return 'text-white'; } function rsiLabel(v) { if (v == null) return ''; if (v >= 70) return ' (Overbought)'; if (v <= 30) return ' (Oversold)'; return ' (Neutral)'; } return (
Momentum Indicators
RSI (14) {t.rsi_14 != null ? `${t.rsi_14.toFixed(1)}${rsiLabel(t.rsi_14)}` : '—'}
MACD Line = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {t.macd_line != null ? t.macd_line.toFixed(4) : '—'}
MACD Signal {t.macd_signal != null ? t.macd_signal.toFixed(4) : '—'}
MACD Histogram = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {t.macd_histogram != null ? t.macd_histogram.toFixed(4) : '—'}
Moving Averages
SMA 50 {t.sma_50 != null ? `$${t.sma_50.toFixed(2)}` : '—'} {t.above_sma50 != null && ( {t.above_sma50 ? '▲ Above' : '▼ Below'} )}
SMA 200 {t.sma_200 != null ? `$${t.sma_200.toFixed(2)}` : '—'} {t.above_sma200 != null && ( {t.above_sma200 ? '▲ Above' : '▼ Below'} )}
% from SMA 50 = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {t.pct_from_sma50 != null ? `${t.pct_from_sma50.toFixed(2)}%` : '—'}
% from SMA 200 = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {t.pct_from_sma200 != null ? `${t.pct_from_sma200.toFixed(2)}%` : '—'}
Signal Summary
{t.rsi_14 != null && ( = 70 ? 'bg-red-500/20 text-red-400 border-red-500/30' : t.rsi_14 <= 30 ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' : 'bg-slate-700 text-slate-400 border-slate-600' }`}> RSI {t.rsi_14 >= 70 ? '🔴 OS' : t.rsi_14 <= 30 ? '🟢 OB' : '⚪ Neutral'} )} {t.macd_histogram != null && ( 0 ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' : 'bg-red-500/20 text-red-400 border-red-500/30' }`}> MACD {t.macd_histogram > 0 ? '🟢 Bull' : '🔴 Bear'} )} {t.above_sma50 != null && ( {t.above_sma50 ? '🟢 Above SMA50' : '🔴 Below SMA50'} )} {t.above_sma200 != null && ( {t.above_sma200 ? '🟢 Above SMA200' : '🔴 Below SMA200'} )}
); } function HoldersTab({ data, loading }) { if (loading) return
{Array.from({ length: 5 }).map((_, i) =>
)}
; if (!data) return
Loading holder data...
; const inst = data.institutional; const ins = data.insiders; function fmtLargeLocal(n) { if (n == null) return '—'; if (Math.abs(n) >= 1e9) return `$${(n / 1e9).toFixed(2)}B`; if (Math.abs(n) >= 1e6) return `$${(n / 1e6).toFixed(2)}M`; if (Math.abs(n) >= 1e3) return `$${(n / 1e3).toFixed(1)}K`; return `$${n.toFixed(0)}`; } return (
{/* Institutional summary */} {inst?.summary && (
{[ { label: 'Inst. Ownership', value: inst.summary.pct_held_institutions != null ? `${(inst.summary.pct_held_institutions * 100).toFixed(1)}%` : '—' }, { label: 'Float Inst. %', value: inst.summary.pct_float_institutions != null ? `${(inst.summary.pct_float_institutions * 100).toFixed(1)}%` : '—' }, { label: 'Insider %', value: inst.summary.pct_held_insiders != null ? `${(inst.summary.pct_held_insiders * 100).toFixed(1)}%` : '—' }, { label: 'Inst. Count', value: inst.summary.institution_count?.toLocaleString() ?? '—' }, ].map(m => (
{m.label}
{m.value}
))}
)} {/* Top institutional holders */} {inst?.holders && inst.holders.length > 0 && (
🏛️ Top Institutional Holders
{inst.holders.slice(0, 10).map((h, i) => ( ))}
Institution % Held Shares Value QoQ Change Reported
{h.name} {h.pct_held != null ? `${(h.pct_held * 100).toFixed(2)}%` : '—'} {h.shares != null ? h.shares.toLocaleString() : '—'} {h.value != null ? fmtLargeLocal(h.value) : '—'} {h.shares_change != null && ( 0 ? 'text-emerald-400' : h.shares_change < 0 ? 'text-red-400' : 'text-slate-500'}`}> {h.shares_change > 0 ? '+' : ''}{h.shares_change?.toLocaleString()} )} {h.date_reported || '—'}
)} {/* Insider activity */} {ins && (
👤 Insider Activity (90d)
{ins.insider_summary && ( {ins.insider_summary.net_sentiment} · {ins.insider_summary.buys_90d}B / {ins.insider_summary.sells_90d}S )}
{ins.transactions && ins.transactions.length > 0 ? (
{ins.transactions.slice(0, 15).map((tx, i) => ( ))}
Insider Title Type Shares Value Date
{tx.insider_name} {tx.title} {tx.direction} {tx.shares != null ? tx.shares.toLocaleString() : '—'} {tx.value != null ? fmtLargeLocal(tx.value) : '—'} {tx.start_date || '—'}
) : (
No recent insider transactions
)}
)}
); } function NewsTab({ items, loading }) { if (loading) return
{Array.from({ length: 6 }).map((_, i) =>
)}
; if (!items || items.length === 0) return
No financial news found for this symbol
; return (
{items.map((item, i) => (
{item.title}
{item.snippet && (
{item.snippet}
)}
{item.source} {item.publishedAt && ( <> · {new Date(item.publishedAt).toLocaleDateString()} )}
))}
); }