feat: schedule targeted refresh at each park's exact opening time

In addition to the 2-minute polling interval, compute milliseconds until
each park's opening time (from hoursLabel + park timezone) and schedule
a setTimeout to fire 30s after opening. This ensures the open indicator
and ride counts appear immediately rather than waiting up to 2 minutes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Josh Wright
2026-04-05 11:06:03 -04:00
parent 7ee28c7ca3
commit 7c00ae5000

View File

@@ -11,6 +11,34 @@ 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";
@@ -54,6 +82,28 @@ export function HomePageClient({
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<typeof setTimeout>[] = [];
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 + OPEN_REFRESH_BUFFER_MS));
}
}
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);