diff --git a/app/park/[id]/page.tsx b/app/park/[id]/page.tsx index b16e35b..8432647 100644 --- a/app/park/[id]/page.tsx +++ b/app/park/[id]/page.tsx @@ -6,8 +6,9 @@ import { scrapeRidesForDay } from "@/lib/scrapers/sixflags"; import { fetchLiveRides } from "@/lib/scrapers/queuetimes"; import { QUEUE_TIMES_IDS } from "@/lib/queue-times-map"; import { ParkMonthCalendar } from "@/components/ParkMonthCalendar"; +import { LiveRidePanel } from "@/components/LiveRidePanel"; import type { RideStatus, RidesFetchResult } from "@/lib/scrapers/sixflags"; -import type { LiveRidesResult, LiveRide } from "@/lib/scrapers/queuetimes"; +import type { LiveRidesResult } from "@/lib/scrapers/queuetimes"; // used as prop type below interface PageProps { params: Promise<{ id: string }>; @@ -126,7 +127,7 @@ export default async function ParkPage({ params, searchParams }: PageProps) { {liveRides ? ( - @@ -207,158 +208,6 @@ function LiveBadge() { ); } -// ── Live ride list (Queue-Times data) ────────────────────────────────────── - -function LiveRideList({ - liveRides, - parkOpenToday, -}: { - liveRides: LiveRidesResult; - parkOpenToday: boolean; -}) { - const { rides } = liveRides; - const openRides = rides.filter((r) => r.isOpen); - const closedRides = rides.filter((r) => !r.isOpen); - const anyOpen = openRides.length > 0; - - return ( -
- {/* Summary badge row */} -
- {anyOpen ? ( -
- {openRides.length} open -
- ) : ( -
- {parkOpenToday ? "Not open yet — check back soon" : "No rides open"} -
- )} - {anyOpen && closedRides.length > 0 && ( -
- {closedRides.length} closed / down -
- )} -
- - {/* Two-column grid */} -
- {openRides.map((ride) => )} - {closedRides.map((ride) => )} -
- - {/* Attribution — required by Queue-Times terms */} -
- Powered by{" "} - - Queue-Times.com - - {" "}· Updates every 5 minutes -
-
- ); -} - -function LiveRideRow({ ride }: { ride: LiveRide }) { - const showWait = ride.isOpen && ride.waitMinutes > 0; - - return ( -
-
- - - {ride.name} - -
- {showWait && ( - - {ride.waitMinutes} min - - )} - {ride.isOpen && !showWait && ( - - walk-on - - )} -
- ); -} - // ── Schedule ride list (Six Flags operating-hours API fallback) ──────────── function RideList({ diff --git a/components/LiveRidePanel.tsx b/components/LiveRidePanel.tsx new file mode 100644 index 0000000..d488dc7 --- /dev/null +++ b/components/LiveRidePanel.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { useState } from "react"; +import type { LiveRidesResult, LiveRide } from "@/lib/scrapers/queuetimes"; + +interface LiveRidePanelProps { + liveRides: LiveRidesResult; + parkOpenToday: boolean; +} + +export function LiveRidePanel({ liveRides, parkOpenToday }: LiveRidePanelProps) { + const { rides } = liveRides; + const hasCoasters = rides.some((r) => r.isCoaster); + const [coastersOnly, setCoastersOnly] = useState(false); + + const visible = coastersOnly ? rides.filter((r) => r.isCoaster) : rides; + const openRides = visible.filter((r) => r.isOpen); + const closedRides = visible.filter((r) => !r.isOpen); + const anyOpen = openRides.length > 0; + + return ( +
+ {/* ── Toolbar: summary + coaster toggle ────────────────────────────── */} +
+ {/* Open count badge */} + {anyOpen ? ( +
+ {openRides.length} open +
+ ) : ( +
+ {parkOpenToday ? "Not open yet — check back soon" : "No rides open"} +
+ )} + + {/* Closed count badge */} + {anyOpen && closedRides.length > 0 && ( +
+ {closedRides.length} closed / down +
+ )} + + {/* Coaster toggle — only shown when the park has categorised coasters */} + {hasCoasters && ( + + )} +
+ + {/* ── Ride grid ────────────────────────────────────────────────────── */} +
+ {openRides.map((ride) => )} + {closedRides.map((ride) => )} +
+ + {/* ── Attribution ──────────────────────────────────────────────────── */} +
+ Powered by{" "} + + Queue-Times.com + + {" "}· Updates every 5 minutes +
+
+ ); +} + +function RideRow({ ride }: { ride: LiveRide }) { + const showWait = ride.isOpen && ride.waitMinutes > 0; + + return ( +
+
+ + + {ride.name} + +
+ {showWait && ( + + {ride.waitMinutes} min + + )} + {ride.isOpen && !showWait && ( + + walk-on + + )} +
+ ); +} diff --git a/lib/scrapers/queuetimes.ts b/lib/scrapers/queuetimes.ts index c9c3201..d33053c 100644 --- a/lib/scrapers/queuetimes.ts +++ b/lib/scrapers/queuetimes.ts @@ -21,6 +21,8 @@ export interface LiveRide { isOpen: boolean; waitMinutes: number; lastUpdated: string; // ISO 8601 + /** True when Queue-Times placed this ride in a "Coasters" land category. */ + isCoaster: boolean; } export interface LiveRidesResult { @@ -76,8 +78,10 @@ export async function fetchLiveRides( const rides: LiveRide[] = []; - // Rides are nested inside lands + // Rides are nested inside lands. Queue-Times labels coaster sections + // with names like "Coasters", "Steel Coasters", "Wooden Coasters", etc. for (const land of json.lands ?? []) { + const isCoaster = land.name.toLowerCase().includes("coaster"); for (const r of land.rides ?? []) { if (!r.name) continue; rides.push({ @@ -85,6 +89,7 @@ export async function fetchLiveRides( isOpen: r.is_open, waitMinutes: r.wait_time ?? 0, lastUpdated: r.last_updated, + isCoaster, }); } } @@ -97,6 +102,7 @@ export async function fetchLiveRides( isOpen: r.is_open, waitMinutes: r.wait_time ?? 0, lastUpdated: r.last_updated, + isCoaster: false, }); }