'use client'; import { Clock, ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { createChart, ColorType, LineStyle, BaselineSeries, LineSeries, } from 'lightweight-charts'; type StockWidgetProps = { symbol: string; shortName: string; longName?: string; exchange?: string; currency?: string; marketState?: string; regularMarketPrice?: number; regularMarketChange?: number; regularMarketChangePercent?: number; regularMarketPreviousClose?: number; regularMarketOpen?: number; regularMarketDayHigh?: number; regularMarketDayLow?: number; regularMarketVolume?: number; averageDailyVolume3Month?: number; marketCap?: number; fiftyTwoWeekLow?: number; fiftyTwoWeekHigh?: number; trailingPE?: number; forwardPE?: number; dividendYield?: number; earningsPerShare?: number; website?: string; postMarketPrice?: number; postMarketChange?: number; postMarketChangePercent?: number; preMarketPrice?: number; preMarketChange?: number; preMarketChangePercent?: number; chartData?: { '1D'?: { timestamps: number[]; prices: number[] } | null; '5D'?: { timestamps: number[]; prices: number[] } | null; '1M'?: { timestamps: number[]; prices: number[] } | null; '3M'?: { timestamps: number[]; prices: number[] } | null; '6M'?: { timestamps: number[]; prices: number[] } | null; '1Y'?: { timestamps: number[]; prices: number[] } | null; MAX?: { timestamps: number[]; prices: number[] } | null; } | null; comparisonData?: Array<{ ticker: string; name: string; chartData: { '1D'?: { timestamps: number[]; prices: number[] } | null; '5D'?: { timestamps: number[]; prices: number[] } | null; '1M'?: { timestamps: number[]; prices: number[] } | null; '3M'?: { timestamps: number[]; prices: number[] } | null; '6M'?: { timestamps: number[]; prices: number[] } | null; '1Y'?: { timestamps: number[]; prices: number[] } | null; MAX?: { timestamps: number[]; prices: number[] } | null; }; }> | null; error?: string; }; const formatNumber = (num: number | undefined, decimals = 2): string => { if (num === undefined || num === null) return 'N/A'; return num.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }); }; const formatLargeNumber = (num: number | undefined): string => { if (num === undefined || num === null) return 'N/A'; if (num >= 1e12) return `$${(num / 1e12).toFixed(2)}T`; if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`; if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`; if (num >= 1e3) return `$${(num / 1e3).toFixed(2)}K`; return `$${num.toFixed(2)}`; }; const Stock = (props: StockWidgetProps) => { const [isDarkMode, setIsDarkMode] = useState(false); const [selectedTimeframe, setSelectedTimeframe] = useState< '1D' | '5D' | '1M' | '3M' | '6M' | '1Y' | 'MAX' >('1M'); const chartContainerRef = useRef(null); useEffect(() => { const checkDarkMode = () => { setIsDarkMode(document.documentElement.classList.contains('dark')); }; checkDarkMode(); const observer = new MutationObserver(checkDarkMode); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'], }); return () => observer.disconnect(); }, []); useEffect(() => { const currentChartData = props.chartData?.[selectedTimeframe]; if ( !chartContainerRef.current || !currentChartData || currentChartData.timestamps.length === 0 ) { return; } const chart = createChart(chartContainerRef.current, { width: chartContainerRef.current.clientWidth, height: 280, layout: { background: { type: ColorType.Solid, color: 'transparent' }, textColor: isDarkMode ? '#6b7280' : '#9ca3af', fontSize: 11, attributionLogo: false, }, grid: { vertLines: { color: isDarkMode ? '#21262d' : '#e8edf1', style: LineStyle.Solid, }, horzLines: { color: isDarkMode ? '#21262d' : '#e8edf1', style: LineStyle.Solid, }, }, crosshair: { vertLine: { color: isDarkMode ? '#30363d' : '#d0d7de', labelVisible: false, }, horzLine: { color: isDarkMode ? '#30363d' : '#d0d7de', labelVisible: true, }, }, rightPriceScale: { borderVisible: false, visible: false, }, leftPriceScale: { borderVisible: false, visible: true, }, timeScale: { borderVisible: false, timeVisible: false, }, handleScroll: false, handleScale: false, }); const prices = currentChartData.prices; let baselinePrice: number; if (selectedTimeframe === '1D') { baselinePrice = props.regularMarketPreviousClose ?? prices[0]; } else { baselinePrice = prices[0]; } const baselineSeries = chart.addSeries(BaselineSeries); baselineSeries.applyOptions({ baseValue: { type: 'price', price: baselinePrice }, topLineColor: isDarkMode ? '#14b8a6' : '#0d9488', topFillColor1: isDarkMode ? 'rgba(20, 184, 166, 0.28)' : 'rgba(13, 148, 136, 0.24)', topFillColor2: isDarkMode ? 'rgba(20, 184, 166, 0.05)' : 'rgba(13, 148, 136, 0.05)', bottomLineColor: isDarkMode ? '#f87171' : '#dc2626', bottomFillColor1: isDarkMode ? 'rgba(248, 113, 113, 0.05)' : 'rgba(220, 38, 38, 0.05)', bottomFillColor2: isDarkMode ? 'rgba(248, 113, 113, 0.28)' : 'rgba(220, 38, 38, 0.24)', lineWidth: 2, crosshairMarkerVisible: true, crosshairMarkerRadius: 4, crosshairMarkerBorderColor: '', crosshairMarkerBackgroundColor: '', }); const data = currentChartData.timestamps.map((timestamp, index) => { const price = currentChartData.prices[index]; return { time: (timestamp / 1000) as any, value: price, }; }); baselineSeries.setData(data); const comparisonColors = ['#8b5cf6', '#f59e0b', '#ec4899']; if (props.comparisonData && props.comparisonData.length > 0) { props.comparisonData.forEach((comp, index) => { const compChartData = comp.chartData[selectedTimeframe]; if (compChartData && compChartData.prices.length > 0) { const compData = compChartData.timestamps.map((timestamp, i) => ({ time: (timestamp / 1000) as any, value: compChartData.prices[i], })); const compSeries = chart.addSeries(LineSeries); compSeries.applyOptions({ color: comparisonColors[index] || '#6b7280', lineWidth: 2, crosshairMarkerVisible: true, crosshairMarkerRadius: 4, priceScaleId: 'left', }); compSeries.setData(compData); } }); } chart.timeScale().fitContent(); const handleResize = () => { if (chartContainerRef.current) { chart.applyOptions({ width: chartContainerRef.current.clientWidth, }); } }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); chart.remove(); }; }, [ props.chartData, props.comparisonData, selectedTimeframe, isDarkMode, props.regularMarketPreviousClose, ]); const isPositive = (props.regularMarketChange ?? 0) >= 0; const isMarketOpen = props.marketState === 'REGULAR'; const isPreMarket = props.marketState === 'PRE'; const isPostMarket = props.marketState === 'POST'; const displayPrice = isPostMarket ? props.postMarketPrice ?? props.regularMarketPrice : isPreMarket ? props.preMarketPrice ?? props.regularMarketPrice : props.regularMarketPrice; const displayChange = isPostMarket ? props.postMarketChange ?? props.regularMarketChange : isPreMarket ? props.preMarketChange ?? props.regularMarketChange : props.regularMarketChange; const displayChangePercent = isPostMarket ? props.postMarketChangePercent ?? props.regularMarketChangePercent : isPreMarket ? props.preMarketChangePercent ?? props.regularMarketChangePercent : props.regularMarketChangePercent; const changeColor = isPositive ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'; if (props.error) { return (

Error: {props.error}

); } return (
{props.website && ( {`${props.symbol} { (e.target as HTMLImageElement).style.display = 'none'; }} /> )}

{props.symbol}

{props.exchange && ( {props.exchange} )} {isMarketOpen && (
Live
)} {isPreMarket && (
Pre-Market
)} {isPostMarket && (
After Hours
)}

{props.longName || props.shortName}

{props.currency === 'USD' ? '$' : ''} {formatNumber(displayPrice)}
{isPositive ? ( ) : displayChange === 0 ? ( ) : ( )} {displayChange !== undefined && displayChange >= 0 ? '+' : ''} {formatNumber(displayChange)} ( {displayChangePercent !== undefined && displayChangePercent >= 0 ? '+' : ''} {formatNumber(displayChangePercent)}%)
{props.chartData && (
{(['1D', '5D', '1M', '3M', '6M', '1Y', 'MAX'] as const).map( (timeframe) => ( ), )}
{props.comparisonData && props.comparisonData.length > 0 && (
{props.symbol} {props.comparisonData.map((comp, index) => { const colors = ['#8b5cf6', '#f59e0b', '#ec4899']; return (
{comp.ticker}
); })}
)}
Prev Close ${formatNumber(props.regularMarketPreviousClose)}
52W Range ${formatNumber(props.fiftyTwoWeekLow, 2)}-$ {formatNumber(props.fiftyTwoWeekHigh, 2)}
Market Cap {formatLargeNumber(props.marketCap)}
Open ${formatNumber(props.regularMarketOpen)}
P/E Ratio {props.trailingPE ? formatNumber(props.trailingPE, 2) : 'N/A'}
Dividend Yield {props.dividendYield ? `${formatNumber(props.dividendYield * 100, 2)}%` : 'N/A'}
Day Range ${formatNumber(props.regularMarketDayLow, 2)}-$ {formatNumber(props.regularMarketDayHigh, 2)}
Volume {formatLargeNumber(props.regularMarketVolume)}
EPS $ {props.earningsPerShare ? formatNumber(props.earningsPerShare, 2) : 'N/A'}
)}
); }; export default Stock;