institutional-trader/frontend/src/components/dashboard/StockDetailPanel.jsx

643 lines
30 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
<tr className="border-b border-slate-800/50 hover:bg-slate-800/20 transition-colors">
<td className="py-2 px-3 text-slate-400 text-sm">{label}</td>
<td className={`py-2 px-3 text-sm font-medium text-right ${
highlight === 'positive' ? 'text-emerald-400' :
highlight === 'negative' ? 'text-red-400' :
'text-white'
}`}>{value}</td>
</tr>
);
}
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 (
<span className={`text-xs px-2 py-0.5 rounded border font-medium ${styles[sentiment] || styles.NEUTRAL}`}>
{icons[sentiment]} {sentiment}
</span>
);
}
function DirectionArrow({ dir }) {
if (dir === 'INCREASED') return <span className="text-emerald-400 font-bold"></span>;
if (dir === 'DECREASED') return <span className="text-red-400 font-bold"></span>;
return <span className="text-slate-500"></span>;
}
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 (
<div className="bg-slate-900 border border-slate-700/50 rounded-xl overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-700/50 bg-slate-950/40">
<div className="flex items-center gap-4">
<div>
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold text-white">{symbol}</h2>
{stockData?.capTier && (
<span className={`text-xs px-2 py-0.5 rounded border font-medium ${
stockData.capTier === 'LARGE' ? 'bg-blue-500/20 text-blue-400 border-blue-500/30' :
stockData.capTier === 'MID' ? 'bg-purple-500/20 text-purple-400 border-purple-500/30' :
stockData.capTier === 'ETF' ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' :
'bg-orange-500/20 text-orange-400 border-orange-500/30'
}`}>{stockData.capTier} CAP</span>
)}
</div>
<div className="text-slate-400 text-sm">{meta?.name} · {meta?.sector}</div>
</div>
{q && (
<div className="ml-4">
<div className="text-2xl font-bold text-white tabular-nums">
${q.price?.toFixed(2) ?? '—'}
</div>
<div className={`text-sm font-medium ${
(q.change_pct ?? 0) >= 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) + '%' : '—'})
</div>
</div>
)}
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-white transition-colors text-2xl font-light w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-800"
>
×
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-slate-700/50 bg-slate-950/20">
{TABS.map(tab => (
<button
key={tab.id}
onClick={() => 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}
</button>
))}
</div>
{/* Content */}
<div className="p-5 overflow-y-auto max-h-[calc(100vh-300px)]">
{loading ? (
<div className="space-y-3 animate-pulse">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="h-10 bg-slate-800 rounded" />
))}
</div>
) : (
<>
{activeTab === 'overview' && <OverviewTab q={q} f={f} t={t} />}
{activeTab === 'fundamentals' && <FundamentalsTab f={f} />}
{activeTab === 'technicals' && <TechnicalsTab t={t} q={q} />}
{activeTab === 'holders' && <HoldersTab data={holders} loading={holdersLoading} />}
{activeTab === 'news' && <NewsTab items={news} loading={newsLoading} />}
</>
)}
</div>
</div>
);
}
function OverviewTab({ q, f, t }) {
return (
<div className="grid grid-cols-2 gap-6">
<div>
<div className="text-xs uppercase tracking-widest text-slate-500 mb-3 font-semibold">Price Snapshot</div>
<table className="w-full">
<tbody>
<MetricRow label="Open" value={q?.open != null ? `$${q.open.toFixed(2)}` : '—'} />
<MetricRow label="Day High" value={q?.high != null ? `$${q.high.toFixed(2)}` : '—'} />
<MetricRow label="Day Low" value={q?.low != null ? `$${q.low.toFixed(2)}` : '—'} />
<MetricRow label="Prev Close" value={q?.prev_close != null ? `$${q.prev_close.toFixed(2)}` : '—'} />
<MetricRow label="52W High" value={f?.week52_high != null ? `$${f.week52_high.toFixed(2)}` : '—'} />
<MetricRow label="52W Low" value={f?.week52_low != null ? `$${f.week52_low.toFixed(2)}` : '—'} />
<MetricRow label="Volume" value={q?.volume != null ? q.volume.toLocaleString() : '—'} />
<MetricRow label="Avg Vol (3M)" value={f?.avg_volume_3m != null ? f.avg_volume_3m.toLocaleString() : '—'} />
</tbody>
</table>
</div>
<div>
<div className="text-xs uppercase tracking-widest text-slate-500 mb-3 font-semibold">Key Metrics</div>
<table className="w-full">
<tbody>
<MetricRow label="P/E (TTM)" value={fmt(f?.pe_ttm)} />
<MetricRow label="P/E (Fwd)" value={fmt(f?.pe_forward)} />
<MetricRow label="EPS (TTM)" value={f?.eps_ttm != null ? `$${f.eps_ttm.toFixed(2)}` : '—'} />
<MetricRow label="Beta" value={fmt(f?.beta)} />
<MetricRow label="Short % Float" value={fmtPct(f?.short_pct_float)} highlight={f?.short_pct_float > 0.15 ? 'negative' : undefined} />
<MetricRow label="Inst. Ownership" value={fmtPct(f?.held_pct_institutions)} />
<MetricRow label="Analyst Target" value={f?.target_mean != null ? `$${f.target_mean.toFixed(2)}` : '—'} />
<MetricRow label="Recommendation" value={f?.recommendation?.toUpperCase() ?? '—'}
highlight={f?.recommendation === 'buy' || f?.recommendation === 'strongBuy' ? 'positive' :
f?.recommendation === 'sell' || f?.recommendation === 'strongSell' ? 'negative' : undefined}
/>
</tbody>
</table>
</div>
</div>
);
}
function FundamentalsTab({ f }) {
if (!f) return <div className="text-slate-500 text-center py-8">No fundamental data available</div>;
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 (
<div className="grid grid-cols-2 gap-x-6 gap-y-6">
{sections.map(section => (
<div key={section.title}>
<div className="text-xs uppercase tracking-widest text-slate-500 mb-3 font-semibold">{section.title}</div>
<table className="w-full">
<tbody>
{section.metrics.map(m => (
<MetricRow key={m.label} label={m.label} value={m.value} highlight={m.h} />
))}
</tbody>
</table>
</div>
))}
</div>
);
}
function TechnicalsTab({ t, q }) {
if (!t) return <div className="text-slate-500 text-center py-8">Insufficient price history for technical calculations</div>;
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 (
<div className="grid grid-cols-2 gap-6">
<div>
<div className="text-xs uppercase tracking-widest text-slate-500 mb-3 font-semibold">Momentum Indicators</div>
<table className="w-full">
<tbody>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-slate-400 text-sm">RSI (14)</td>
<td className={`py-2 px-3 text-sm font-bold text-right ${rsiColor(t.rsi_14)}`}>
{t.rsi_14 != null ? `${t.rsi_14.toFixed(1)}${rsiLabel(t.rsi_14)}` : '—'}
</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-slate-400 text-sm">MACD Line</td>
<td className={`py-2 px-3 text-sm font-medium text-right ${t.macd_line >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{t.macd_line != null ? t.macd_line.toFixed(4) : '—'}
</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-slate-400 text-sm">MACD Signal</td>
<td className="py-2 px-3 text-sm font-medium text-right text-slate-300">
{t.macd_signal != null ? t.macd_signal.toFixed(4) : '—'}
</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-slate-400 text-sm">MACD Histogram</td>
<td className={`py-2 px-3 text-sm font-medium text-right ${t.macd_histogram >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{t.macd_histogram != null ? t.macd_histogram.toFixed(4) : '—'}
</td>
</tr>
</tbody>
</table>
</div>
<div>
<div className="text-xs uppercase tracking-widest text-slate-500 mb-3 font-semibold">Moving Averages</div>
<table className="w-full">
<tbody>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-slate-400 text-sm">SMA 50</td>
<td className="py-2 px-3 text-sm text-right">
<span className="text-white">{t.sma_50 != null ? `$${t.sma_50.toFixed(2)}` : '—'}</span>
{t.above_sma50 != null && (
<span className={`ml-2 text-xs ${t.above_sma50 ? 'text-emerald-400' : 'text-red-400'}`}>
{t.above_sma50 ? '▲ Above' : '▼ Below'}
</span>
)}
</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-slate-400 text-sm">SMA 200</td>
<td className="py-2 px-3 text-sm text-right">
<span className="text-white">{t.sma_200 != null ? `$${t.sma_200.toFixed(2)}` : '—'}</span>
{t.above_sma200 != null && (
<span className={`ml-2 text-xs ${t.above_sma200 ? 'text-emerald-400' : 'text-red-400'}`}>
{t.above_sma200 ? '▲ Above' : '▼ Below'}
</span>
)}
</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-slate-400 text-sm">% from SMA 50</td>
<td className={`py-2 px-3 text-sm font-medium text-right ${t.pct_from_sma50 >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{t.pct_from_sma50 != null ? `${t.pct_from_sma50.toFixed(2)}%` : '—'}
</td>
</tr>
<tr className="border-b border-slate-800/50">
<td className="py-2 px-3 text-slate-400 text-sm">% from SMA 200</td>
<td className={`py-2 px-3 text-sm font-medium text-right ${t.pct_from_sma200 >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{t.pct_from_sma200 != null ? `${t.pct_from_sma200.toFixed(2)}%` : '—'}
</td>
</tr>
</tbody>
</table>
<div className="mt-4 p-3 rounded-lg bg-slate-800/50 border border-slate-700/30">
<div className="text-xs text-slate-500 mb-1 font-semibold uppercase tracking-wide">Signal Summary</div>
<div className="flex flex-wrap gap-2 mt-1">
{t.rsi_14 != null && (
<span className={`text-xs px-2 py-1 rounded border ${
t.rsi_14 >= 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'}
</span>
)}
{t.macd_histogram != null && (
<span className={`text-xs px-2 py-1 rounded border ${
t.macd_histogram > 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'}
</span>
)}
{t.above_sma50 != null && (
<span className={`text-xs px-2 py-1 rounded border ${
t.above_sma50 ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' :
'bg-red-500/20 text-red-400 border-red-500/30'
}`}>
{t.above_sma50 ? '🟢 Above SMA50' : '🔴 Below SMA50'}
</span>
)}
{t.above_sma200 != null && (
<span className={`text-xs px-2 py-1 rounded border ${
t.above_sma200 ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' :
'bg-red-500/20 text-red-400 border-red-500/30'
}`}>
{t.above_sma200 ? '🟢 Above SMA200' : '🔴 Below SMA200'}
</span>
)}
</div>
</div>
</div>
</div>
);
}
function HoldersTab({ data, loading }) {
if (loading) return <div className="space-y-3 animate-pulse">{Array.from({ length: 5 }).map((_, i) => <div key={i} className="h-10 bg-slate-800 rounded" />)}</div>;
if (!data) return <div className="text-slate-500 text-center py-8">Loading holder data...</div>;
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 (
<div className="space-y-6">
{/* Institutional summary */}
{inst?.summary && (
<div className="grid grid-cols-4 gap-3">
{[
{ 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 => (
<div key={m.label} className="bg-slate-800/50 rounded-lg p-3 border border-slate-700/30">
<div className="text-xs text-slate-500">{m.label}</div>
<div className="text-lg font-bold text-white mt-1">{m.value}</div>
</div>
))}
</div>
)}
{/* Top institutional holders */}
{inst?.holders && inst.holders.length > 0 && (
<div>
<div className="text-xs uppercase tracking-widest text-slate-500 mb-3 font-semibold">🏛 Top Institutional Holders</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-slate-500 text-xs bg-slate-950/30">
<th className="text-left px-3 py-2">Institution</th>
<th className="text-right px-3 py-2">% Held</th>
<th className="text-right px-3 py-2">Shares</th>
<th className="text-right px-3 py-2">Value</th>
<th className="text-center px-3 py-2">QoQ Change</th>
<th className="text-right px-3 py-2">Reported</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{inst.holders.slice(0, 10).map((h, i) => (
<tr key={i} className="hover:bg-slate-800/20">
<td className="px-3 py-2.5 text-white font-medium">{h.name}</td>
<td className="px-3 py-2.5 text-right text-slate-300 tabular-nums">
{h.pct_held != null ? `${(h.pct_held * 100).toFixed(2)}%` : '—'}
</td>
<td className="px-3 py-2.5 text-right text-slate-400 tabular-nums">
{h.shares != null ? h.shares.toLocaleString() : '—'}
</td>
<td className="px-3 py-2.5 text-right text-slate-400 tabular-nums">
{h.value != null ? fmtLargeLocal(h.value) : '—'}
</td>
<td className="px-3 py-2.5 text-center">
<DirectionArrow dir={h.direction} />
{h.shares_change != null && (
<span className={`ml-1 text-xs ${h.shares_change > 0 ? 'text-emerald-400' : h.shares_change < 0 ? 'text-red-400' : 'text-slate-500'}`}>
{h.shares_change > 0 ? '+' : ''}{h.shares_change?.toLocaleString()}
</span>
)}
</td>
<td className="px-3 py-2.5 text-right text-slate-500 text-xs">{h.date_reported || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Insider activity */}
{ins && (
<div>
<div className="flex items-center gap-3 mb-3">
<div className="text-xs uppercase tracking-widest text-slate-500 font-semibold">👤 Insider Activity (90d)</div>
{ins.insider_summary && (
<span className={`text-xs px-2 py-0.5 rounded border font-medium ${
ins.insider_summary.net_sentiment === 'BULLISH' ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' :
ins.insider_summary.net_sentiment === 'BEARISH' ? 'bg-red-500/20 text-red-400 border-red-500/30' :
'bg-slate-700 text-slate-400 border-slate-600'
}`}>
{ins.insider_summary.net_sentiment} · {ins.insider_summary.buys_90d}B / {ins.insider_summary.sells_90d}S
</span>
)}
</div>
{ins.transactions && ins.transactions.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-slate-500 text-xs bg-slate-950/30">
<th className="text-left px-3 py-2">Insider</th>
<th className="text-left px-3 py-2">Title</th>
<th className="text-center px-3 py-2">Type</th>
<th className="text-right px-3 py-2">Shares</th>
<th className="text-right px-3 py-2">Value</th>
<th className="text-right px-3 py-2">Date</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{ins.transactions.slice(0, 15).map((tx, i) => (
<tr key={i} className="hover:bg-slate-800/20">
<td className="px-3 py-2 text-white font-medium">{tx.insider_name}</td>
<td className="px-3 py-2 text-slate-400 text-xs">{tx.title}</td>
<td className="px-3 py-2 text-center">
<span className={`text-xs px-2 py-0.5 rounded font-medium ${
tx.direction === 'BUY' ? 'bg-emerald-500/20 text-emerald-400' :
tx.direction === 'SELL' ? 'bg-red-500/20 text-red-400' :
'bg-slate-700 text-slate-400'
}`}>{tx.direction}</span>
</td>
<td className="px-3 py-2 text-right text-slate-300 tabular-nums">
{tx.shares != null ? tx.shares.toLocaleString() : '—'}
</td>
<td className="px-3 py-2 text-right text-slate-400 tabular-nums">
{tx.value != null ? fmtLargeLocal(tx.value) : '—'}
</td>
<td className="px-3 py-2 text-right text-slate-500 text-xs">{tx.start_date || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-slate-500 text-sm py-4 text-center">No recent insider transactions</div>
)}
</div>
)}
</div>
);
}
function NewsTab({ items, loading }) {
if (loading) return <div className="space-y-3 animate-pulse">{Array.from({ length: 6 }).map((_, i) => <div key={i} className="h-16 bg-slate-800 rounded" />)}</div>;
if (!items || items.length === 0) return <div className="text-slate-500 text-center py-8">No financial news found for this symbol</div>;
return (
<div className="space-y-3">
{items.map((item, i) => (
<a
key={i}
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="block p-3 rounded-lg bg-slate-800/40 border border-slate-700/30 hover:bg-slate-800/70 hover:border-slate-600/50 transition-all group"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="text-white text-sm font-medium group-hover:text-blue-400 transition-colors line-clamp-2">
{item.title}
</div>
{item.snippet && (
<div className="text-slate-500 text-xs mt-1 line-clamp-1">{item.snippet}</div>
)}
<div className="flex items-center gap-2 mt-1.5">
<span className="text-xs text-slate-600">{item.source}</span>
{item.publishedAt && (
<>
<span className="text-xs text-slate-700">·</span>
<span className="text-xs text-slate-600">
{new Date(item.publishedAt).toLocaleDateString()}
</span>
</>
)}
</div>
</div>
<SentimentBadge sentiment={item.sentiment} />
</div>
</a>
))}
</div>
);
}