feat: initial project scaffold with CI/CD and Docker deployment

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>
This commit is contained in:
2026-04-04 00:48:09 -04:00
parent af6aa29474
commit 548c7ae09e
26 changed files with 9602 additions and 0 deletions

123
components/CalendarGrid.tsx Normal file
View File

@@ -0,0 +1,123 @@
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>
);
}