'use client' import { useEffect, useState, useRef } from 'react' import { StockCard } from '@/components/widgets/StockCard' import { NewsHeadlines } from '@/components/widgets/NewsHeadlines' import { getUserSymbols } from '@/lib/userInterestsApi' import { WeatherWidget } from '@/components/widgets/WeatherWidget' import { getTimeFormat, type TimeFormat } from '@/lib/weatherApi' type MarketStatus = { label: string color: 'green' | 'yellow' | 'red' } function getMarketStatus(now: Date): MarketStatus { const day = now.getDay() if (day === 0 || day === 6) { return { label: 'NYSE Closed', color: 'red' } } const estStr = now.toLocaleString('en-US', { timeZone: 'America/New_York', hour: 'numeric', minute: 'numeric', hour12: false, }) const [estHourStr, estMinStr] = estStr.split(':') const estMinutes = parseInt(estHourStr) * 60 + parseInt(estMinStr) const t0700 = 7 * 60 const t0930 = 9 * 60 + 30 const t1600 = 16 * 60 const t2000 = 20 * 60 if (estMinutes >= t0700 && estMinutes < t0930) { return { label: 'NYSE Premarket', color: 'yellow' } } if (estMinutes >= t0930 && estMinutes < t1600) { return { label: 'NYSE Open', color: 'green' } } if (estMinutes >= t1600 && estMinutes < t2000) { return { label: 'NYSE After-Hours', color: 'yellow' } } return { label: 'NYSE Closed', color: 'red' } } const indicatorColors = { green: { box: 'bg-positive', text: 'text-positive' }, yellow: { box: 'bg-yellow-400', text: 'text-yellow-500' }, red: { box: 'bg-negative', text: 'text-negative' }, } // Skeleton placeholder for StockCard function StockCardSkeleton() { return (
) } // Skeleton placeholder for NewsHeadlines (inside card) function NewsHeadlinesSkeleton() { return (
{[1, 2, 3, 4].map((i) => (
))}
) } export function FrontPage() { const [currentTime, setCurrentTime] = useState('') const [currentDate, setCurrentDate] = useState('') const [marketStatus, setMarketStatus] = useState({ label: 'NYSE Closed', color: 'red' }) const [timeFormat, setTimeFormatState] = useState('12h') // Symbol list state const [symbols, setSymbols] = useState([]) const [initialLoading, setInitialLoading] = useState(true) const [symbolsError, setSymbolsError] = useState(null) // Rotation state - track displayed symbol separately for seamless transitions const [activeIndex, setActiveIndex] = useState(0) const [displayedSymbol, setDisplayedSymbol] = useState(null) const [animationKey, setAnimationKey] = useState(0) const [isTransitioning, setIsTransitioning] = useState(false) // Track if first symbol has been shown const hasShownFirst = useRef(false) // Derived active symbol from index const activeSymbol = symbols.length > 0 ? symbols[activeIndex] : null // Fetch user symbols on mount useEffect(() => { setInitialLoading(true) setSymbolsError(null) getUserSymbols() .then((items) => { setSymbols(items) setActiveIndex(0) if (items.length > 0) { setDisplayedSymbol(items[0]) hasShownFirst.current = true } }) .catch((err) => setSymbolsError(err.message)) .finally(() => setInitialLoading(false)) }, []) // Rotation interval - only when more than one symbol useEffect(() => { if (symbols.length <= 1) return const interval = setInterval(() => { setIsTransitioning(true) // After outgoing animation, switch to next setTimeout(() => { setActiveIndex((prev) => { const next = (prev + 1) % symbols.length setDisplayedSymbol(symbols[next]) setAnimationKey((k) => k + 1) return next }) setIsTransitioning(false) }, 300) }, 11000) return () => clearInterval(interval) }, [symbols]) // Reset index if it becomes invalid after symbols change useEffect(() => { if (activeIndex >= symbols.length && symbols.length > 0) { setActiveIndex(0) setDisplayedSymbol(symbols[0]) } }, [symbols, activeIndex]) // Load time format preference on mount useEffect(() => { setTimeFormatState(getTimeFormat()) }, []) useEffect(() => { const update = () => { const now = new Date() const is24h = timeFormat === '24h' setCurrentTime(now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: !is24h })) setCurrentDate(now.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })) setMarketStatus(getMarketStatus(now)) } update() const interval = setInterval(update, 1000) return () => clearInterval(interval) }, [timeFormat]) const colors = indicatorColors[marketStatus.color] return (
{/* Top Half - Time, Date, Market Status, Weather */}
{/* Left: Time, Date, Market Status */}
{currentTime}
{currentDate}
{marketStatus.label}
{/* Right: Weather */}
{/* Bottom Half - Stock Summary & News */}
{/* Left: Stock Card */}
{/* Initial loading skeleton */} {initialLoading && } {/* Error state */} {!initialLoading && symbolsError && (
Unable to load symbols
)} {/* Empty state */} {!initialLoading && !symbolsError && symbols.length === 0 && (
No symbols added yet.
)} {/* Active symbol with slide transition */} {!initialLoading && !symbolsError && displayedSymbol && (
)}
{/* Right: News Headlines */}
{/* Initial loading skeleton */} {initialLoading && } {/* Error state */} {!initialLoading && symbolsError && (

News Headlines

Unable to load
)} {/* Empty state */} {!initialLoading && !symbolsError && symbols.length === 0 && (

News Headlines

No symbols to show headlines for.
)} {/* Active headlines with slide transition */} {!initialLoading && !symbolsError && displayedSymbol && (

News Headlines

)}
) }