feat: UI redesign with park detail pages and ride status
Some checks failed
Build and Deploy / Build & Push (push) Failing after 22s
Some checks failed
Build and Deploy / Build & Push (push) Failing after 22s
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>
This commit is contained in:
170
components/ParkCard.tsx
Normal file
170
components/ParkCard.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user