feat: add Fast Lane wait times toggle on park pages
Build and Deploy / Build & Push (push) Successful in 1m3s

Join Fast Lane waits from the Six Flags /wait-times endpoint onto
Queue-Times rides by name. A new toggle on the live ride panel swaps
the shown wait to the Fast Lane number; regular waits and open status
still come from Queue-Times.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 22:51:52 -04:00
parent aa46cc1b3d
commit bfe099322f
6 changed files with 455 additions and 70 deletions
+134 -53
View File
@@ -12,7 +12,9 @@ interface LiveRidePanelProps {
export function LiveRidePanel({ liveRides, parkOpenToday, isWeatherDelay }: LiveRidePanelProps) {
const { rides } = liveRides;
const hasCoasters = rides.some((r) => r.isCoaster);
const hasFastLane = rides.some((r) => r.hasFastLane);
const [coastersOnly, setCoastersOnly] = useState(false);
const [fastLaneMode, setFastLaneMode] = useState(false);
// Pre-select coaster filter if Coaster Mode is enabled on the homepage.
useEffect(() => {
@@ -21,6 +23,21 @@ export function LiveRidePanel({ liveRides, parkOpenToday, isWeatherDelay }: Live
}
}, [hasCoasters]);
// Restore Fast Lane mode from a previous visit.
useEffect(() => {
if (hasFastLane && localStorage.getItem("fastLaneMode") === "true") {
setFastLaneMode(true);
}
}, [hasFastLane]);
const toggleFastLane = () => {
setFastLaneMode((v) => {
const next = !v;
localStorage.setItem("fastLaneMode", String(next));
return next;
});
};
const visible = coastersOnly ? rides.filter((r) => r.isCoaster) : rides;
const openRides = visible.filter((r) => r.isOpen);
const closedRides = visible.filter((r) => !r.isOpen);
@@ -94,35 +111,69 @@ export function LiveRidePanel({ liveRides, parkOpenToday, isWeatherDelay }: Live
</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>
{/* Toggle group — pushed to the right */}
{(hasCoasters || hasFastLane) && (
<div style={{ marginLeft: "auto", display: "flex", gap: 8, flexWrap: "wrap" }}>
{/* Fast Lane toggle — swaps shown waits to Fast Lane numbers */}
{hasFastLane && (
<button
onClick={toggleFastLane}
style={{
display: "flex",
alignItems: "center",
gap: 5,
padding: "4px 12px",
borderRadius: 20,
border: fastLaneMode
? "1px solid var(--color-accent)"
: "1px solid var(--color-border)",
background: fastLaneMode
? "var(--color-accent-muted)"
: "var(--color-surface)",
color: fastLaneMode
? "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",
}}
>
Fast Lane
</button>
)}
{/* Coaster toggle — only shown when the park has categorised coasters */}
{hasCoasters && (
<button
onClick={() => setCoastersOnly((v) => !v)}
style={{
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>
)}
</div>
@@ -132,16 +183,18 @@ export function LiveRidePanel({ liveRides, parkOpenToday, isWeatherDelay }: Live
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} />)}
{openRides.map((ride) => <RideRow key={ride.name} ride={ride} fastLaneMode={fastLaneMode} />)}
{closedRides.map((ride) => <RideRow key={ride.name} ride={ride} fastLaneMode={fastLaneMode} />)}
</div>
</div>
);
}
function RideRow({ ride }: { ride: LiveRide }) {
function RideRow({ ride, fastLaneMode }: { ride: LiveRide; fastLaneMode: boolean }) {
const showWait = ride.isOpen && ride.waitMinutes > 0;
const fastLaneActive = fastLaneMode && ride.hasFastLane;
const flWait = ride.fastLaneMinutes ?? 0;
return (
<div style={{
@@ -174,27 +227,55 @@ function RideRow({ ride }: { ride: LiveRide }) {
{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>
{/* Fast Lane mode: swap the shown wait to the Fast Lane number */}
{fastLaneMode ? (
ride.isOpen && fastLaneActive ? (
<span style={{
fontSize: "0.72rem",
color: "var(--color-accent)",
fontWeight: 600,
flexShrink: 0,
whiteSpace: "nowrap",
}}>
{flWait > 0 ? `${flWait} min` : "⚡ walk-on"}
</span>
) : ride.isOpen ? (
<span style={{
fontSize: "0.68rem",
color: "var(--color-text-muted)",
fontWeight: 500,
flexShrink: 0,
opacity: 0.7,
whiteSpace: "nowrap",
}}>
no Fast Lane
</span>
) : null
) : (
<>
{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>
);