'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}
{/* 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 && (
)}
)
}