Next.js 15 + Tailwind CSS v4 week calendar showing Six Flags park hours. Scrapes the internal CloudFront API, stores results in SQLite. Includes Dockerfile (Debian/Playwright-compatible), docker-compose, and Gitea Actions pipeline that builds and pushes to the container registry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
124 lines
3.9 KiB
TypeScript
124 lines
3.9 KiB
TypeScript
import type { Park } from "@/lib/scrapers/types";
|
|
|
|
interface CalendarGridProps {
|
|
parks: Park[];
|
|
calendar: Record<string, boolean[]>;
|
|
daysInMonth: number;
|
|
year: number;
|
|
month: number;
|
|
}
|
|
|
|
const DOW_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
|
|
export function CalendarGrid({
|
|
parks,
|
|
calendar,
|
|
daysInMonth,
|
|
year,
|
|
month,
|
|
}: CalendarGridProps) {
|
|
const days = Array.from({ length: daysInMonth }, (_, i) => i + 1);
|
|
const today = new Date();
|
|
const todayDay =
|
|
today.getFullYear() === year && today.getMonth() + 1 === month
|
|
? today.getDate()
|
|
: null;
|
|
|
|
return (
|
|
<div className="overflow-x-auto">
|
|
<table className="border-collapse text-sm w-full min-w-max">
|
|
<thead>
|
|
<tr>
|
|
<th
|
|
className="sticky left-0 z-10 px-3 py-2 text-left font-medium border-b min-w-40"
|
|
style={{
|
|
backgroundColor: "var(--color-bg)",
|
|
color: "var(--color-text-muted)",
|
|
borderColor: "var(--color-border)",
|
|
}}
|
|
>
|
|
Park
|
|
</th>
|
|
{days.map((day) => {
|
|
const dow = new Date(year, month - 1, day).getDay();
|
|
const isWeekend = dow === 0 || dow === 6;
|
|
const isToday = day === todayDay;
|
|
return (
|
|
<th
|
|
key={day}
|
|
className="px-1 py-2 text-center font-normal w-8 border-b"
|
|
style={{
|
|
color: isWeekend
|
|
? "var(--color-text)"
|
|
: "var(--color-text-muted)",
|
|
borderColor: "var(--color-border)",
|
|
backgroundColor: isToday
|
|
? "var(--color-surface)"
|
|
: undefined,
|
|
}}
|
|
>
|
|
<div className="text-xs">{DOW_LABELS[dow]}</div>
|
|
<div
|
|
style={
|
|
isToday
|
|
? { fontWeight: 700, color: "var(--color-open)" }
|
|
: undefined
|
|
}
|
|
>
|
|
{day}
|
|
</div>
|
|
</th>
|
|
);
|
|
})}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{parks.map((park) => {
|
|
const parkDays = calendar[park.id] ?? [];
|
|
return (
|
|
<tr key={park.id}>
|
|
<td
|
|
className="sticky left-0 z-10 px-3 py-1 text-xs border-b whitespace-nowrap"
|
|
style={{
|
|
backgroundColor: "var(--color-bg)",
|
|
color: "var(--color-text-muted)",
|
|
borderColor: "var(--color-border)",
|
|
}}
|
|
>
|
|
{park.shortName}
|
|
</td>
|
|
{days.map((day) => {
|
|
const isOpen = parkDays[day - 1] ?? false;
|
|
const isToday = day === todayDay;
|
|
return (
|
|
<td
|
|
key={day}
|
|
className="px-1 py-1 text-center border-b"
|
|
style={{
|
|
borderColor: "var(--color-border)",
|
|
backgroundColor: isToday
|
|
? "rgba(30,41,59,0.3)"
|
|
: undefined,
|
|
}}
|
|
title={`${park.shortName} — ${month}/${day}: ${isOpen ? "Open" : "Closed"}`}
|
|
>
|
|
<span
|
|
className="inline-block w-5 h-5 rounded-sm"
|
|
style={{
|
|
backgroundColor: isOpen
|
|
? "var(--color-open)"
|
|
: "var(--color-closed)",
|
|
}}
|
|
/>
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|