Files
SixFlagsSuperCalendar/components/ParkCard.tsx
josh e48038c399
Some checks failed
Build and Deploy / Build & Push (push) Failing after 22s
feat: UI redesign with park detail pages and ride status
Visual overhaul:
- Warmer color system with amber accent for Today, better text hierarchy
- Row hover highlighting, sticky column shadow on horizontal scroll
- Closed cells replaced with dot (·) instead of "Closed" text
- Regional grouping (Northeast/Southeast/Midwest/Texas & South/West)
- Two-row header with park count badge and WeekNav on separate lines
- Amber "Today" button in WeekNav when off current week
- Mobile card layout (< 1024px) with 7-day grid per park; table on desktop
- Skeleton loading state via app/loading.tsx

Park detail pages (/park/[id]):
- Month calendar view with ← → navigation via ?month= param
- Live ride status fetched from Six Flags API (cached 1h)
- Ride hours only shown when they differ from park operating hours
- Fallback to nearest upcoming open day when today is dropped by API,
  including cross-month fallback for end-of-month edge case

Data layer:
- Park type gains region field; parks.ts exports groupByRegion()
- db.ts gains getParkMonthData() for single-park month queries
- sixflags.ts gains scrapeRidesForDay() returning RidesFetchResult
  with rides, dataDate, isExact, and parkHoursLabel

Removed: CalendarGrid.tsx, MonthNav.tsx (dead code)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:53:06 -04:00

171 lines
5.6 KiB
TypeScript

import Link from "next/link";
import type { Park } from "@/lib/scrapers/types";
import type { DayData } from "@/lib/db";
interface ParkCardProps {
park: Park;
weekDates: string[]; // 7 dates YYYY-MM-DD
parkData: Record<string, DayData>;
today: string;
}
const DOW_SHORT = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
function parseDate(iso: string) {
const d = new Date(iso + "T00:00:00");
return { day: d.getDate(), dow: d.getDay(), isWeekend: d.getDay() === 0 || d.getDay() === 6 };
}
export function ParkCard({ park, weekDates, parkData, today }: ParkCardProps) {
return (
<div
data-park={park.name.toLowerCase()}
style={{
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
borderRadius: 12,
padding: "14px 14px 12px",
display: "flex",
flexDirection: "column",
gap: 10,
}}
>
{/* Park name + location */}
<div>
<Link href={`/park/${park.id}`} className="park-name-link">
<span style={{ fontWeight: 600, fontSize: "0.9rem", lineHeight: 1.2 }}>
{park.name}
</span>
</Link>
<div style={{ fontSize: "0.7rem", color: "var(--color-text-muted)", marginTop: 2 }}>
{park.location.city}, {park.location.state}
</div>
</div>
{/* 7-day grid */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 4,
}}>
{weekDates.map((date) => {
const pd = parseDate(date);
const isToday = date === today;
const dayData = parkData[date];
const isOpen = dayData?.isOpen && dayData?.hoursLabel;
const isPH = dayData?.specialType === "passholder_preview";
let cellBg = "transparent";
let cellBorder = "1px solid var(--color-border-subtle)";
let cellBorderRadius = "6px";
if (isToday) {
cellBg = "var(--color-today-bg)";
cellBorder = `1px solid var(--color-today-border)`;
} else if (pd.isWeekend) {
cellBg = "var(--color-weekend-header)";
}
return (
<div key={date} style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 3,
padding: "6px 2px",
background: cellBg,
border: cellBorder,
borderRadius: cellBorderRadius,
minWidth: 0,
}}>
{/* Day name */}
<span style={{
fontSize: "0.6rem",
textTransform: "uppercase",
letterSpacing: "0.04em",
color: isToday ? "var(--color-today-text)" : pd.isWeekend ? "var(--color-text-secondary)" : "var(--color-text-muted)",
fontWeight: isToday || pd.isWeekend ? 600 : 400,
}}>
{DOW_SHORT[pd.dow]}
</span>
{/* Date number */}
<span style={{
fontSize: "0.8rem",
fontWeight: isToday ? 700 : 500,
color: isToday ? "var(--color-today-text)" : pd.isWeekend ? "var(--color-text)" : "var(--color-text-secondary)",
lineHeight: 1,
}}>
{pd.day}
</span>
{/* Status */}
{!dayData ? (
<span style={{ fontSize: "0.65rem", color: "var(--color-text-dim)", lineHeight: 1 }}></span>
) : isPH && isOpen ? (
<span style={{
fontSize: "0.6rem",
fontWeight: 700,
color: "var(--color-ph-label)",
letterSpacing: "0.02em",
textAlign: "center",
lineHeight: 1.2,
}}>
PH
</span>
) : isOpen ? (
<span style={{
fontSize: "0.58rem",
fontWeight: 600,
color: "var(--color-open-text)",
lineHeight: 1,
textAlign: "center",
}}>
Open
</span>
) : (
<span style={{ fontSize: "0.9rem", color: "var(--color-text-dim)", lineHeight: 1 }}>·</span>
)}
</div>
);
})}
</div>
{/* Hours detail row — show the open day hours inline */}
{weekDates.some((d) => parkData[d]?.isOpen && parkData[d]?.hoursLabel) && (
<div style={{
display: "flex",
flexWrap: "wrap",
gap: "4px 12px",
paddingTop: 4,
borderTop: "1px solid var(--color-border-subtle)",
}}>
{weekDates.map((date) => {
const pd = parseDate(date);
const dayData = parkData[date];
if (!dayData?.isOpen || !dayData?.hoursLabel) return null;
const isPH = dayData.specialType === "passholder_preview";
return (
<span key={date} style={{
fontSize: "0.68rem",
color: isPH ? "var(--color-ph-hours)" : "var(--color-open-hours)",
display: "flex",
gap: 4,
alignItems: "center",
}}>
<span style={{ color: "var(--color-text-muted)", fontWeight: 600 }}>
{DOW_SHORT[pd.dow]}
</span>
{dayData.hoursLabel}
{isPH && (
<span style={{ color: "var(--color-ph-label)", fontSize: "0.6rem", fontWeight: 700 }}>PH</span>
)}
</span>
);
})}
</div>
)}
</div>
);
}