import { useState, useEffect, useCallback, useRef } from "react"; import { useAuth } from "../../hooks/useAuth"; import GoogleAuthButton from "../public/GoogleAuthButton"; import { googleLogin } from "../../api/client"; import type { CredentialResponse } from "@react-oauth/google"; import { getStockData } from "../../api/stockData"; import type { StockDataResponse, StockMetricRow } from "../../api/stockData"; type Period = 7 | 30 | 60; const DEFAULT_TICKER = "B2V"; const DEFAULT_PERIOD: Period = 30; // ─── Theme definitions ──────────────────────────────────── type ThemeId = "newscast" | "bloomberg" | "economist"; interface Theme { id: ThemeId; label: string; // layout backgrounds pageBg: string; cardBg: string; surfBg: string; border: string; grid: string; // accents accent: string; accentDim: string; amber: string; amberLt: string; // text text: string; textMid: string; textSub: string; // chart chartBg: string; // price moves up: string; upFill: string; down: string; downFill: string; // ticker bar tickerBg: string; tickerLabel: string; // whether text on accent is white or dark onAccent: string; // tag/badge styling for "LIVE" badge badgeBg: string; badgeText: string; } const THEMES: Record = { newscast: { id: "newscast", label: "Newscast", pageBg: "#07080f", cardBg: "#10131f", surfBg: "#0c0e18", border: "#1a1e30", grid: "#141826", accent: "#cc1a1a", accentDim: "#3d0a0a", amber: "#d97706", amberLt: "#fbbf24", text: "#f0f2f8", textMid: "#8892aa", textSub: "#5a6480", chartBg: "#07080f", up: "#22c55e", upFill: "#15803d", down: "#ef4444", downFill: "#b91c1c", tickerBg: "#0c0e18", tickerLabel: "#cc1a1a", onAccent: "#ffffff", badgeBg: "#cc1a1a", badgeText: "#ffffff", }, bloomberg: { id: "bloomberg", label: "Bloomberg", pageBg: "#020202", cardBg: "#0a0a0a", surfBg: "#060606", border: "#1e1e1e", grid: "#111111", accent: "#e8a032", accentDim: "#3d2700", amber: "#e8a032", amberLt: "#f5c061", text: "#f0ede8", textMid: "#b0a080", textSub: "#5a5040", chartBg: "#020202", up: "#00d97e", upFill: "#00a85a", down: "#ff3838", downFill: "#cc2020", tickerBg: "#060606", tickerLabel: "#e8a032", onAccent: "#000000", badgeBg: "#e8a032", badgeText: "#000000", }, economist: { id: "economist", label: "Economist", pageBg: "#faf8f4", cardBg: "#ffffff", surfBg: "#f5f3ef", border: "#d8d3c8", grid: "#ece9e2", accent: "#e3120b", accentDim: "#fde8e7", amber: "#c05c00", amberLt: "#a04800", text: "#1a1a1a", textMid: "#4a4a4a", textSub: "#888888", chartBg: "#faf8f4", up: "#007a3d", upFill: "#005c2e", down: "#c0392b", downFill: "#a93226", tickerBg: "#e3120b", tickerLabel: "#e3120b", onAccent: "#ffffff", badgeBg: "#e3120b", badgeText: "#ffffff", }, }; // ─── Dynamic trading days (relative to today) ──────────── function getRecentTradingDays(n: number): string[] { const result: string[] = []; const cursor = new Date(); cursor.setHours(0, 0, 0, 0); while (result.length < n) { const dow = cursor.getDay(); if (dow !== 0 && dow !== 6) { result.unshift(cursor.toISOString().slice(0, 10)); } cursor.setDate(cursor.getDate() - 1); } return result; } const TRADING_DAYS = getRecentTradingDays(30); // ─── Formatters ─────────────────────────────────────────── function fmtPrice(n: number | null | undefined, cur = "USD") { if (n == null) return "—"; return new Intl.NumberFormat("en-US", { style: "currency", currency: cur, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n); } function fmtLarge(n: number | null | undefined) { if (n == null) return "—"; const a = Math.abs(n), s = n < 0 ? "-" : ""; if (a >= 1e12) return `${s}$${(a / 1e12).toFixed(2)}T`; if (a >= 1e9) return `${s}$${(a / 1e9).toFixed(2)}B`; if (a >= 1e6) return `${s}$${(a / 1e6).toFixed(2)}M`; if (a >= 1e3) return `${s}$${(a / 1e3).toFixed(1)}K`; return `${s}$${a.toFixed(2)}`; } function fmtShortDate(d: string) { return new Date(d + "T12:00:00Z").toLocaleDateString("en-US", { month: "short", day: "numeric" }); } function fmtYear(d: string) { return d.slice(0, 4); } // ─── Preset dummy data (uses current trading days) ─────── type PriceRow = { date: string; open: number; high: number; low: number; close: number; volume: number }; function makePrices(base: number, seed: number): PriceRow[] { let price = base; return TRADING_DAYS.map((date, i) => { const r = Math.sin(i * seed + seed) * 0.5 + Math.cos(i * 1.7 + seed) * 0.3; price = Math.max(price + r * base * 0.012, base * 0.85); const spread = base * 0.006; const open = Math.round((price - r * base * 0.005) * 100) / 100; const close = Math.round(price * 100) / 100; const high = Math.round((Math.max(open, close) + Math.abs(Math.sin(i * 3.1)) * spread) * 100) / 100; const low = Math.round((Math.min(open, close) - Math.abs(Math.cos(i * 2.7)) * spread) * 100) / 100; return { date, open, high, low, close, volume: Math.round(30000 + Math.abs(Math.sin(i * 1.9 + seed)) * 60000) }; }); } function makeIncome(revenue: number, netIncome: number): StockMetricRow[] { const yrs = ["2025-12-31", "2024-12-31", "2023-12-31"]; const g = (v: number) => yrs.map((d, i) => ({ date: d, value: Math.round(v * Math.pow(0.87, i)) })); return [ { metric: "Total Revenue", values: g(revenue) }, { metric: "Gross Profit", values: g(revenue * 0.55) }, { metric: "Operating Income", values: g(netIncome * 1.3) }, { metric: "Net Income", values: g(netIncome) }, { metric: "EBITDA", values: g(netIncome * 1.6) }, ]; } function makeBalance(assets: number): StockMetricRow[] { const yrs = ["2025-12-31", "2024-12-31", "2023-12-31"]; const g = (v: number) => yrs.map((d, i) => ({ date: d, value: Math.round(v * Math.pow(0.9, i)) })); const liab = assets * 0.48; return [ { metric: "Total Assets", values: g(assets) }, { metric: "Total Liabilities", values: g(liab) }, { metric: "Stockholders Equity", values: g(assets - liab) }, { metric: "Total Debt", values: g(liab * 0.6) }, { metric: "Cash", values: g(assets * 0.12) }, ]; } const PRESET_DATA: Record = { B2V: { ticker: "B2V", period: "30", info: { name: "Blog2Video Inc. ✦ Demo", sector: "Technology", industry: "AI & Video Software", currency: "USD", current_price: 42.38, previous_close: 40.31, market_cap: 1_240_000_000, pe_ratio: 28.4, dividend_yield: null }, prices: makePrices(38, 1.3), income_statement: makeIncome(180_000_000, 28_000_000), balance_sheet: makeBalance(320_000_000) }, SPCX: { ticker: "SPCX", period: "30", info: { name: "SPC Credit", sector: "Financial Services", industry: "Asset Management", currency: "USD", current_price: 25.47, previous_close: 25.69, market_cap: 412_000_000, pe_ratio: null, dividend_yield: 0.089 }, prices: makePrices(25, 2.1), income_statement: [], balance_sheet: makeBalance(412_000_000) }, AAPL: { ticker: "AAPL", period: "30", info: { name: "Apple Inc.", sector: "Technology", industry: "Consumer Electronics", currency: "USD", current_price: 198.45, previous_close: 199.34, market_cap: 3_050_000_000_000, pe_ratio: 32.1, dividend_yield: 0.0052 }, prices: makePrices(194, 3.7), income_statement: makeIncome(391_000_000_000, 100_000_000_000), balance_sheet: makeBalance(352_000_000_000) }, GOOGL: { ticker: "GOOGL", period: "30", info: { name: "Alphabet Inc.", sector: "Communication Svcs", industry: "Internet Content", currency: "USD", current_price: 185.23, previous_close: 186.28, market_cap: 2_270_000_000_000, pe_ratio: 24.8, dividend_yield: 0.0048 }, prices: makePrices(181, 4.2), income_statement: makeIncome(307_000_000_000, 73_000_000_000), balance_sheet: makeBalance(402_000_000_000) }, NVDA: { ticker: "NVDA", period: "30", info: { name: "NVIDIA Corporation", sector: "Technology", industry: "Semiconductors", currency: "USD", current_price: 134.56, previous_close: 130.84, market_cap: 3_290_000_000_000, pe_ratio: 48.2, dividend_yield: 0.0003 }, prices: makePrices(128, 5.1), income_statement: makeIncome(130_000_000_000, 72_000_000_000), balance_sheet: makeBalance(111_000_000_000) }, TSLA: { ticker: "TSLA", period: "30", info: { name: "Tesla, Inc.", sector: "Consumer Cyclical", industry: "Auto Manufacturers", currency: "USD", current_price: 248.90, previous_close: 244.12, market_cap: 796_000_000_000, pe_ratio: 89.7, dividend_yield: null }, prices: makePrices(238, 6.8), income_statement: makeIncome(98_000_000_000, 7_000_000_000), balance_sheet: makeBalance(106_000_000_000) }, MSFT: { ticker: "MSFT", period: "30", info: { name: "Microsoft Corporation", sector: "Technology", industry: "Software—Infrastructure", currency: "USD", current_price: 414.72, previous_close: 412.57, market_cap: 3_080_000_000_000, pe_ratio: 36.8, dividend_yield: 0.007 }, prices: makePrices(408, 7.2), income_statement: makeIncome(245_000_000_000, 88_000_000_000), balance_sheet: makeBalance(484_000_000_000) }, AMZN: { ticker: "AMZN", period: "30", info: { name: "Amazon.com Inc.", sector: "Consumer Cyclical", industry: "Internet Retail", currency: "USD", current_price: 224.15, previous_close: 222.80, market_cap: 2_370_000_000_000, pe_ratio: 42.3, dividend_yield: null }, prices: makePrices(218, 8.4), income_statement: makeIncome(590_000_000_000, 30_000_000_000), balance_sheet: makeBalance(464_000_000_000) }, SPY: { ticker: "SPY", period: "30", info: { name: "SPDR S&P 500 ETF", sector: "ETF", industry: "Large Blend", currency: "USD", current_price: 542.18, previous_close: 540.84, market_cap: 560_000_000_000, pe_ratio: 22.4, dividend_yield: 0.013 }, prices: makePrices(536, 9.1), income_statement: [], balance_sheet: [] }, QQQ: { ticker: "QQQ", period: "30", info: { name: "Invesco QQQ Trust", sector: "ETF", industry: "Large Growth", currency: "USD", current_price: 468.32, previous_close: 466.43, market_cap: 296_000_000_000, pe_ratio: 27.9, dividend_yield: 0.006 }, prices: makePrices(462, 2.9), income_statement: [], balance_sheet: [] }, }; const ALL_TICKERS = Object.keys(PRESET_DATA); const QUICK_PICKS = ["B2V", "SPCX", "AAPL", "GOOGL", "NVDA", "TSLA"] as const; // ─── Scrolling market ticker ────────────────────────────── const TICKER_SCROLL = [ { symbol: "B2V", price: 42.00, pct: 5.12 }, { symbol: "SPCX", price: 25.47, pct: -0.86 }, { symbol: "SPY", price: 542.18, pct: 0.25 }, { symbol: "QQQ", price: 468.32, pct: 0.40 }, { symbol: "AAPL", price: 198.45, pct: -0.45 }, { symbol: "MSFT", price: 414.72, pct: 0.52 }, { symbol: "NVDA", price: 134.56, pct: 2.44 }, { symbol: "GOOGL", price: 185.23, pct: -0.56 }, { symbol: "TSLA", price: 248.90, pct: 1.96 }, { symbol: "AMZN", price: 224.15, pct: 0.43 }, { symbol: "BTC-USD", price: 98450, pct: 1.27 }, { symbol: "GLD", price: 238.74, pct: -0.20 }, ]; function MarketTicker({ t }: { t: Theme }) { const items = [...TICKER_SCROLL, ...TICKER_SCROLL]; return (
MARKETS
{items.map((item, i) => (
{item.symbol} {item.price < 1000 ? item.price.toFixed(2) : item.price.toLocaleString("en-US", { maximumFractionDigits: 0 })} = 0 ? t.up : t.down, fontFamily: "monospace" }}> {item.pct >= 0 ? "▲" : "▼"} {Math.abs(item.pct).toFixed(2)}%
))}
); } // ─── Line chart (SVG, theme-aware) ─────────────────────── interface PricePoint { date: string; open: number | null; high: number | null; low: number | null; close: number | null; volume: number | null; } function LineChart({ prices, currency, ticker, t }: { prices: PricePoint[]; currency: string; ticker: string; t: Theme }) { const containerRef = useRef(null); const [dims, setDims] = useState({ w: 900, h: 440 }); const [hoverIdx, setHoverIdx] = useState(null); useEffect(() => { const el = containerRef.current; if (!el) return; const obs = new ResizeObserver(([e]) => setDims({ w: e.contentRect.width, h: e.contentRect.height })); obs.observe(el); return () => obs.disconnect(); }, []); const valid = prices.filter(p => p.close != null); if (!valid.length) return (
No data
); const ML = 72, MR = 18, MT = 20, MB = 40, VOL_H = 52; const { w, h } = dims; const chartW = w - ML - MR; const priceH = h - MT - MB - VOL_H - 8; const closes = valid.map(p => p.close!); const rawMax = Math.max(...closes), rawMin = Math.min(...closes); const pad = Math.max((rawMax - rawMin) * 0.1, rawMax * 0.01); const pMax = rawMax + pad, pMin = rawMin - pad; const vMax = Math.max(...valid.map(p => p.volume ?? 0), 1); const n = valid.length; const slot = n > 1 ? chartW / (n - 1) : chartW; const xOf = (i: number) => ML + i * slot; const yP = (p: number) => MT + (1 - (p - pMin) / (pMax - pMin)) * priceH; // Y-ticks (6 clean values) const yRange = pMax - pMin; const rawStep = yRange / 5; const mag = Math.pow(10, Math.floor(Math.log10(rawStep))); const step = Math.ceil(rawStep / mag) * mag; const firstTick = Math.ceil(pMin / step) * step; const yTicks = Array.from({ length: 8 }, (_, i) => firstTick + i * step).filter(v => v >= pMin && v <= pMax); // X-ticks const xTickCount = Math.min(n, 8); const xStep = Math.max(Math.floor((n - 1) / (xTickCount - 1)), 1); const xTicks = valid.map((p, i) => ({ i, label: fmtShortDate(p.date) })).filter((_, i) => i % xStep === 0 || i === n - 1); const linePath = valid.map((p, i) => `${i === 0 ? "M" : "L"}${xOf(i).toFixed(1)},${yP(p.close!).toFixed(1)}`).join(" "); const fillPath = `${linePath} L${xOf(n - 1).toFixed(1)},${(h - MB - VOL_H - 8).toFixed(1)} L${ML},${(h - MB - VOL_H - 8).toFixed(1)} Z`; const lastClose = valid[valid.length - 1]?.close; const isOverallUp = valid.length >= 2 ? (valid[valid.length - 1].close ?? 0) >= (valid[0].close ?? 0) : true; const lineColor = isOverallUp ? t.up : t.down; const gradId = `svgrad-${ticker}`; const clipId = `svclip-${ticker}`; const hov = hoverIdx != null ? valid[hoverIdx] : null; const handleMouseMove = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); const idx = Math.max(0, Math.min(n - 1, Math.round((e.clientX - rect.left - ML) / slot))); setHoverIdx(idx); }; return (
setHoverIdx(null)} style={{ display: "block", cursor: "crosshair" }}> {/* Y-axis label */} Price ({currency}) {/* Y grid + labels */} {yTicks.map(tick => { const y = yP(tick); return ( {new Intl.NumberFormat("en-US", { style: "currency", currency, minimumFractionDigits: tick >= 1000 ? 0 : 2, maximumFractionDigits: tick >= 1000 ? 0 : 2 }).format(tick)} ); })} {/* Y-axis border */} {/* Volume separator */} {/* Volume label */} VOL {/* Volume bars */} {valid.map((p, i) => { const isUp = (p.close ?? 0) >= (p.open ?? 0); const barW = Math.max((n > 1 ? slot * 0.55 : 8), 2); const vH = ((p.volume ?? 0) / vMax) * VOL_H; return ( ); })} {/* X-axis */} {/* X labels */} {xTicks.map(({ i, label }) => ( {label} ))} {/* X-axis label */} Date {/* Fill + line */} {/* Current price badge */} {lastClose != null && ( {new Intl.NumberFormat("en-US", { style: "currency", currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(lastClose)} )} {/* Crosshair + dot */} {hov && hoverIdx != null && ( <> )} {/* Tooltip */} {hov && hoverIdx != null && (() => { const isUp = (hov.close ?? 0) >= (hov.open ?? 0); const tx = xOf(hoverIdx) > dims.w * 0.62 ? xOf(hoverIdx) - 170 : xOf(hoverIdx) + 14; return (

{fmtShortDate(hov.date)}

{(["Open","High","Low","Close"] as const).map((lbl, idx) => { const v = [hov.open, hov.high, hov.low, hov.close][idx]; return (
{lbl} {fmtPrice(v, currency)}
); })}
Volume {fmtLarge(hov.volume)}
); })()}
); } // ─── Financial table (theme-aware, full-width) ──────────── function FinancialTable({ rows, title, t }: { rows: StockMetricRow[]; title: string; t: Theme }) { if (!rows.length) return (
No {title.toLowerCase()} data for this ticker.
); const dates = rows[0]?.values.map(v => v.date) ?? []; return (

{title}

"110px").join(" ")}`, color: t.textSub, borderBottom: `1px solid ${t.border}` }}> Metric {dates.map(d => {fmtYear(d)})}
{rows.map((row, i) => (
"110px").join(" ")}`, borderBottom: i < rows.length - 1 ? `1px solid ${t.border}` : undefined, background: i % 2 === 1 ? `rgba(0,0,0,0.02)` : "transparent" }}> {row.metric} {row.values.map(v => ( {fmtLarge(v.value)} ))}
))}
); } // ─── Signup modal (FreeTemplatesPage pattern, purple) ──── function SignupModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) { const { login } = useAuth(); const [signingIn, setSigningIn] = useState(false); const [error, setError] = useState(null); const handleSuccess = useCallback(async (response: CredentialResponse) => { if (!response.credential) return; setSigningIn(true); setError(null); try { const res = await googleLogin(response.credential, false, localStorage.getItem("b2v_ref_code")); localStorage.removeItem("b2v_ref_code"); login(res.data.access_token, res.data.user); onSuccess(); } catch { setError("Sign-in failed. Please try again."); } finally { setSigningIn(false); } }, [login, onSuccess]); return (
{ if (e.target === e.currentTarget) onClose(); }} >

