From 819e7161972fda6ecc789b442cbf520ca42e02ad Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 4 Apr 2026 12:56:20 -0400 Subject: [PATCH] feat: coaster-only toggle on live ride status panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Queue-Times groups rides into lands (e.g. "Coasters", "Family", "Kids"). Capture that categorisation in LiveRide.isCoaster and surface it as a toggle in the new LiveRidePanel client component. - lib/scrapers/queuetimes.ts: add isCoaster: boolean to LiveRide, derived from land.name.toLowerCase().includes("coaster") - components/LiveRidePanel.tsx: client component replacing the old inline LiveRideList; adds a "๐ŸŽข Coasters only" pill toggle that filters the grid; toggle only appears when the park has coaster- categorised rides; amber when active, muted when inactive - app/park/[id]/page.tsx: swap LiveRideList for LiveRidePanel, remove now-dead LiveRideList/LiveRideRow functions Co-Authored-By: Claude Sonnet 4.6 --- app/park/[id]/page.tsx | 157 +-------------------------- components/LiveRidePanel.tsx | 200 +++++++++++++++++++++++++++++++++++ lib/scrapers/queuetimes.ts | 8 +- 3 files changed, 210 insertions(+), 155 deletions(-) create mode 100644 components/LiveRidePanel.tsx 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, }); }