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:
@@ -11,6 +11,34 @@ import { PARKS, groupByRegion } from "@/lib/parks";
|
|||||||
import type { DayData } from "@/lib/db";
|
import type { DayData } from "@/lib/db";
|
||||||
|
|
||||||
const REFRESH_INTERVAL_MS = 2 * 60 * 1000; // 2 minutes
|
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";
|
const COASTER_MODE_KEY = "coasterMode";
|
||||||
|
|
||||||
@@ -54,6 +82,28 @@ export function HomePageClient({
|
|||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [isCurrentWeek, router]);
|
}, [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.
|
// Remember the current week so the park page back button returns here.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("lastWeek", weekStart);
|
localStorage.setItem("lastWeek", weekStart);
|
||||||
|
|||||||
Reference in New Issue
Block a user