"use client"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { WeekCalendar } from "./WeekCalendar"; import { MobileCardList } from "./MobileCardList"; import { WeekNav } from "./WeekNav"; import { Legend } from "./Legend"; import { EmptyState } from "./EmptyState"; import { PARKS, groupByRegion } from "@/lib/parks"; import type { DayData } from "@/lib/db"; const REFRESH_INTERVAL_MS = 2 * 60 * 1000; // 2 minutes const OPEN_REFRESH_BUFFER_MS = 30_000; // 30s after opening time before hitting the API /** Parse the opening hour/minute from a hoursLabel like "10am", "10:30am", "11am". */ function parseOpenTime(hoursLabel: string): { hour: number; minute: number } | null { const openPart = hoursLabel.split(" - ")[0].trim(); const match = openPart.match(/^(\d+)(?::(\d+))?(am|pm)$/i); if (!match) return null; let hour = parseInt(match[1], 10); const minute = match[2] ? parseInt(match[2], 10) : 0; const period = match[3].toLowerCase(); if (period === "pm" && hour !== 12) hour += 12; if (period === "am" && hour === 12) hour = 0; return { hour, minute }; } /** Milliseconds from now until a given local clock time in a timezone. Negative if already past. */ function msUntilLocalTime(hour: number, minute: number, timezone: string): number { const now = new Date(); const parts = new Intl.DateTimeFormat("en-US", { timeZone: timezone, hour: "numeric", minute: "2-digit", hour12: false, }).formatToParts(now); const localHour = parseInt(parts.find(p => p.type === "hour")!.value, 10) % 24; const localMinute = parseInt(parts.find(p => p.type === "minute")!.value, 10); return ((hour * 60 + minute) - (localHour * 60 + localMinute)) * 60_000; } const COASTER_MODE_KEY = "coasterMode"; interface HomePageClientProps { weekStart: string; weekDates: string[]; today: string; isCurrentWeek: boolean; data: Record>; rideCounts: Record; coasterCounts: Record; openParkIds: string[]; closingParkIds: string[]; weatherDelayParkIds: string[]; hasCoasterData: boolean; scrapedCount: number; } export function HomePageClient({ weekStart, weekDates, today, isCurrentWeek, data, rideCounts, coasterCounts, openParkIds, closingParkIds, weatherDelayParkIds, hasCoasterData, scrapedCount, }: HomePageClientProps) { const router = useRouter(); const [coastersOnly, setCoastersOnly] = useState(false); // Hydrate from localStorage after mount to avoid SSR mismatch. useEffect(() => { setCoastersOnly(localStorage.getItem(COASTER_MODE_KEY) === "true"); }, []); // Periodically re-fetch server data (ride counts, open status) without a full page reload. useEffect(() => { if (!isCurrentWeek) return; const id = setInterval(() => router.refresh(), REFRESH_INTERVAL_MS); return () => clearInterval(id); }, [isCurrentWeek, router]); // Schedule a targeted refresh at each park's exact opening time so the // open indicator and ride counts appear immediately rather than waiting // up to 2 minutes for the next polling cycle. useEffect(() => { if (!isCurrentWeek) return; const timeouts: ReturnType[] = []; for (const park of PARKS) { const dayData = data[park.id]?.[today]; if (!dayData?.isOpen || !dayData.hoursLabel) continue; const openTime = parseOpenTime(dayData.hoursLabel); if (!openTime) continue; const ms = msUntilLocalTime(openTime.hour, openTime.minute, park.timezone); // Only schedule if opening is still in the future (within the next 24h) if (ms > 0 && ms < 24 * 60 * 60 * 1000) { timeouts.push(setTimeout(() => router.refresh(), ms)); // mark as open timeouts.push(setTimeout(() => router.refresh(), ms + OPEN_REFRESH_BUFFER_MS)); // pick up ride counts } } return () => timeouts.forEach(clearTimeout); }, [isCurrentWeek, today, data, router]); // Remember the current week so the park page back button returns here. useEffect(() => { localStorage.setItem("lastWeek", weekStart); }, [weekStart]); const toggle = () => { const next = !coastersOnly; setCoastersOnly(next); localStorage.setItem(COASTER_MODE_KEY, String(next)); }; const activeCounts = coastersOnly ? coasterCounts : rideCounts; const visibleParks = PARKS.filter((park) => weekDates.some((date) => data[park.id]?.[date]?.isOpen) ); const grouped = groupByRegion(visibleParks); return (
{/* ── Header ───────────────────────────────────────────────────────────── */}
{/* Row 1: Title + controls */}
Thoosie Calendar
{hasCoasterData && ( )} 0 ? "var(--color-open-bg)" : "var(--color-surface)", border: `1px solid ${visibleParks.length > 0 ? "var(--color-open-border)" : "var(--color-border)"}`, borderRadius: 20, padding: "4px 14px", fontSize: "0.78rem", color: visibleParks.length > 0 ? "var(--color-open-hours)" : "var(--color-text-muted)", fontWeight: 600, alignItems: "center", whiteSpace: "nowrap", }}> {visibleParks.length} of {PARKS.length} parks open this week
{/* Row 2: Week nav + legend */}
{/* ── Main content ────────────────────────────────────────────────────── */}
{scrapedCount === 0 ? ( ) : ( <> {/* Mobile: card list (hidden on lg+) */}
{/* Desktop: week table (hidden below lg) */}
)}
); }