feat: park page operating window check; always show ride total
All checks were successful
Build and Deploy / Build & Push (push) Successful in 5m54s
All checks were successful
Build and Deploy / Build & Push (push) Successful in 5m54s
- 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 <noreply@anthropic.com>
This commit is contained in:
25
app/page.tsx
25
app/page.tsx
@@ -5,7 +5,7 @@ import { Legend } from "@/components/Legend";
|
|||||||
import { EmptyState } from "@/components/EmptyState";
|
import { EmptyState } from "@/components/EmptyState";
|
||||||
import { PARKS, groupByRegion } from "@/lib/parks";
|
import { PARKS, groupByRegion } from "@/lib/parks";
|
||||||
import { openDb, getDateRange } from "@/lib/db";
|
import { openDb, getDateRange } from "@/lib/db";
|
||||||
import { getTodayLocal } from "@/lib/env";
|
import { getTodayLocal, isWithinOperatingWindow } from "@/lib/env";
|
||||||
import { fetchLiveRides } from "@/lib/scrapers/queuetimes";
|
import { fetchLiveRides } from "@/lib/scrapers/queuetimes";
|
||||||
import { QUEUE_TIMES_IDS } from "@/lib/queue-times-map";
|
import { QUEUE_TIMES_IDS } from "@/lib/queue-times-map";
|
||||||
|
|
||||||
@@ -13,29 +13,6 @@ interface PageProps {
|
|||||||
searchParams: Promise<{ week?: string }>;
|
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 {
|
function getWeekStart(param: string | undefined): string {
|
||||||
if (param && /^\d{4}-\d{2}-\d{2}$/.test(param)) {
|
if (param && /^\d{4}-\d{2}-\d{2}$/.test(param)) {
|
||||||
const d = new Date(param + "T00:00:00");
|
const d = new Date(param + "T00:00:00");
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ParkMonthCalendar } from "@/components/ParkMonthCalendar";
|
|||||||
import { LiveRidePanel } from "@/components/LiveRidePanel";
|
import { LiveRidePanel } from "@/components/LiveRidePanel";
|
||||||
import type { RideStatus, RidesFetchResult } from "@/lib/scrapers/sixflags";
|
import type { RideStatus, RidesFetchResult } from "@/lib/scrapers/sixflags";
|
||||||
import type { LiveRidesResult } from "@/lib/scrapers/queuetimes"; // used as prop type below
|
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 {
|
interface PageProps {
|
||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
@@ -54,8 +54,22 @@ export default async function ParkPage({ params, searchParams }: PageProps) {
|
|||||||
let liveRides: LiveRidesResult | null = null;
|
let liveRides: LiveRidesResult | null = null;
|
||||||
let ridesResult: RidesFetchResult | 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) {
|
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
|
// Only hit the schedule API as a fallback when live data is unavailable
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ export function LiveRidePanel({ liveRides, parkOpenToday }: LiveRidePanelProps)
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Closed count badge */}
|
{/* Closed count badge — always shown when there are closed rides */}
|
||||||
{anyOpen && closedRides.length > 0 && (
|
{closedRides.length > 0 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: "var(--color-surface)",
|
background: "var(--color-surface)",
|
||||||
border: "1px solid var(--color-border)",
|
border: "1px solid var(--color-border)",
|
||||||
@@ -69,7 +69,7 @@ export function LiveRidePanel({ liveRides, parkOpenToday }: LiveRidePanelProps)
|
|||||||
color: "var(--color-text-muted)",
|
color: "var(--color-text-muted)",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
{closedRides.length} closed / down
|
{closedRides.length} {anyOpen ? "closed / down" : "rides total"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
24
lib/env.ts
24
lib/env.ts
@@ -33,3 +33,27 @@ export function getTodayLocal(): string {
|
|||||||
const d = String(now.getDate()).padStart(2, "0");
|
const d = String(now.getDate()).padStart(2, "0");
|
||||||
return `${y}-${m}-${d}`;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user