Files
SixFlagsSuperCalendar/app/park/[id]/page.tsx
T
josh 0dc84c7597
Build and Deploy / Lint, typecheck, test (push) Successful in 33s
Build and Deploy / Build & Push (push) Successful in 1m15s
fix: bypass Data Cache on live park/ride pages so navigation shows fresh data
The ride detail and park pages fetched with `next: { revalidate: 60 }`,
which is stale-while-revalidate. After hours of no traffic the Data Cache
held a morning snapshot; the first click served that stale value and only
the second request (e.g. a browser refresh) got the just-revalidated
payload. The endpoint also bundles live state with chart history, so one
stale fetch made the whole page wrong.

Switch the live-data fetches to `cache: "no-store"`. The calendar-month
fetch keeps its 5-min ISR since operating hours change slowly.
2026-06-01 21:06:58 -04:00

404 lines
13 KiB
TypeScript

import type { Metadata } from "next";
import { BackToCalendarLink } from "@/components/BackToCalendarLink";
import { notFound } from "next/navigation";
import { PARK_MAP } from "@/lib/parks";
import { ParkMonthCalendar } from "@/components/ParkMonthCalendar";
import { LiveRidePanel } from "@/components/LiveRidePanel";
import type { RideStatus, RidesFetchResult } from "@/lib/scrapers/sixflags";
import type { LiveRidesResult } from "@/lib/scrapers/queuetimes";
import { getTodayLocal } from "@/lib/env";
import { apiFetch } from "@/lib/api";
import type { DayData } from "@/lib/types";
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ month?: string }>;
}
interface CalendarMonthResponse {
parkId: string;
year: number;
month: number;
monthData: Record<string, DayData>;
today: string;
}
interface RidesResponse {
parkId: string;
today: string;
parkOpenToday: boolean;
withinWindow: boolean;
isWeatherDelay: boolean;
liveRides: LiveRidesResult | null;
scheduleFallback: RidesFetchResult | null;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { id } = await params;
const park = PARK_MAP.get(id);
if (!park) return { title: "Park not found | Thoosie Calendar" };
const title = `${park.name} | Thoosie Calendar`;
const description = `Operating hours and live ride status for ${park.name} (${park.location.city}, ${park.location.state}).`;
return {
title,
description,
openGraph: { title, description },
};
}
function parseMonthParam(param: string | undefined): string {
if (param && /^\d{4}-\d{2}$/.test(param)) {
const [y, m] = param.split("-").map(Number);
if (y >= 2020 && y <= 2030 && m >= 1 && m <= 12) {
return param;
}
}
return getTodayLocal().slice(0, 7);
}
export default async function ParkPage({ params, searchParams }: PageProps) {
const { id } = await params;
const { month: monthParam } = await searchParams;
const park = PARK_MAP.get(id);
if (!park) notFound();
const monthStr = parseMonthParam(monthParam);
const [year, month] = monthStr.split("-").map(Number);
const [calendarData, ridesData] = await Promise.all([
apiFetch<CalendarMonthResponse>(
`/api/calendar/${id}/month?month=${monthStr}`,
{ revalidate: 300 },
),
apiFetch<RidesResponse>(
`/api/parks/${id}/rides`,
{ noStore: true },
),
]);
if (!calendarData) {
return <DataUnavailable parkName={park.name} />;
}
const { monthData, today } = calendarData;
const parkOpenToday = ridesData?.parkOpenToday ?? false;
const isWeatherDelay = ridesData?.isWeatherDelay ?? false;
const liveRides = ridesData?.liveRides ?? null;
const ridesResult = ridesData?.scheduleFallback ?? null;
return (
<div style={{ minHeight: "100vh", background: "var(--color-bg)" }}>
{/* ── Header ─────────────────────────────────────────────────────────── */}
<header style={{
position: "sticky",
top: 0,
zIndex: 20,
background: "var(--color-bg)",
borderBottom: "1px solid var(--color-border)",
padding: "12px 24px",
display: "flex",
alignItems: "center",
gap: 16,
}}>
<BackToCalendarLink />
<div style={{ width: 1, height: 16, background: "var(--color-border)" }} />
<span style={{ fontSize: "0.9rem", fontWeight: 600, color: "var(--color-text)", letterSpacing: "-0.01em" }}>
{park.name}
</span>
<span style={{ fontSize: "0.75rem", color: "var(--color-text-muted)" }}>
{park.location.city}, {park.location.state}
</span>
</header>
<main style={{ padding: "24px 32px", maxWidth: 1280, margin: "0 auto", display: "flex", flexDirection: "column", gap: 40 }}>
{/* ── Month Calendar ───────────────────────────────────────────────── */}
<section>
<ParkMonthCalendar
parkId={id}
year={year}
month={month}
monthData={monthData}
today={today}
timezone={park.timezone}
/>
</section>
{/* ── Ride Status ─────────────────────────────────────────────────── */}
<section>
<SectionHeading aside={liveRides ? (
<span style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap", justifyContent: "flex-end" }}>
<a
href="https://queue-times.com"
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: "0.68rem",
color: "var(--color-text-dim)",
textDecoration: "none",
display: "flex",
alignItems: "center",
gap: 4,
transition: "color 120ms ease",
}}
className="park-name-link"
>
via queue-times.com
</a>
{liveRides.rides.some((r) => r.hasFastLane) && (
<span style={{ fontSize: "0.68rem", color: "var(--color-text-dim)" }}>
· Fast Lane via sixflags.com
</span>
)}
</span>
) : undefined}>
Rides
{liveRides ? (
<LiveBadge />
) : ridesResult && !ridesResult.isExact ? (
<span style={{ fontSize: "0.72rem", fontWeight: 400, color: "var(--color-text-muted)", marginLeft: 8 }}>
{formatShortDate(ridesResult.dataDate)}
</span>
) : (
<span style={{ fontSize: "0.72rem", fontWeight: 400, color: "var(--color-text-muted)", marginLeft: 8 }}>
Today
</span>
)}
</SectionHeading>
{liveRides ? (
<LiveRidePanel
parkId={id}
liveRides={liveRides}
parkOpenToday={parkOpenToday}
isWeatherDelay={isWeatherDelay}
/>
) : (
<RideList
ridesResult={ridesResult}
parkOpenToday={parkOpenToday}
/>
)}
</section>
</main>
</div>
);
}
// ── Helpers ────────────────────────────────────────────────────────────────
function formatShortDate(iso: string): string {
return new Date(iso + "T00:00:00").toLocaleDateString("en-US", {
weekday: "short", month: "short", day: "numeric",
});
}
// ── Sub-components ─────────────────────────────────────────────────────────
function SectionHeading({ children, aside }: { children: React.ReactNode; aside?: React.ReactNode }) {
return (
<div style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 14,
paddingBottom: 10,
borderBottom: "1px solid var(--color-border)",
}}>
<h2 style={{
fontSize: "0.85rem",
fontWeight: 700,
color: "var(--color-text)",
letterSpacing: "0.04em",
textTransform: "uppercase",
margin: 0,
display: "flex",
alignItems: "center",
}}>
{children}
</h2>
{aside}
</div>
);
}
function LiveBadge() {
return (
<span style={{
display: "inline-flex",
alignItems: "center",
gap: 5,
marginLeft: 10,
padding: "2px 8px",
borderRadius: 20,
background: "var(--color-open-bg)",
border: "1px solid var(--color-open-border)",
fontSize: "0.65rem",
fontWeight: 700,
letterSpacing: "0.06em",
textTransform: "uppercase",
color: "var(--color-open-text)",
verticalAlign: "middle",
}}>
<span style={{
width: 5,
height: 5,
borderRadius: "50%",
background: "var(--color-open-text)",
display: "inline-block",
}} />
Live
</span>
);
}
// ── Schedule ride list (Six Flags operating-hours API fallback) ────────────
function RideList({
ridesResult,
parkOpenToday,
}: {
ridesResult: RidesFetchResult | null;
parkOpenToday: boolean;
}) {
if (!parkOpenToday) {
return <Callout>Park is closed today no ride schedule available.</Callout>;
}
if (!ridesResult || ridesResult.rides.length === 0) {
return <Callout>Ride schedule is not yet available from the API.</Callout>;
}
const { rides, isExact, dataDate, parkHoursLabel } = ridesResult;
const openRides = rides.filter((r) => r.isOpen);
const closedRides = rides.filter((r) => !r.isOpen);
return (
<div>
{/* Summary badge row */}
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16, flexWrap: "wrap" }}>
<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)",
}}>
{openRides.length} open
</div>
{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)",
}}>
{closedRides.length} closed / unscheduled
</div>
)}
{!isExact && (
<span style={{ fontSize: "0.7rem", color: "var(--color-text-dim)" }}>
Showing {formatShortDate(dataDate)} live schedule updates daily
</span>
)}
</div>
{/* Two-column grid */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
gap: 6,
}}>
{openRides.map((ride) => <RideRow key={ride.name} ride={ride} parkHoursLabel={parkHoursLabel} />)}
{closedRides.map((ride) => <RideRow key={ride.name} ride={ride} parkHoursLabel={parkHoursLabel} />)}
</div>
</div>
);
}
function RideRow({ ride, parkHoursLabel }: { ride: RideStatus; parkHoursLabel?: string }) {
const showHours = ride.isOpen && ride.hoursLabel && ride.hoursLabel !== parkHoursLabel;
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>
{showHours && (
<span style={{
fontSize: "0.72rem",
color: "var(--color-open-hours)",
fontWeight: 500,
flexShrink: 0,
whiteSpace: "nowrap",
}}>
{ride.hoursLabel}
</span>
)}
</div>
);
}
function Callout({ children }: { children: React.ReactNode }) {
return (
<div style={{
padding: "14px 18px",
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
borderRadius: 8,
fontSize: "0.82rem",
color: "var(--color-text-muted)",
lineHeight: 1.5,
}}>
{children}
</div>
);
}
function DataUnavailable({ parkName }: { parkName: string }) {
return (
<main style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24, background: "var(--color-bg)" }}>
<div style={{ maxWidth: 480, textAlign: "center" }}>
<h1 style={{ fontSize: "1.2rem", fontWeight: 700, color: "var(--color-text)", marginBottom: 12 }}>
{parkName} data is unavailable
</h1>
<p style={{ color: "var(--color-text-muted)", lineHeight: 1.6, fontSize: "0.9rem" }}>
We could not reach the backend. Refresh in a moment.
</p>
</div>
</main>
);
}