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:
123
components/CalendarGrid.tsx
Normal file
123
components/CalendarGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user