import Link from "next/link";
import { notFound } from "next/navigation";
import { PARK_MAP } from "@/lib/parks";
import { openDb, getParkMonthData, getApiId } from "@/lib/db";
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 type { RideStatus, RidesFetchResult } from "@/lib/scrapers/sixflags";
import type { LiveRidesResult, LiveRide } from "@/lib/scrapers/queuetimes";
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ month?: string }>;
}
function parseMonthParam(param: string | undefined): { year: number; month: number } {
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 { year: y, month: m };
}
}
const now = new Date();
return { year: now.getFullYear(), month: now.getMonth() + 1 };
}
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 today = new Date().toISOString().slice(0, 10);
const { year, month } = parseMonthParam(monthParam);
const db = openDb();
const monthData = getParkMonthData(db, id, year, month);
const apiId = getApiId(db, id);
db.close();
const todayData = monthData[today];
const parkOpenToday = todayData?.isOpen && todayData?.hoursLabel;
// ── Ride data: try live Queue-Times first, fall back to schedule ──────────
const queueTimesId = QUEUE_TIMES_IDS[id];
let liveRides: LiveRidesResult | null = null;
let ridesResult: RidesFetchResult | null = null;
if (queueTimesId) {
liveRides = await fetchLiveRides(queueTimesId);
}
// Only hit the schedule API as a fallback when live data is unavailable
if (!liveRides && apiId !== null) {
// Note: the API drops today's date from its response (only returns future dates),
// so scrapeRidesForDay may fall back to the nearest upcoming date.
ridesResult = await scrapeRidesForDay(apiId, today);
}
return (
{/* ── Header ─────────────────────────────────────────────────────────── */}
← Calendar
{park.name}
{park.location.city}, {park.location.state}
{/* ── Month Calendar ───────────────────────────────────────────────── */}
{/* ── Ride Status ─────────────────────────────────────────────────── */}
Rides
{liveRides ? (
) : ridesResult && !ridesResult.isExact ? (
{formatShortDate(ridesResult.dataDate)}
) : (
Today
)}
{liveRides ? (
) : (
)}
);
}
// ── 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 }: { children: React.ReactNode }) {
return (
{children}
);
}
function LiveBadge() {
return (
Live
);
}
// ── 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 */}
);
}
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({
ridesResult,
parkOpenToday,
apiIdMissing,
}: {
ridesResult: RidesFetchResult | null;
parkOpenToday: boolean;
apiIdMissing: boolean;
}) {
if (apiIdMissing) {
return (
Park API ID not discovered yet. Run{" "}
npm run discover
{" "}
to enable ride data.
);
}
if (!parkOpenToday) {
return Park is closed today — no ride schedule available.;
}
if (!ridesResult || ridesResult.rides.length === 0) {
return Ride schedule is not yet available from the API.;
}
const { rides, isExact, dataDate, parkHoursLabel } = ridesResult;
const openRides = rides.filter((r) => r.isOpen);
const closedRides = rides.filter((r) => !r.isOpen);
return (
{/* Summary badge row */}
{openRides.length} open
{closedRides.length > 0 && (
{closedRides.length} closed / unscheduled
)}
{!isExact && (
Showing {formatShortDate(dataDate)} — live schedule updates daily
)}
{/* Two-column grid */}
{openRides.map((ride) => )}
{closedRides.map((ride) => )}
);
}
function RideRow({ ride, parkHoursLabel }: { ride: RideStatus; parkHoursLabel?: string }) {
const showHours = ride.isOpen && ride.hoursLabel && ride.hoursLabel !== parkHoursLabel;
return (
{ride.name}
{showHours && (
{ride.hoursLabel}
)}
);
}
function Callout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}