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>
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts";
|
|
|
|
export interface TodaySample {
|
|
localTime: string;
|
|
isOpen: boolean;
|
|
waitMinutes: number | null;
|
|
fastLaneMinutes: number | null;
|
|
}
|
|
|
|
interface Props {
|
|
samples: TodaySample[];
|
|
hasFastLane: boolean;
|
|
}
|
|
|
|
export default function WaitTimeTodayChart({ samples, hasFastLane }: Props) {
|
|
// Map samples: closed periods → null so Recharts breaks the line.
|
|
const data = samples.map((s) => ({
|
|
time: s.localTime,
|
|
wait: s.isOpen ? s.waitMinutes : null,
|
|
fl: s.isOpen ? s.fastLaneMinutes : null,
|
|
}));
|
|
|
|
// Show every Nth tick on the X axis so labels don't overlap.
|
|
const tickInterval = Math.max(1, Math.floor(data.length / 8));
|
|
|
|
return (
|
|
<div style={{ width: "100%", height: 280 }}>
|
|
<ResponsiveContainer>
|
|
<LineChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 4 }}>
|
|
<CartesianGrid stroke="#272727" strokeDasharray="3 3" vertical={false} />
|
|
<XAxis
|
|
dataKey="time"
|
|
stroke="#737373"
|
|
tick={{ fontSize: 11, fill: "#737373" }}
|
|
interval={tickInterval}
|
|
tickLine={false}
|
|
/>
|
|
<YAxis
|
|
stroke="#737373"
|
|
tick={{ fontSize: 11, fill: "#737373" }}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
label={{ value: "min", position: "insideLeft", angle: -90, offset: 16, style: { fontSize: 10, fill: "#737373" } }}
|
|
/>
|
|
<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 }} />
|
|
<Line
|
|
name="Wait"
|
|
type="monotone"
|
|
dataKey="wait"
|
|
stroke="#4ade80"
|
|
strokeWidth={2}
|
|
dot={false}
|
|
connectNulls={false}
|
|
isAnimationActive={false}
|
|
/>
|
|
{hasFastLane && (
|
|
<Line
|
|
name="Fast Lane"
|
|
type="monotone"
|
|
dataKey="fl"
|
|
stroke="#ff4d8d"
|
|
strokeWidth={2}
|
|
dot={false}
|
|
connectNulls={false}
|
|
isAnimationActive={false}
|
|
/>
|
|
)}
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
);
|
|
}
|