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 => (
setActiveTab(tab.id)}
className={`px-4 py-2.5 text-sm font-medium transition-all ${
activeTab === tab.id
? 'border-b-2 border-blue-500 text-blue-400 bg-blue-500/5'
: 'text-slate-500 hover:text-slate-300'
}`}
>
{tab.label}
))}
{/* Content */}
{loading ? (
{Array.from({ length: 8 }).map((_, i) => (
))}
) : (
<>
{activeTab === 'overview' &&
}
{activeTab === 'fundamentals' &&
}
{activeTab === 'technicals' &&
}
{activeTab === 'holders' &&
}
{activeTab === 'news' &&
}
>
)}
);
}
function OverviewTab({ q, f, t }) {
return (
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 => (
))}
)}
{/* Top institutional holders */}
{inst?.holders && inst.holders.length > 0 && (
🏛️ Top Institutional Holders
Institution
% Held
Shares
Value
QoQ Change
Reported
{inst.holders.slice(0, 10).map((h, i) => (
{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 ? (
Insider
Title
Type
Shares
Value
Date
{ins.transactions.slice(0, 15).map((tx, i) => (
{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 (
);
}