refactor: make frontend a pure presentation layer fetching from backend API

Server components now fetch composed data from the backend instead of
directly querying SQLite and external APIs. Removes better-sqlite3
dependency from the frontend entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 21:43:59 -04:00
parent ccd35c4648
commit 3815da2d3f
15 changed files with 55 additions and 1320 deletions
+31 -61
View File
@@ -1,33 +1,27 @@
import Link from "next/link";
import { BackToCalendarLink } from "@/components/BackToCalendarLink";
import { notFound } from "next/navigation";
import { PARK_MAP } from "@/lib/parks";
import { openDb, getParkMonthData } from "@/lib/db";
import { scrapeRidesForDay } from "@/lib/scrapers/sixflags";
import { fetchLiveRides } from "@/lib/scrapers/queuetimes";
import { fetchToday } from "@/lib/scrapers/sixflags";
import { QUEUE_TIMES_IDS } from "@/lib/queue-times-map";
import { getCoasterSet } from "@/lib/coaster-data";
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"; // used as prop type below
import { getTodayLocal, isWithinOperatingWindow } from "@/lib/env";
import type { LiveRidesResult } from "@/lib/scrapers/queuetimes";
import { getTodayLocal } from "@/lib/env";
const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:3001";
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ month?: string }>;
}
function parseMonthParam(param: string | undefined): { year: number; month: number } {
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 { year: y, month: m };
return param;
}
}
const [y, m] = getTodayLocal().split("-").map(Number);
return { year: y, month: m };
return getTodayLocal().slice(0, 7);
}
export default async function ParkPage({ params, searchParams }: PageProps) {
@@ -37,54 +31,30 @@ export default async function ParkPage({ params, searchParams }: PageProps) {
const park = PARK_MAP.get(id);
if (!park) notFound();
const today = getTodayLocal();
const { year, month } = parseMonthParam(monthParam);
const monthStr = parseMonthParam(monthParam);
const [year, month] = monthStr.split("-").map(Number);
const db = openDb();
const monthData = getParkMonthData(db, id, year, month);
db.close();
const [calendarData, ridesData] = await Promise.all([
fetch(`${BACKEND_URL}/api/calendar/${id}/month?month=${monthStr}`, {
next: { revalidate: 300 },
}).then((r) => r.json()),
fetch(`${BACKEND_URL}/api/parks/${id}/rides`, {
next: { revalidate: 60 },
}).then((r) => r.json()),
]);
const liveToday = await fetchToday(park.apiId, 300).catch(() => null);
const todayData = liveToday
? { isOpen: liveToday.isOpen, hoursLabel: liveToday.hoursLabel ?? null, specialType: liveToday.specialType ?? null }
: 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];
const coasterSet = getCoasterSet(id);
let liveRides: LiveRidesResult | null = null;
let ridesResult: RidesFetchResult | null = null;
// Determine if we're within the 1h-before-open to 1h-after-close window.
const withinWindow = todayData?.hoursLabel
? isWithinOperatingWindow(todayData.hoursLabel, park.timezone)
: false;
if (queueTimesId) {
const raw = await fetchLiveRides(queueTimesId, coasterSet);
if (raw) {
// Outside the window: show the ride list but force all rides closed
liveRides = withinWindow
? raw
: {
...raw,
rides: raw.rides.map((r) => ({ ...r, isOpen: false, waitMinutes: 0 })),
};
}
}
// Weather delay: park is within operating hours but queue-times shows 0 open rides
const isWeatherDelay =
withinWindow &&
liveRides !== null &&
liveRides.rides.length > 0 &&
liveRides.rides.every((r) => !r.isOpen);
if (!liveRides) {
ridesResult = await scrapeRidesForDay(park.apiId, today);
}
const { monthData, today } = calendarData;
const {
parkOpenToday,
isWeatherDelay,
liveRides,
scheduleFallback: ridesResult,
}: {
parkOpenToday: boolean;
isWeatherDelay: boolean;
liveRides: LiveRidesResult | null;
scheduleFallback: RidesFetchResult | null;
} = ridesData;
return (
<div style={{ minHeight: "100vh", background: "var(--color-bg)" }}>
@@ -162,13 +132,13 @@ export default async function ParkPage({ params, searchParams }: PageProps) {
{liveRides ? (
<LiveRidePanel
liveRides={liveRides}
parkOpenToday={!!parkOpenToday}
parkOpenToday={parkOpenToday}
isWeatherDelay={isWeatherDelay}
/>
) : (
<RideList
ridesResult={ridesResult}
parkOpenToday={!!parkOpenToday}
parkOpenToday={parkOpenToday}
/>
)}
</section>