From fbf4337a83d2d8ed76e5b83636523b3ea9eb357d Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 4 Apr 2026 20:43:33 -0400 Subject: [PATCH] feat: park page operating window check; always show ride total MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract isWithinOperatingWindow() to lib/env.ts (shared) - Park detail page: always fetch Queue-Times, but force all rides closed when outside the ±1h operating window - LiveRidePanel: always show closed ride count badge (not just when some rides are also open); label reads "X rides total" when none are open vs "X closed / down" when some are Co-Authored-By: Claude Sonnet 4.6 --- app/page.tsx | 25 +------------------------ app/park/[id]/page.tsx | 18 ++++++++++++++++-- components/LiveRidePanel.tsx | 6 +++--- lib/env.ts | 24 ++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 29 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 8d554e8..9115b06 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,7 +5,7 @@ import { Legend } from "@/components/Legend"; import { EmptyState } from "@/components/EmptyState"; import { PARKS, groupByRegion } from "@/lib/parks"; import { openDb, getDateRange } from "@/lib/db"; -import { getTodayLocal } from "@/lib/env"; +import { getTodayLocal, isWithinOperatingWindow } from "@/lib/env"; import { fetchLiveRides } from "@/lib/scrapers/queuetimes"; import { QUEUE_TIMES_IDS } from "@/lib/queue-times-map"; @@ -13,29 +13,6 @@ interface PageProps { searchParams: Promise<{ week?: string }>; } -/** - * Returns true when the current local time is within 1 hour before open - * or 1 hour after close, based on a hoursLabel like "10am – 6pm". - */ -function isWithinOperatingWindow(hoursLabel: string): boolean { - const m = hoursLabel.match( - /^(\d+)(?::(\d+))?(am|pm)\s*[–-]\s*(\d+)(?::(\d+))?(am|pm)$/i - ); - if (!m) return true; // unparseable — show anyway - 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]); - const now = new Date(); - const nowMin = now.getHours() * 60 + now.getMinutes(); - return nowMin >= openMin - 60 && nowMin <= closeMin + 60; -} - function getWeekStart(param: string | undefined): string { if (param && /^\d{4}-\d{2}-\d{2}$/.test(param)) { const d = new Date(param + "T00:00:00"); diff --git a/app/park/[id]/page.tsx b/app/park/[id]/page.tsx index 5dd6d7b..15345a8 100644 --- a/app/park/[id]/page.tsx +++ b/app/park/[id]/page.tsx @@ -10,7 +10,7 @@ import { ParkMonthCalendar } from "@/components/ParkMonthCalendar"; import { LiveRidePanel } from "@/components/LiveRidePanel"; import type { RideStatus, RidesFetchResult } from "@/lib/scrapers/sixflags"; import type { LiveRidesResult } from "@/lib/scrapers/queuetimes"; // used as prop type below -import { getTodayLocal } from "@/lib/env"; +import { getTodayLocal, isWithinOperatingWindow } from "@/lib/env"; interface PageProps { params: Promise<{ id: string }>; @@ -54,8 +54,22 @@ export default async function ParkPage({ params, searchParams }: PageProps) { let liveRides: LiveRidesResult | null = null; let ridesResult: RidesFetchResult | null = null; + // Determine if we're within the 1h-before-open to 1h-after-close window. + const withinWindow = todayData?.hoursLabel + ? isWithinOperatingWindow(todayData.hoursLabel) + : false; + if (queueTimesId) { - liveRides = await fetchLiveRides(queueTimesId, coasterSet); + const raw = await fetchLiveRides(queueTimesId, coasterSet); + if (raw) { + // Outside the window: show the ride list but force all rides closed + liveRides = withinWindow + ? raw + : { + ...raw, + rides: raw.rides.map((r) => ({ ...r, isOpen: false, waitMinutes: 0 })), + }; + } } // Only hit the schedule API as a fallback when live data is unavailable diff --git a/components/LiveRidePanel.tsx b/components/LiveRidePanel.tsx index 2aa249a..662594d 100644 --- a/components/LiveRidePanel.tsx +++ b/components/LiveRidePanel.tsx @@ -57,8 +57,8 @@ export function LiveRidePanel({ liveRides, parkOpenToday }: LiveRidePanelProps) )} - {/* Closed count badge */} - {anyOpen && closedRides.length > 0 && ( + {/* Closed count badge — always shown when there are closed rides */} + {closedRides.length > 0 && (
- {closedRides.length} closed / down + {closedRides.length} {anyOpen ? "closed / down" : "rides total"}
)} diff --git a/lib/env.ts b/lib/env.ts index 36ae2f8..186c060 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -33,3 +33,27 @@ export function getTodayLocal(): string { const d = String(now.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } + +/** + * Returns true when the current local time is within 1 hour before open + * or 1 hour after close, based on a hoursLabel like "10am – 6pm". + * Falls back to true when the label can't be parsed. + */ +export function isWithinOperatingWindow(hoursLabel: 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]); + const now = new Date(); + const nowMin = now.getHours() * 60 + now.getMinutes(); + return nowMin >= openMin - 60 && nowMin <= closeMin + 60; +}