Files
SixFlagsSuperCalendar/components/LiveRidePanel.tsx
Josh Wright 5b575f962e
All checks were successful
Build and Deploy / Build & Push (push) Successful in 53s
feat: persistent Coaster Mode toggle in header top-right
- Moves the coaster toggle out of WeekNav and into the homepage header
  top-right as "🎢 Coaster Mode", alongside the parks open badge
- State is stored in localStorage ("coasterMode") so the preference
  persists across sessions and page refreshes
- Dropped the ?coasters=1 URL param entirely; the server always fetches
  both rideCounts and coasterCounts, and HomePageClient picks which to
  display client-side — no flash or server round-trip on toggle
- Individual park pages: LiveRidePanel reads localStorage on mount and
  pre-selects the Coasters Only filter when Coaster Mode is active
- Extracted HomePageClient (client component) to own the full homepage
  UI; page.tsx is now pure data-fetching

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 08:36:38 -04:00

188 lines
5.9 KiB
TypeScript

"use client";
import { useState, useEffect } 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);
// Pre-select coaster filter if Coaster Mode is enabled on the homepage.
useEffect(() => {
if (hasCoasters && localStorage.getItem("coasterMode") === "true") {
setCoastersOnly(true);
}
}, [hasCoasters]);
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 (
<div>
{/* ── Toolbar: summary + coaster toggle ────────────────────────────── */}
<div style={{
display: "flex",
alignItems: "center",
gap: 10,
marginBottom: 16,
flexWrap: "wrap",
}}>
{/* Open count badge */}
{anyOpen ? (
<div style={{
background: "var(--color-open-bg)",
border: "1px solid var(--color-open-border)",
borderRadius: 20,
padding: "4px 12px",
fontSize: "0.72rem",
fontWeight: 600,
color: "var(--color-open-hours)",
flexShrink: 0,
}}>
{openRides.length} open
</div>
) : (
<div style={{
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
borderRadius: 20,
padding: "4px 12px",
fontSize: "0.72rem",
fontWeight: 500,
color: "var(--color-text-muted)",
flexShrink: 0,
}}>
{parkOpenToday ? "Not open yet — check back soon" : "No rides open"}
</div>
)}
{/* Closed count badge — always shown when there are closed rides */}
{closedRides.length > 0 && (
<div style={{
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
borderRadius: 20,
padding: "4px 12px",
fontSize: "0.72rem",
fontWeight: 500,
color: "var(--color-text-muted)",
flexShrink: 0,
}}>
{closedRides.length} {anyOpen ? "closed / down" : "rides total"}
</div>
)}
{/* Coaster toggle — only shown when the park has categorised coasters */}
{hasCoasters && (
<button
onClick={() => setCoastersOnly((v) => !v)}
style={{
marginLeft: "auto",
display: "flex",
alignItems: "center",
gap: 5,
padding: "4px 12px",
borderRadius: 20,
border: coastersOnly
? "1px solid var(--color-accent)"
: "1px solid var(--color-border)",
background: coastersOnly
? "var(--color-accent-muted)"
: "var(--color-surface)",
color: coastersOnly
? "var(--color-accent)"
: "var(--color-text-muted)",
fontSize: "0.72rem",
fontWeight: 600,
cursor: "pointer",
transition: "background 150ms ease, border-color 150ms ease, color 150ms ease",
whiteSpace: "nowrap",
}}
>
🎢 Coasters only
</button>
)}
</div>
{/* ── Ride grid ────────────────────────────────────────────────────── */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
gap: 6,
}}>
{openRides.map((ride) => <RideRow key={ride.name} ride={ride} />)}
{closedRides.map((ride) => <RideRow key={ride.name} ride={ride} />)}
</div>
</div>
);
}
function RideRow({ ride }: { ride: LiveRide }) {
const showWait = ride.isOpen && ride.waitMinutes > 0;
return (
<div style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 10,
padding: "8px 12px",
background: "var(--color-surface)",
border: `1px solid ${ride.isOpen ? "var(--color-open-border)" : "var(--color-border)"}`,
borderRadius: 8,
opacity: ride.isOpen ? 1 : 0.6,
}}>
<div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
<span style={{
width: 7,
height: 7,
borderRadius: "50%",
background: ride.isOpen ? "var(--color-open-text)" : "var(--color-text-dim)",
flexShrink: 0,
}} />
<span title={ride.name} style={{
fontSize: "0.8rem",
color: ride.isOpen ? "var(--color-text)" : "var(--color-text-muted)",
fontWeight: ride.isOpen ? 500 : 400,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}>
{ride.name}
</span>
</div>
{showWait && (
<span style={{
fontSize: "0.72rem",
color: "var(--color-open-hours)",
fontWeight: 600,
flexShrink: 0,
whiteSpace: "nowrap",
}}>
{ride.waitMinutes} min
</span>
)}
{ride.isOpen && !showWait && (
<span style={{
fontSize: "0.68rem",
color: "var(--color-open-text)",
fontWeight: 500,
flexShrink: 0,
opacity: 0.7,
}}>
walk-on
</span>
)}
</div>
);
}