Files
SixFlagsSuperCalendar/app/page.tsx
T
josh 4652a92c29 refactor: hardcode API IDs and coaster lists, remove Playwright discovery
Embed Six Flags API IDs directly in the park registry and snapshot
coaster lists from park-meta.json into a TypeScript module. This
eliminates the Playwright-based discovery script, RCDB scraper, and
runtime dependency on park-meta.json — preparing for the backend
API transition.

- Add apiId field to Park type and all 24 park entries
- Create lib/coaster-data.ts with hardcoded coaster lists
- Update page components to use park.apiId and new getCoasterSet()
- Remove scripts/discover.ts, lib/scrapers/rcdb.ts, lib/park-meta.ts
- Remove data/park-meta.json from shared volume
- Remove playwright devDependency and discover npm script
- Simplify scripts/scrape.ts (no RCDB, no discovery checks)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:25:53 -04:00

147 lines
5.3 KiB
TypeScript

import { HomePageClient } from "@/components/HomePageClient";
import { PARKS } from "@/lib/parks";
import { openDb, getDateRange } from "@/lib/db";
import { getTodayLocal, isWithinOperatingWindow, getOperatingStatus } from "@/lib/env";
import { fetchLiveRides } from "@/lib/scrapers/queuetimes";
import { fetchToday } from "@/lib/scrapers/sixflags";
import { QUEUE_TIMES_IDS } from "@/lib/queue-times-map";
import { getCoasterSet, hasCoasterData } from "@/lib/coaster-data";
import type { DayData } from "@/lib/db";
interface PageProps {
searchParams: Promise<{ week?: string }>;
}
function getWeekStart(param: string | undefined): string {
if (param && /^\d{4}-\d{2}-\d{2}$/.test(param)) {
const d = new Date(param + "T00:00:00");
if (!isNaN(d.getTime())) {
d.setDate(d.getDate() - d.getDay());
return d.toISOString().slice(0, 10);
}
}
const todayIso = getTodayLocal();
const d = new Date(todayIso + "T00:00:00");
d.setDate(d.getDate() - d.getDay());
return d.toISOString().slice(0, 10);
}
function getWeekDates(sundayIso: string): string[] {
return Array.from({ length: 7 }, (_, i) => {
const d = new Date(sundayIso + "T00:00:00");
d.setDate(d.getDate() + i);
return d.toISOString().slice(0, 10);
});
}
function getCurrentWeekStart(): string {
const todayIso = getTodayLocal();
const d = new Date(todayIso + "T00:00:00");
d.setDate(d.getDate() - d.getDay());
return d.toISOString().slice(0, 10);
}
export default async function HomePage({ searchParams }: PageProps) {
const params = await searchParams;
const weekStart = getWeekStart(params.week);
const weekDates = getWeekDates(weekStart);
const endDate = weekDates[6];
const today = getTodayLocal();
const isCurrentWeek = weekStart === getCurrentWeekStart();
const db = openDb();
const data = getDateRange(db, weekStart, endDate);
// Merge live today data from the Six Flags API (dateless endpoint, 5-min ISR cache).
// This ensures weather delays, early closures, and hour changes surface within 5 minutes
// without waiting for the next scheduled scrape. Only fetched when viewing the current week.
if (weekDates.includes(today)) {
const todayResults = await Promise.all(
PARKS.map(async (p) => {
const live = await fetchToday(p.apiId, 300); // 5-min ISR cache
return live ? { parkId: p.id, live } : null;
})
);
for (const result of todayResults) {
if (!result) continue;
const { parkId, live } = result;
if (!data[parkId]) data[parkId] = {};
data[parkId][today] = {
isOpen: live.isOpen,
hoursLabel: live.hoursLabel ?? null,
specialType: live.specialType ?? null,
} satisfies DayData;
}
}
db.close();
const scrapedCount = Object.values(data).reduce(
(sum, parkData) => sum + Object.keys(parkData).length,
0
);
const coasterDataAvailable = hasCoasterData();
let rideCounts: Record<string, number> = {};
let coasterCounts: Record<string, number> = {};
let closingParkIds: string[] = [];
let openParkIds: string[] = [];
let weatherDelayParkIds: string[] = [];
if (weekDates.includes(today)) {
// Parks within operating hours right now (for open dot — independent of ride counts)
const openTodayParks = PARKS.filter((p) => {
const dayData = data[p.id]?.[today];
if (!dayData?.isOpen || !dayData.hoursLabel) return false;
return isWithinOperatingWindow(dayData.hoursLabel, p.timezone);
});
openParkIds = openTodayParks.map((p) => p.id);
closingParkIds = openTodayParks
.filter((p) => {
const dayData = data[p.id]?.[today];
return dayData?.hoursLabel
? getOperatingStatus(dayData.hoursLabel, p.timezone) === "closing"
: false;
})
.map((p) => p.id);
// Only fetch ride counts for parks that have queue-times coverage
const trackedParks = openTodayParks.filter((p) => QUEUE_TIMES_IDS[p.id]);
const results = await Promise.all(
trackedParks.map(async (p) => {
const coasterSet = getCoasterSet(p.id);
const result = await fetchLiveRides(QUEUE_TIMES_IDS[p.id], coasterSet, 300);
const rideCount = result ? result.rides.filter((r) => r.isOpen).length : null;
const coasterCount = result ? result.rides.filter((r) => r.isOpen && r.isCoaster).length : 0;
return { id: p.id, rideCount, coasterCount };
})
);
// Parks with queue-times coverage but 0 open rides = likely weather delay
weatherDelayParkIds = results
.filter(({ rideCount }) => rideCount === 0)
.map(({ id }) => id);
rideCounts = Object.fromEntries(
results.filter(({ rideCount }) => rideCount != null && rideCount > 0).map(({ id, rideCount }) => [id, rideCount!])
);
coasterCounts = Object.fromEntries(
results.filter(({ coasterCount }) => coasterCount > 0).map(({ id, coasterCount }) => [id, coasterCount])
);
}
return (
<HomePageClient
weekStart={weekStart}
weekDates={weekDates}
today={today}
isCurrentWeek={isCurrentWeek}
data={data}
rideCounts={rideCounts}
coasterCounts={coasterCounts}
openParkIds={openParkIds}
closingParkIds={closingParkIds}
weatherDelayParkIds={weatherDelayParkIds}
hasCoasterData={coasterDataAvailable}
scrapedCount={scrapedCount}
/>
);
}