Free stock tool

Unlock full market data

Sign in to search any ticker, change time ranges, and view income statements and balance sheets — all free.

    {["Any stock, ETF, or index ticker", "7, 30, or 60-day live price history", "Annual income statement & balance sheet"].map(item => (
  • {item}
  • ))}
{signingIn ? (
Signing in…
) : ( setError("Sign-in failed. Please try again.")} text="signup_with" width="320" /> )}
{error &&

{error}

}

No credit card required · Free forever

); } // ─── Main component ─────────────────────────────────────── export default function StockVisualizer() { const { user, login } = useAuth(); const isAuthed = Boolean(user); const [themeId, setThemeId] = useState("newscast"); const t = THEMES[themeId]; const [activeTicker, setActiveTicker] = useState(DEFAULT_TICKER); const [searchQuery, setSearchQuery] = useState(DEFAULT_TICKER); const [showDropdown, setShowDropdown] = useState(false); const [period, setPeriod] = useState(DEFAULT_PERIOD); const [data, setData] = useState(PRESET_DATA[DEFAULT_TICKER]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [showModal, setShowModal] = useState(false); const filtered = ALL_TICKERS.filter(s => s.startsWith(searchQuery.toUpperCase()) && s !== activeTicker); const fetchData = useCallback(async (sym: string, days: Period) => { setLoading(true); setError(null); try { const res = await getStockData(sym, days); setData(res.data); } catch (err: unknown) { setError((err as { response?: { data?: { detail?: string } } })?.response?.data?.detail ?? "Failed to load data."); setData(null); } finally { setLoading(false); } }, []); useEffect(() => { if (!isAuthed) return; // B2V is fictional — keep it as demo data, never hit the API for it if (activeTicker === "B2V") { setData(PRESET_DATA["B2V"]); return; } // All real tickers always fetch live data from yfinance void fetchData(activeTicker, period); }, [activeTicker, period, isAuthed, fetchData]); const switchTicker = useCallback((sym: string) => { if (!isAuthed) { setShowModal(true); return; } setActiveTicker(sym); setSearchQuery(sym); setShowDropdown(false); if (sym === "B2V") setData(PRESET_DATA["B2V"]); else void fetchData(sym, period); }, [isAuthed, period, fetchData]); const handleSearchSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); if (!isAuthed) { setShowModal(true); return; } const sym = searchQuery.trim().toUpperCase(); if (!sym) return; setActiveTicker(sym); setShowDropdown(false); if (sym === "B2V") setData(PRESET_DATA["B2V"]); else void fetchData(sym, period); }, [isAuthed, searchQuery, period, fetchData]); const prices = data?.prices ?? []; const cur = data?.info.currency ?? "USD"; const lastPrice = data?.info.current_price ?? (prices.length ? prices[prices.length - 1]?.close ?? null : null); const prevClose = data?.info.previous_close ?? (prices.length >= 2 ? prices[prices.length - 2]?.close ?? null : null); const change = lastPrice != null && prevClose != null ? lastPrice - prevClose : null; const changePct = change != null && prevClose ? (change / Math.abs(prevClose)) * 100 : null; const isUp = change != null ? change >= 0 : null; return (
{showModal && ( setShowModal(false)} onSuccess={() => setShowModal(false)} /> )} {/* ── Theme + toolbar ──────────────────────────────── */}
{/* LIVE badge */} LIVE Market Data {/* Theme switcher */}
{(["newscast", "bloomberg", "economist"] as ThemeId[]).map(id => ( ))}
{/* ── Search + quick picks + period ───────────────── */}
{/* Search */}
{ if (!isAuthed) { setShowModal(true); return; } setSearchQuery(e.target.value.toUpperCase()); setShowDropdown(true); }} onFocus={() => { if (!isAuthed) { setShowModal(true); return; } setShowDropdown(true); }} onBlur={() => setTimeout(() => setShowDropdown(false), 150)} readOnly={!isAuthed} placeholder="Search any ticker…" className="w-48 rounded-xl border pl-9 pr-3 py-2 text-sm font-bold uppercase tracking-wider focus:outline-none" style={{ border: `1px solid ${t.border}`, background: t.surfBg, color: t.text, cursor: "pointer", }} />
{/* Autocomplete dropdown */} {showDropdown && filtered.length > 0 && isAuthed && (
{filtered.map(sym => ( ))}
)}
{/* Quick picks */}
{QUICK_PICKS.map(sym => ( ))} {!isAuthed && ( )}
{/* Period */}
{([7, 30, 60] as Period[]).map(p => ( ))}
{/* ── Demo notice ──────────────────────────────────── */} {activeTicker === "B2V" && (
Demo mode — B2V (Blog2Video Inc.) is a fictional ticker. Sign in below to switch to real stocks — SPCX, AAPL, GOOGL, NVDA, TSLA, or search any symbol.
)} {/* ── Chart ────────────────────────────────────────── */}
{/* Price header */}
{data?.ticker ?? activeTicker} {data?.info.name && ( — {data.info.name} )}
{loading ?
: (
{fmtPrice(lastPrice, cur)} {change != null && changePct != null && ( {change >= 0 ? "▲" : "▼"} {fmtPrice(Math.abs(change), cur)} ({Math.abs(changePct).toFixed(2)}%) )}
)}
{[ { label: "MKT CAP", val: fmtLarge(data?.info.market_cap) }, { label: "P/E", val: data?.info.pe_ratio?.toFixed(2) ?? "N/A" }, { label: "DIV YLD", val: data?.info.dividend_yield != null ? `${(data.info.dividend_yield * 100).toFixed(2)}%` : "—" }, { label: "SECTOR", val: data?.info.sector ?? "—" }, { label: "INDUSTRY",val: data?.info.industry ?? "—" }, ].map(({ label, val }) => (

{label}

{val}

))}
{/* Line chart */}
{loading ?
: error ?

{error}

: }
{/* ── Financial statements ─────────────────────────── */}
{!isAuthed && (
Sign in to view financials

No credit card required

)}
{/* ── Bottom CTA ───────────────────────────────────── */} {!isAuthed && (
Free · No credit card

Search any real ticker and unlock financials

B2V is a demo ticker. Sign in to search SPCX, AAPL, GOOGL, NVDA, TSLA, or any stock symbol — and view live 30-day price charts plus 3 years of annual income statements and balance sheets.

{ if (!response.credential) return; try { const res = await googleLogin(response.credential, false, localStorage.getItem("b2v_ref_code")); localStorage.removeItem("b2v_ref_code"); login(res.data.access_token, res.data.user); } catch { /* silently ignore */ } }} onError={() => undefined} text="signup_with" width="300" />
)}
); }