4f838d99c1
Build and Deploy / Build & Push (push) Successful in 3m7s
Adds a cron-driven sampler that snapshots Queue-Times waits and Six Flags Fast Lane data every 5 minutes into a new ride_wait_samples table, and a clickable per-ride detail page at /park/[id]/ride/[slug] with Today / 7d / 30d Recharts views plus a 30d uptime pill. Rides are keyed by Queue-Times' stable qt_ride_id so renames don't fragment history. Samples store pre-bucketed local_date and local_time in the park's IANA timezone so aggregations are pure SQL and DST-safe. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts";
|
|
|
|
export interface DailyAggregate {
|
|
localDate: string;
|
|
avgWait: number | null;
|
|
maxWait: number | null;
|
|
avgFastLane: number | null;
|
|
maxFastLane: number | null;
|
|
uptimePct: number;
|
|
sampleCount: number;
|
|
}
|
|
|
|
interface Props {
|
|
data: DailyAggregate[];
|
|
hasFastLane: boolean;
|
|
mode: "regular" | "fastLane";
|
|
}
|
|
|
|
function shortDay(localDate: string): string {
|
|
// "2026-05-29" → "May 29"
|
|
const [, m, d] = localDate.split("-");
|
|
const month = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][parseInt(m, 10)];
|
|
return `${month} ${parseInt(d, 10)}`;
|
|
}
|
|
|
|
export default function WeeklyStatsChart({ data, hasFastLane, mode }: Props) {
|
|
const showFastLane = mode === "fastLane" && hasFastLane;
|
|
const chartData = data.map((d) => ({
|
|
day: shortDay(d.localDate),
|
|
avg: showFastLane
|
|
? (d.avgFastLane !== null ? Math.round(d.avgFastLane) : null)
|
|
: (d.avgWait !== null ? Math.round(d.avgWait) : null),
|
|
max: showFastLane ? d.maxFastLane : d.maxWait,
|
|
}));
|
|
|
|
return (
|
|
<div style={{ width: "100%", height: 240 }}>
|
|
<ResponsiveContainer>
|
|
<BarChart data={chartData} margin={{ top: 8, right: 16, left: 0, bottom: 4 }}>
|
|
<CartesianGrid stroke="#272727" strokeDasharray="3 3" vertical={false} />
|
|
<XAxis dataKey="day" stroke="#737373" tick={{ fontSize: 11, fill: "#737373" }} tickLine={false} />
|
|
<YAxis stroke="#737373" tick={{ fontSize: 11, fill: "#737373" }} tickLine={false} axisLine={false} />
|
|
<Tooltip
|
|
contentStyle={{
|
|
background: "#1c1c1c",
|
|
border: "1px solid #333",
|
|
borderRadius: 6,
|
|
fontSize: "0.75rem",
|
|
color: "#f5f5f5",
|
|
}}
|
|
labelStyle={{ color: "#b0b0b0" }}
|
|
formatter={(value, name) => {
|
|
if (value === null || value === undefined) return ["—", name];
|
|
return [`${value} min`, name];
|
|
}}
|
|
/>
|
|
<Legend wrapperStyle={{ fontSize: "0.72rem", paddingTop: 4 }} />
|
|
<Bar name="Avg" dataKey="avg" fill={showFastLane ? "#ff4d8d" : "#4ade80"} radius={[3, 3, 0, 0]} isAnimationActive={false} />
|
|
<Bar name="Max" dataKey="max" fill={showFastLane ? "#3d0f22" : "#0a1a0d"} stroke={showFastLane ? "#ff4d8d" : "#22c55e"} strokeWidth={1} radius={[3, 3, 0, 0]} isAnimationActive={false} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
);
|
|
}
|