/* FinPulse — Stock detail (demo + live via MarketService). */
const TIME_TABS = ['1D', '5D', '1M', '3M', '6M', '1Y', '5Y'];
function AnalystBar({ rec }) {
if (!rec || !rec.total) return null;
const segments = [
{ k: 'strongBuy', label: 'Strong Buy', c: 'var(--up)' },
{ k: 'buy', label: 'Buy', c: '#22c55e' },
{ k: 'hold', label: 'Hold', c: 'var(--cat-economy)' },
{ k: 'sell', label: 'Sell', c: '#f97316' },
{ k: 'strongSell', label: 'Strong Sell', c: 'var(--down)' },
];
return (
Analyst Consensus
{rec.total} analysts · {rec.period || 'latest'}
{segments.map((s) => rec[s.k] > 0 && (
))}
{segments.filter((s) => rec[s.k] > 0).map((s) => (
{s.label} {rec[s.k]}
))}
);
}
function StockScreen({ stock, onOpenStock, onBack, onOpenArticle }) {
const L = useLive();
const [s, setS] = useState(stock);
const [range, setRange] = useState('1D');
const [refreshing, setRefreshing] = useState(false);
const [liveNews, setLiveNews] = useState(null);
const [rec, setRec] = useState(null);
const [peers, setPeers] = useState([]);
const [insights, setInsights] = useState(null);
useEffect(() => {
setS(stock); setRange(stock && stock.live ? 'LIVE' : '1D');
setLiveNews(null); setRec(null); setPeers([]); setInsights(null);
}, [stock]);
useEffect(() => {
if (!s || !s.live) return;
const isUS = s.market !== 'IN' && !/\.NS$/i.test(s.symbol);
if (isUS && L.hasKey) {
L.market.getCompanyNews(s.symbol).then((items) => {
if (window.NewsCache) window.NewsCache.putMany(items);
setLiveNews(items);
}).catch(() => setLiveNews([]));
L.market.getRecommendations(s.symbol).then(setRec).catch(() => setRec(null));
L.market.getPeers(s.symbol).then(setPeers).catch(() => setPeers([]));
L.market.getStockInsights(s.symbol).then(setInsights).catch(() => setInsights(null));
} else if (isUS) {
setLiveNews([]); setRec(null); setPeers([]); setInsights(null);
} else if (L.hasKey) {
const sym = s.symbol || (s.ticker + '.NS');
L.market.getCompanyNews(sym).then((items) => {
if (window.NewsCache) window.NewsCache.putMany(items);
setLiveNews(items);
}).catch(() => setLiveNews([]));
}
}, [s && s.id, L.hasKey]);
if (!s) return null;
if (!s.live) return ;
const live = s.live;
const C = curSym(s.cur);
const up = s.chg >= 0;
const data = live ? s.history.LIVE : s.history[range];
async function refresh() {
if (!live) return;
setRefreshing(true);
try {
setS(await L.market.refreshStock(s));
L.refreshRateLimit();
} catch (e) {} finally { setRefreshing(false); }
}
const dash = (v) => (v == null || v === 0 || Number.isNaN(v) ? '—' : v);
const metrics = [
{ label: 'Market Cap', value: s.mcap != null ? mcapM(s.mcap, s.cur) : '—', sub: s.sector || s.exchange || '—', color: s.sectorColor },
{ label: 'P/E Ratio', value: s.pe != null ? Number(s.pe).toFixed(1) : '—', sub: 'TTM', color: 'var(--accent)' },
{ label: '52W High', value: s.hi52 != null ? C + fmt.price(s.hi52) : '—', sub: s.hi52 ? ((1 - s.price / s.hi52) * 100).toFixed(1) + '% below' : '', color: 'var(--up)' },
{ label: '52W Low', value: s.lo52 != null ? C + fmt.price(s.lo52) : '—', sub: s.lo52 ? ((s.price / s.lo52 - 1) * 100).toFixed(1) + '% above' : '', color: 'var(--down)' },
];
const stats = [
['Open', s.open != null ? C + fmt.price(s.open) : '—'], ['Prev Close', s.prevClose != null ? C + fmt.price(s.prevClose) : '—'],
['Day High', s.dayHigh != null ? C + fmt.price(s.dayHigh) : '—'], ['Day Low', s.dayLow != null ? C + fmt.price(s.dayLow) : '—'],
['Volume', s.vol != null ? fmt.vol(s.vol) + (s.market === 'IN' ? ' sh' : '') : '—'],
['52W Range', s.lo52 != null && s.hi52 != null ? C + fmt.price(s.lo52) + ' – ' + C + fmt.price(s.hi52) : '—'],
['Dividend Yld', s.div != null ? Number(s.div).toFixed(2) + '%' : '—'], ['Beta', s.beta != null ? Number(s.beta).toFixed(2) : '—'],
['Exchange', s.exchange || '—'], ['Currency', s.cur || '—'],
];
const related = liveNews || [];
const similar = peers;
return (
Back
{live && s.logo ?
{ e.target.style.display = 'none'; }} /> :
}
{s.name}
{s.sector && {s.sector} }
{live ? `${s.exchange || 'Listed'} · ${s.ticker}` : `NSE: ${s.ticker} · BSE: ${500000 + s.id * 13}`}
{C}{fmt.price(s.price)}
{(s.change >= 0 ? '+' : '') + fmt.price(s.change)} ({fmt.pct(s.chg)})
{live && {refreshing ? '…' : 'Refresh'} }
{live ? TODAY
: TIME_TABS.map((t) => (
setRange(t)}>{t}
))}
{live &&
Indicative line from live OHLC. Intraday candle history requires a premium Finnhub plan.
}
{metrics.map((m, i) => )}
{live && rec &&
}
{live && insights && insights.priceTarget && (
Analyst Price Target
{insights.priceTarget.analysts || '—'} analysts
)}
{live && insights && insights.earnings && insights.earnings.length > 0 && (
Earnings Surprises
Period Actual Estimate Surprise
{insights.earnings.map((e) => (
{e.period}
{e.actual != null ? e.actual : '—'}
{e.estimate != null ? e.estimate : '—'}
= 0 ? 'var(--up)' : 'var(--down)' }}>
{e.surprisePct != null ? fmt.pct(e.surprisePct) : '—'}
))}
)}
{live && insights && insights.upgrades && insights.upgrades.length > 0 && (
Upgrades & Downgrades
{insights.upgrades.map((u) => (
{u.company} · {u.fromGrade || '—'} → {u.toGrade || u.action}
{u.date}
))}
)}
{!live && (
Volume · 22 sessions in lakh shares
)}
Key Statistics
{stats.map((kv, i) => (
{kv[0]} {dash(kv[1])}
))}
About
{live
? `${s.name} (${s.ticker}) is listed on ${s.exchange || 'a major exchange'} in the ${s.sector} sector. Live data via Finnhub.`
: `${s.name} is a leading ${s.sector.toLowerCase()} company listed on the NSE and BSE, part of India's benchmark indices and tracked closely by institutional and retail investors.`}
{insights && insights.sentiment && (
News sentiment: {insights.sentiment.sentiment === 'bull' ? 'Bullish' : 'Bearish'}
{insights.sentiment.bullish != null ? ` (${insights.sentiment.bullish}% bull / ${insights.sentiment.bearish}% bear)` : ''}
)}
+ Watchlist
{live && s.web
?
Website
:
Set Alert }
{live ? 'Company News' : 'Related News'}
{related.length ?
:
No recent news.
}
{insights && insights.press && insights.press.length > 0 && (
<>
Press Releases
>
)}
{insights && insights.dividends && insights.dividends.length > 0 && (
<>
Recent Dividends
{insights.dividends.map((d) => (
{d.date}
{d.currency || '$'}{d.amount}
))}
>
)}
{similar.length > 0 && (
Peers in {s.sector}
{similar.map((x) => (
onOpenStock(x)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 0', cursor: 'pointer', borderBottom: '1px solid var(--border)' }}>
{x.name}
))}
)}
);
}
Object.assign(window, { StockScreen });