All checks were successful
Build and Deploy / Build & Push (push) Successful in 4m22s
- isWithinOperatingWindow now accepts an IANA timezone and reads the current time in the park's local timezone via Intl.DateTimeFormat, fixing false positives when the server runs in UTC but parks store hours in local time (e.g. Pacific parks showing open at 6:50 AM EDT) - Remove the 1-hour pre-open buffer so parks are not marked open before their doors actually open; retain the 1-hour post-close grace period - Add getTimezoneAbbr() helper to derive the short tz label (EDT, PDT…) - All hours labels now display with the local timezone abbreviation (e.g. "10am – 6pm PDT") in WeekCalendar, ParkCard, and ParkMonthCalendar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
6.0 KiB
TypeScript
169 lines
6.0 KiB
TypeScript
import Link from "next/link";
|
||
import type { Park } from "@/lib/scrapers/types";
|
||
import type { DayData } from "@/lib/db";
|
||
import { getTimezoneAbbr } from "@/lib/env";
|
||
|
||
interface ParkCardProps {
|
||
park: Park;
|
||
weekDates: string[]; // 7 dates YYYY-MM-DD, Sun–Sat
|
||
parkData: Record<string, DayData>;
|
||
today: string;
|
||
openRideCount?: number;
|
||
coastersOnly?: boolean;
|
||
}
|
||
|
||
const DOW = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||
|
||
export function ParkCard({ park, weekDates, parkData, today, openRideCount, coastersOnly }: ParkCardProps) {
|
||
const openDays = weekDates.filter((d) => parkData[d]?.isOpen && parkData[d]?.hoursLabel);
|
||
const tzAbbr = getTimezoneAbbr(park.timezone);
|
||
const isOpenToday = openDays.includes(today);
|
||
|
||
return (
|
||
<Link
|
||
href={`/park/${park.id}`}
|
||
data-park={park.name.toLowerCase()}
|
||
style={{ textDecoration: "none", display: "block" }}
|
||
>
|
||
<div className="park-card" style={{
|
||
background: "var(--color-surface)",
|
||
border: "1px solid var(--color-border)",
|
||
borderRadius: 12,
|
||
overflow: "hidden",
|
||
}}>
|
||
{/* ── Card header ───────────────────────────────────────────────────── */}
|
||
<div style={{
|
||
padding: "14px 16px 12px",
|
||
display: "flex",
|
||
alignItems: "flex-start",
|
||
justifyContent: "space-between",
|
||
gap: 12,
|
||
}}>
|
||
<div>
|
||
<div style={{
|
||
fontSize: "0.95rem",
|
||
fontWeight: 600,
|
||
color: "var(--color-text)",
|
||
lineHeight: 1.2,
|
||
}}>
|
||
{park.name}
|
||
</div>
|
||
<div style={{
|
||
fontSize: "0.72rem",
|
||
color: "var(--color-text-muted)",
|
||
marginTop: 3,
|
||
}}>
|
||
{park.location.city}, {park.location.state}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 5, flexShrink: 0 }}>
|
||
{isOpenToday ? (
|
||
<div style={{
|
||
background: "var(--color-open-bg)",
|
||
border: "1px solid var(--color-open-border)",
|
||
borderRadius: 20,
|
||
padding: "4px 10px",
|
||
fontSize: "0.65rem",
|
||
fontWeight: 700,
|
||
color: "var(--color-open-text)",
|
||
whiteSpace: "nowrap",
|
||
letterSpacing: "0.03em",
|
||
}}>
|
||
Open today
|
||
</div>
|
||
) : (
|
||
<div style={{
|
||
background: "transparent",
|
||
border: "1px solid var(--color-border)",
|
||
borderRadius: 20,
|
||
padding: "4px 10px",
|
||
fontSize: "0.65rem",
|
||
fontWeight: 500,
|
||
color: "var(--color-text-muted)",
|
||
whiteSpace: "nowrap",
|
||
}}>
|
||
Closed today
|
||
</div>
|
||
)}
|
||
{isOpenToday && openRideCount !== undefined && (
|
||
<div style={{
|
||
fontSize: "0.65rem",
|
||
color: "var(--color-open-hours)",
|
||
fontWeight: 500,
|
||
textAlign: "right",
|
||
}}>
|
||
{openRideCount} {coastersOnly
|
||
? (openRideCount === 1 ? "coaster" : "coasters")
|
||
: (openRideCount === 1 ? "ride" : "rides")} operating
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Open days list ────────────────────────────────────────────────── */}
|
||
{openDays.length > 0 && (
|
||
<div style={{ borderTop: "1px solid var(--color-border-subtle)" }}>
|
||
{openDays.map((date, i) => {
|
||
const dow = new Date(date + "T00:00:00").getDay();
|
||
const isToday = date === today;
|
||
const dayData = parkData[date];
|
||
const isPH = dayData.specialType === "passholder_preview";
|
||
const isLast = i === openDays.length - 1;
|
||
|
||
return (
|
||
<div
|
||
key={date}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
padding: "9px 16px",
|
||
background: isToday ? "var(--color-today-bg)" : "transparent",
|
||
borderBottom: isLast ? "none" : "1px solid var(--color-border-subtle)",
|
||
}}
|
||
>
|
||
<span style={{
|
||
fontSize: "0.82rem",
|
||
fontWeight: isToday ? 600 : 400,
|
||
color: isToday
|
||
? "var(--color-today-text)"
|
||
: "var(--color-text-secondary)",
|
||
}}>
|
||
{isToday ? "Today" : DOW[dow]}
|
||
</span>
|
||
|
||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||
{isPH && (
|
||
<span style={{
|
||
fontSize: "0.58rem",
|
||
fontWeight: 700,
|
||
color: "var(--color-ph-label)",
|
||
letterSpacing: "0.05em",
|
||
textTransform: "uppercase",
|
||
}}>
|
||
Passholder
|
||
</span>
|
||
)}
|
||
<span style={{
|
||
fontSize: "0.82rem",
|
||
fontWeight: isToday ? 600 : 500,
|
||
color: isPH
|
||
? "var(--color-ph-hours)"
|
||
: isToday
|
||
? "var(--color-today-text)"
|
||
: "var(--color-open-hours)",
|
||
}}>
|
||
{dayData.hoursLabel} {tzAbbr}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Link>
|
||
);
|
||
}
|