improve: redesign mobile card layout for usability
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m24s
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m24s
Replace the cramped 7-column day grid with a clean open-days list. Each card now shows: - Park name + "Open today" / "Closed today" badge in the header - One row per open day (Today, Monday, Friday...) with full hours - Today row highlighted in amber; passholder days labeled inline - Whole card is a tap target linking to the park detail page Also: - Hide the legend below sm breakpoint (not needed on phones) - Reduce horizontal padding to 16px on mobile (was 24px) - Tighten MobileCardList vertical spacing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
12
app/page.tsx
12
app/page.tsx
@@ -72,7 +72,7 @@ export default async function HomePage({ searchParams }: PageProps) {
|
|||||||
}}>
|
}}>
|
||||||
{/* Row 1: Title + park count */}
|
{/* Row 1: Title + park count */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: "12px 24px 10px",
|
padding: "12px 16px 10px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
@@ -100,9 +100,9 @@ export default async function HomePage({ searchParams }: PageProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 2: Week nav + legend */}
|
{/* Row 2: Week nav + legend (legend hidden on mobile) */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: "8px 24px 10px",
|
padding: "8px 16px 10px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
@@ -114,12 +114,14 @@ export default async function HomePage({ searchParams }: PageProps) {
|
|||||||
weekDates={weekDates}
|
weekDates={weekDates}
|
||||||
isCurrentWeek={isCurrentWeek}
|
isCurrentWeek={isCurrentWeek}
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<div className="hidden sm:flex">
|
||||||
|
<Legend />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* ── Main content ───────────────────────────────────────────────────── */}
|
{/* ── Main content ───────────────────────────────────────────────────── */}
|
||||||
<main style={{ padding: "0 24px 48px" }}>
|
<main className="px-4 sm:px-6 pb-12">
|
||||||
{scrapedCount === 0 ? (
|
{scrapedCount === 0 ? (
|
||||||
<EmptyState />
|
<EmptyState />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface MobileCardListProps {
|
|||||||
|
|
||||||
export function MobileCardList({ grouped, weekDates, data, today }: MobileCardListProps) {
|
export function MobileCardList({ grouped, weekDates, data, today }: MobileCardListProps) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 24, paddingTop: 16 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 20, paddingTop: 14 }}>
|
||||||
{Array.from(grouped.entries()).map(([region, parks]) => (
|
{Array.from(grouped.entries()).map(([region, parks]) => (
|
||||||
<div key={region} data-region={region}>
|
<div key={region} data-region={region}>
|
||||||
{/* Region heading */}
|
{/* Region heading */}
|
||||||
|
|||||||
@@ -4,167 +4,150 @@ import type { DayData } from "@/lib/db";
|
|||||||
|
|
||||||
interface ParkCardProps {
|
interface ParkCardProps {
|
||||||
park: Park;
|
park: Park;
|
||||||
weekDates: string[]; // 7 dates YYYY-MM-DD
|
weekDates: string[]; // 7 dates YYYY-MM-DD, Sun–Sat
|
||||||
parkData: Record<string, DayData>;
|
parkData: Record<string, DayData>;
|
||||||
today: string;
|
today: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DOW_SHORT = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
const DOW = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||||
|
|
||||||
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) {
|
export function ParkCard({ park, weekDates, parkData, today }: ParkCardProps) {
|
||||||
|
const openDays = weekDates.filter((d) => parkData[d]?.isOpen && parkData[d]?.hoursLabel);
|
||||||
|
const isOpenToday = openDays.includes(today);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Link
|
||||||
|
href={`/park/${park.id}`}
|
||||||
data-park={park.name.toLowerCase()}
|
data-park={park.name.toLowerCase()}
|
||||||
style={{
|
style={{ textDecoration: "none", display: "block" }}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
background: "var(--color-surface)",
|
background: "var(--color-surface)",
|
||||||
border: "1px solid var(--color-border)",
|
border: "1px solid var(--color-border)",
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: "14px 14px 12px",
|
overflow: "hidden",
|
||||||
display: "flex",
|
transition: "border-color 120ms ease",
|
||||||
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) => {
|
{/* ── Card header ───────────────────────────────────────────────────── */}
|
||||||
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)";
|
|
||||||
const 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={{
|
<div style={{
|
||||||
|
padding: "14px 16px 12px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexWrap: "wrap",
|
alignItems: "flex-start",
|
||||||
gap: "4px 12px",
|
justifyContent: "space-between",
|
||||||
paddingTop: 4,
|
gap: 12,
|
||||||
borderTop: "1px solid var(--color-border-subtle)",
|
|
||||||
}}>
|
}}>
|
||||||
{weekDates.map((date) => {
|
<div>
|
||||||
const pd = parseDate(date);
|
<div style={{
|
||||||
const dayData = parkData[date];
|
fontSize: "0.95rem",
|
||||||
if (!dayData?.isOpen || !dayData?.hoursLabel) return null;
|
fontWeight: 600,
|
||||||
const isPH = dayData.specialType === "passholder_preview";
|
color: "var(--color-text)",
|
||||||
return (
|
lineHeight: 1.2,
|
||||||
<span key={date} style={{
|
}}>
|
||||||
fontSize: "0.68rem",
|
{park.name}
|
||||||
color: isPH ? "var(--color-ph-hours)" : "var(--color-open-hours)",
|
</div>
|
||||||
display: "flex",
|
<div style={{
|
||||||
gap: 4,
|
fontSize: "0.72rem",
|
||||||
alignItems: "center",
|
color: "var(--color-text-muted)",
|
||||||
}}>
|
marginTop: 3,
|
||||||
<span style={{ color: "var(--color-text-muted)", fontWeight: 600 }}>
|
}}>
|
||||||
{DOW_SHORT[pd.dow]}
|
{park.location.city}, {park.location.state}
|
||||||
</span>
|
</div>
|
||||||
{dayData.hoursLabel}
|
</div>
|
||||||
{isPH && (
|
|
||||||
<span style={{ color: "var(--color-ph-label)", fontSize: "0.6rem", fontWeight: 700 }}>PH</span>
|
{isOpenToday ? (
|
||||||
)}
|
<div style={{
|
||||||
</span>
|
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",
|
||||||
|
flexShrink: 0,
|
||||||
|
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",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
Closed today
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</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}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user