All checks were successful
Build and Deploy / Build & Push (push) Successful in 4m22s
- isWithinOperatingWindow now accepts an IANA timezone and reads the current time in the park's local timezone via Intl.DateTimeFormat, fixing false positives when the server runs in UTC but parks store hours in local time (e.g. Pacific parks showing open at 6:50 AM EDT) - Remove the 1-hour pre-open buffer so parks are not marked open before their doors actually open; retain the 1-hour post-close grace period - Add getTimezoneAbbr() helper to derive the short tz label (EDT, PDT…) - All hours labels now display with the local timezone abbreviation (e.g. "10am – 6pm PDT") in WeekCalendar, ParkCard, and ParkMonthCalendar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
/**
|
||
* Environment variable helpers.
|
||
*/
|
||
|
||
/**
|
||
* Parse a staleness window from an env var string (interpreted as hours).
|
||
* Falls back to `defaultHours` when the value is missing, non-numeric,
|
||
* non-finite, or <= 0 — preventing NaN from silently breaking staleness checks.
|
||
*/
|
||
export function parseStalenessHours(envVar: string | undefined, defaultHours: number): number {
|
||
const parsed = parseInt(envVar ?? "", 10);
|
||
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultHours;
|
||
}
|
||
|
||
/**
|
||
* Returns today's date as YYYY-MM-DD using local wall-clock time with a 3 AM
|
||
* switchover. Before 3 AM local time we still consider it "yesterday", so the
|
||
* calendar doesn't flip to the next day at midnight while people are still out
|
||
* at the park.
|
||
*
|
||
* Important: `new Date().toISOString()` returns UTC, which causes the date to
|
||
* advance at 8 PM EDT (UTC-4) or 7 PM EST (UTC-5) — too early. This helper
|
||
* corrects that by using local year/month/day components and rolling back one
|
||
* day when the local hour is before 3.
|
||
*/
|
||
export function getTodayLocal(): string {
|
||
const now = new Date();
|
||
if (now.getHours() < 3) {
|
||
now.setDate(now.getDate() - 1);
|
||
}
|
||
const y = now.getFullYear();
|
||
const m = String(now.getMonth() + 1).padStart(2, "0");
|
||
const d = String(now.getDate()).padStart(2, "0");
|
||
return `${y}-${m}-${d}`;
|
||
}
|
||
|
||
/**
|
||
* Returns the short timezone abbreviation for a given IANA timezone,
|
||
* e.g. "America/Los_Angeles" → "PDT" or "PST".
|
||
*/
|
||
export function getTimezoneAbbr(timezone: string): string {
|
||
const parts = new Intl.DateTimeFormat("en-US", {
|
||
timeZone: timezone,
|
||
timeZoneName: "short",
|
||
}).formatToParts(new Date());
|
||
return parts.find((p) => p.type === "timeZoneName")?.value ?? "";
|
||
}
|
||
|
||
/**
|
||
* Returns true when the current time in the park's timezone is within
|
||
* the operating window (open time through 1 hour after close), based on
|
||
* a hoursLabel like "10am – 6pm". Falls back to true when unparseable.
|
||
*
|
||
* Uses the park's IANA timezone so a Pacific park's "10am" is correctly
|
||
* compared to Pacific time regardless of where the server is running.
|
||
*/
|
||
export function isWithinOperatingWindow(hoursLabel: string, timezone: string): boolean {
|
||
const m = hoursLabel.match(
|
||
/^(\d+)(?::(\d+))?(am|pm)\s*[–-]\s*(\d+)(?::(\d+))?(am|pm)$/i
|
||
);
|
||
if (!m) return true;
|
||
const toMinutes = (h: string, min: string | undefined, period: string) => {
|
||
let hours = parseInt(h, 10);
|
||
const minutes = min ? parseInt(min, 10) : 0;
|
||
if (period.toLowerCase() === "pm" && hours !== 12) hours += 12;
|
||
if (period.toLowerCase() === "am" && hours === 12) hours = 0;
|
||
return hours * 60 + minutes;
|
||
};
|
||
const openMin = toMinutes(m[1], m[2], m[3]);
|
||
const closeMin = toMinutes(m[4], m[5], m[6]);
|
||
|
||
// Get the current time in the park's local timezone.
|
||
const parts = new Intl.DateTimeFormat("en-US", {
|
||
timeZone: timezone,
|
||
hour: "numeric",
|
||
minute: "2-digit",
|
||
hour12: false,
|
||
}).formatToParts(new Date());
|
||
const h = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10);
|
||
const min = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10);
|
||
const nowMin = (h % 24) * 60 + min;
|
||
|
||
return nowMin >= openMin && nowMin <= closeMin + 60;
|
||
}
|