Files
SixFlagsSuperCalendar/components/ParkMonthCalendar.tsx
Josh Wright a31dda4e9e
All checks were successful
Build and Deploy / Build & Push (push) Successful in 51s
fix: uniform cell heights in park month calendar
Root cause: each week row was its own CSS Grid container, so rows
with open-day pills (hours + separate timezone line) grew taller than
closed rows, making the calendar column lines look staggered/slanted.

- Flatten all day cells into a single grid with gridAutoRows: 76
  so every row is exactly the same fixed height
- All cells get overflow: hidden so content can never push height
- Compact the status pill to a single line (hours + tz inline,
  truncated with ellipsis) — the stacked two-line pill was the
  primary height expander on narrow mobile columns
- Row/column border logic moves from week-wrapper divs to individual
  cell borderRight / borderBottom properties
- Nav link touch targets: padding 6×14 → 10×16, minWidth: 44px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 09:20:36 -04:00

242 lines
8.4 KiB
TypeScript

import Link from "next/link";
import type { DayData } from "@/lib/db";
import { getTimezoneAbbr } from "@/lib/env";
interface ParkMonthCalendarProps {
parkId: string;
year: number;
month: number; // 1-indexed
monthData: Record<string, DayData>; // 'YYYY-MM-DD' → DayData
today: string; // YYYY-MM-DD
timezone: string; // IANA timezone, e.g. "America/New_York"
}
const DOW_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTH_NAMES = [
"January","February","March","April","May","June",
"July","August","September","October","November","December",
];
function prevMonth(year: number, month: number) {
if (month === 1) return { year: year - 1, month: 12 };
return { year, month: month - 1 };
}
function nextMonth(year: number, month: number) {
if (month === 12) return { year: year + 1, month: 1 };
return { year, month: month + 1 };
}
function daysInMonth(year: number, month: number): number {
return new Date(year, month, 0).getDate();
}
export function ParkMonthCalendar({ parkId, year, month, monthData, today, timezone }: ParkMonthCalendarProps) {
const tzAbbr = getTimezoneAbbr(timezone);
const firstDow = new Date(year, month - 1, 1).getDay(); // 0=Sun
const totalDays = daysInMonth(year, month);
const prev = prevMonth(year, month);
const next = nextMonth(year, month);
const prevParam = `${prev.year}-${String(prev.month).padStart(2, "0")}`;
const nextParam = `${next.year}-${String(next.month).padStart(2, "0")}`;
// Build cells: leading empty + actual days
const cells: Array<{ day: number | null; iso: string | null }> = [];
for (let i = 0; i < firstDow; i++) cells.push({ day: null, iso: null });
for (let d = 1; d <= totalDays; d++) {
const iso = `${year}-${String(month).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
cells.push({ day: d, iso });
}
// Pad to complete last row
while (cells.length % 7 !== 0) cells.push({ day: null, iso: null });
const weeks: typeof cells[] = [];
for (let i = 0; i < cells.length; i += 7) {
weeks.push(cells.slice(i, i + 7));
}
return (
<div>
{/* Month nav */}
<div style={{
display: "flex",
alignItems: "center",
gap: 16,
marginBottom: 16,
}}>
<Link
href={`/park/${parkId}?month=${prevParam}`}
style={navLinkStyle}
>
</Link>
<span style={{
fontSize: "1rem",
fontWeight: 600,
color: "var(--color-text)",
minWidth: 160,
textAlign: "center",
letterSpacing: "-0.01em",
}}>
{MONTH_NAMES[month - 1]} {year}
</span>
<Link
href={`/park/${parkId}?month=${nextParam}`}
style={navLinkStyle}
>
</Link>
</div>
{/* Calendar grid */}
<div style={{
border: "1px solid var(--color-border)",
borderRadius: 10,
overflow: "hidden",
background: "var(--color-surface)",
}}>
{/* DOW header */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
borderBottom: "1px solid var(--color-border)",
}}>
{DOW_LABELS.map((d, i) => (
<div key={d} style={{
padding: "8px 0",
textAlign: "center",
fontSize: "0.65rem",
fontWeight: 700,
letterSpacing: "0.06em",
textTransform: "uppercase",
color: i === 0 || i === 6 ? "var(--color-text-secondary)" : "var(--color-text-muted)",
background: i === 0 || i === 6 ? "var(--color-weekend-header)" : "transparent",
}}>
{d}
</div>
))}
</div>
{/*
All day cells in ONE flat grid so every row shares the same
fixed height — no per-week wrapper divs that could produce
rows of different heights on mobile.
*/}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gridAutoRows: 76,
}}>
{cells.map((cell, idx) => {
const ci = idx % 7;
const isLastRow = idx >= cells.length - 7;
const borderBottom = !isLastRow ? "1px solid var(--color-border-subtle)" : "none";
const borderRight = ci < 6 ? "1px solid var(--color-border-subtle)" : "none";
if (!cell.day || !cell.iso) {
return (
<div key={idx} style={{
background: ci === 0 || ci === 6 ? "var(--color-weekend-header)" : "transparent",
borderRight,
borderBottom,
}} />
);
}
const dayData = monthData[cell.iso];
const isToday = cell.iso === today;
const isWeekend = ci === 0 || ci === 6;
const isOpen = dayData?.isOpen && dayData?.hoursLabel;
const isPH = dayData?.specialType === "passholder_preview";
const bg = isToday
? "var(--color-today-bg)"
: isWeekend
? "var(--color-weekend-header)"
: "transparent";
return (
<div key={idx} style={{
padding: "8px 8px",
overflow: "hidden",
background: bg,
borderRight,
borderBottom,
borderLeft: isToday ? "2px solid var(--color-today-border)" : "none",
display: "flex",
flexDirection: "column",
gap: 4,
}}>
{/* Date number */}
<span style={{
fontSize: "0.88rem",
fontWeight: isToday ? 700 : isWeekend ? 600 : 400,
color: isToday
? "var(--color-today-text)"
: isWeekend
? "var(--color-text)"
: "var(--color-text-muted)",
lineHeight: 1,
flexShrink: 0,
}}>
{cell.day}
</span>
{/* Status — single-line pill so all cells stay uniform height */}
{!dayData ? (
<span style={{ fontSize: "0.75rem", color: "var(--color-text-dim)" }}></span>
) : isPH && isOpen ? (
<div style={{
background: "var(--color-ph-bg)",
border: "1px solid var(--color-ph-border)",
borderRadius: 4,
padding: "2px 4px",
overflow: "hidden",
}}>
<div style={{ fontSize: "0.58rem", fontWeight: 700, color: "var(--color-ph-label)", textTransform: "uppercase", letterSpacing: "0.04em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
Passholder
</div>
<div style={{ fontSize: "0.6rem", color: "var(--color-ph-hours)", marginTop: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{dayData.hoursLabel} <span style={{ opacity: 0.6 }}>{tzAbbr}</span>
</div>
</div>
) : isOpen ? (
<div style={{
background: "var(--color-open-bg)",
border: "1px solid var(--color-open-border)",
borderRadius: 4,
padding: "2px 4px",
overflow: "hidden",
}}>
<div style={{ fontSize: "0.6rem", color: "var(--color-open-hours)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{dayData.hoursLabel} <span style={{ opacity: 0.6 }}>{tzAbbr}</span>
</div>
</div>
) : (
<span style={{ fontSize: "1rem", color: "var(--color-text-dim)", lineHeight: 1 }}>·</span>
)}
</div>
);
})}
</div>
</div>
</div>
);
}
const navLinkStyle: React.CSSProperties = {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
padding: "10px 16px",
borderRadius: 8,
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
color: "var(--color-text-muted)",
fontSize: "1rem",
lineHeight: 1,
textDecoration: "none",
minWidth: 44,
};