From 8e969165b4efa3b09be3cefcb6a884a4fce718f0 Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 4 Apr 2026 20:38:12 -0400 Subject: [PATCH] feat: show live open ride count in park name cell - Fetch Queue-Times ride counts for parks open today (5min cache) - Only shown within 1h before open to 1h after scheduled close - Count displayed on the right of the park name/location cell (desktop) and below the open badge (mobile) - Whole park cell is now a clickable link - Hover warms the park cell background; no row-wide highlight Co-Authored-By: Claude Sonnet 4.6 --- app/globals.css | 18 ++++++++------ app/page.tsx | 46 +++++++++++++++++++++++++++++++++++ components/MobileCardList.tsx | 4 ++- components/ParkCard.tsx | 20 +++++++++++---- components/WeekCalendar.tsx | 35 +++++++++++++++++++------- 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/app/globals.css b/app/globals.css index 1388dd2..3e04b3c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -80,20 +80,22 @@ clip-path: inset(0 -16px 0 0); } -/* ── Park row hover (group/group-hover via Tailwind not enough across sticky cols) */ -.park-row:hover td, -.park-row:hover th { - background-color: var(--color-surface-hover) !important; -} - /* ── Park name link hover ────────────────────────────────────────────────── */ .park-name-link { text-decoration: none; color: inherit; - transition: color 120ms ease; + transition: background 150ms ease; } .park-name-link:hover { - color: var(--color-accent); + background: var(--color-surface-hover); +} + +/* ── Mobile park card hover ─────────────────────────────────────────────── */ +.park-card { + transition: background 150ms ease; +} +.park-card:hover { + background: var(--color-surface-hover) !important; } /* ── Pulse animation for skeleton ───────────────────────────────────────── */ diff --git a/app/page.tsx b/app/page.tsx index 40329da..8d554e8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -6,11 +6,36 @@ import { EmptyState } from "@/components/EmptyState"; import { PARKS, groupByRegion } from "@/lib/parks"; import { openDb, getDateRange } from "@/lib/db"; import { getTodayLocal } from "@/lib/env"; +import { fetchLiveRides } from "@/lib/scrapers/queuetimes"; +import { QUEUE_TIMES_IDS } from "@/lib/queue-times-map"; interface PageProps { searchParams: Promise<{ week?: string }>; } +/** + * Returns true when the current local time is within 1 hour before open + * or 1 hour after close, based on a hoursLabel like "10am – 6pm". + */ +function isWithinOperatingWindow(hoursLabel: string): boolean { + const m = hoursLabel.match( + /^(\d+)(?::(\d+))?(am|pm)\s*[–-]\s*(\d+)(?::(\d+))?(am|pm)$/i + ); + if (!m) return true; // unparseable — show anyway + const toMinutes = (h: string, min: string | undefined, period: string) => { + let hours = parseInt(h, 10); + const minutes = min ? parseInt(min, 10) : 0; + if (period.toLowerCase() === "pm" && hours !== 12) hours += 12; + if (period.toLowerCase() === "am" && hours === 12) hours = 0; + return hours * 60 + minutes; + }; + const openMin = toMinutes(m[1], m[2], m[3]); + const closeMin = toMinutes(m[4], m[5], m[6]); + const now = new Date(); + const nowMin = now.getHours() * 60 + now.getMinutes(); + return nowMin >= openMin - 60 && nowMin <= closeMin + 60; +} + function getWeekStart(param: string | undefined): string { if (param && /^\d{4}-\d{2}-\d{2}$/.test(param)) { const d = new Date(param + "T00:00:00"); @@ -57,6 +82,25 @@ export default async function HomePage({ searchParams }: PageProps) { 0 ); + // Fetch live ride counts for parks open today (cached 5 min via Queue-Times). + // Only shown when the current time is within 1h before open to 1h after close. + let rideCounts: Record = {}; + if (weekDates.includes(today)) { + const openTodayParks = PARKS.filter((p) => { + const dayData = data[p.id]?.[today]; + if (!dayData?.isOpen || !QUEUE_TIMES_IDS[p.id] || !dayData.hoursLabel) return false; + return isWithinOperatingWindow(dayData.hoursLabel); + }); + const results = await Promise.all( + openTodayParks.map(async (p) => { + const result = await fetchLiveRides(QUEUE_TIMES_IDS[p.id], null, 300); + const count = result ? result.rides.filter((r) => r.isOpen).length : 0; + return [p.id, count] as [string, number]; + }) + ); + rideCounts = Object.fromEntries(results.filter(([, count]) => count > 0)); + } + const visibleParks = PARKS.filter((park) => weekDates.some((date) => data[park.id]?.[date]?.isOpen) ); @@ -136,6 +180,7 @@ export default async function HomePage({ searchParams }: PageProps) { weekDates={weekDates} data={data} today={today} + rideCounts={rideCounts} /> @@ -146,6 +191,7 @@ export default async function HomePage({ searchParams }: PageProps) { weekDates={weekDates} data={data} grouped={grouped} + rideCounts={rideCounts} /> diff --git a/components/MobileCardList.tsx b/components/MobileCardList.tsx index 8eddd92..b8137f4 100644 --- a/components/MobileCardList.tsx +++ b/components/MobileCardList.tsx @@ -8,9 +8,10 @@ interface MobileCardListProps { weekDates: string[]; data: Record>; today: string; + rideCounts?: Record; } -export function MobileCardList({ grouped, weekDates, data, today }: MobileCardListProps) { +export function MobileCardList({ grouped, weekDates, data, today, rideCounts }: MobileCardListProps) { return (
{Array.from(grouped.entries()).map(([region, parks]) => ( @@ -50,6 +51,7 @@ export function MobileCardList({ grouped, weekDates, data, today }: MobileCardLi weekDates={weekDates} parkData={data[park.id] ?? {}} today={today} + openRideCount={rideCounts?.[park.id]} /> ))}
diff --git a/components/ParkCard.tsx b/components/ParkCard.tsx index 508492f..2307a07 100644 --- a/components/ParkCard.tsx +++ b/components/ParkCard.tsx @@ -7,11 +7,12 @@ interface ParkCardProps { weekDates: string[]; // 7 dates YYYY-MM-DD, Sun–Sat parkData: Record; today: string; + openRideCount?: number; } const DOW = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; -export function ParkCard({ park, weekDates, parkData, today }: ParkCardProps) { +export function ParkCard({ park, weekDates, parkData, today, openRideCount }: ParkCardProps) { const openDays = weekDates.filter((d) => parkData[d]?.isOpen && parkData[d]?.hoursLabel); const isOpenToday = openDays.includes(today); @@ -21,12 +22,11 @@ export function ParkCard({ park, weekDates, parkData, today }: ParkCardProps) { data-park={park.name.toLowerCase()} style={{ textDecoration: "none", display: "block" }} > -
{/* ── Card header ───────────────────────────────────────────────────── */}
+
{isOpenToday ? (
Open today @@ -79,11 +79,21 @@ export function ParkCard({ park, weekDates, parkData, today }: ParkCardProps) { fontWeight: 500, color: "var(--color-text-muted)", whiteSpace: "nowrap", - flexShrink: 0, }}> Closed today
)} + {isOpenToday && openRideCount !== undefined && ( +
+ {openRideCount} open +
+ )} +
{/* ── Open days list ────────────────────────────────────────────────── */} diff --git a/components/WeekCalendar.tsx b/components/WeekCalendar.tsx index 6ec8e08..dd0078c 100644 --- a/components/WeekCalendar.tsx +++ b/components/WeekCalendar.tsx @@ -10,6 +10,7 @@ interface WeekCalendarProps { weekDates: string[]; // 7 dates, YYYY-MM-DD, Sun–Sat data: Record>; // parkId → date → DayData grouped?: Map; // pre-grouped parks (if provided, renders region headers) + rideCounts?: Record; // parkId → open ride count for today } const DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; @@ -163,12 +164,14 @@ function ParkRow({ weekDates, parsedDates, parkData, + rideCounts, }: { park: Park; parkIdx: number; weekDates: string[]; parsedDates: ReturnType[]; parkData: Record; + rideCounts?: Record; }) { const rowBg = parkIdx % 2 === 0 ? "var(--color-bg)" : "var(--color-surface)"; return ( @@ -181,7 +184,7 @@ function ParkRow({ position: "sticky", left: 0, zIndex: 5, - padding: "10px 14px", + padding: 0, borderBottom: "1px solid var(--color-border)", borderRight: "1px solid var(--color-border)", whiteSpace: "nowrap", @@ -189,14 +192,27 @@ function ParkRow({ background: rowBg, transition: "background 120ms ease", }}> - - - {park.name} - + +
+ + {park.name} + +
+ {park.location.city}, {park.location.state} +
+
+ {rideCounts?.[park.id] !== undefined && ( +
+ {rideCounts[park.id]} open +
+ )} -
- {park.location.city}, {park.location.state} -
{weekDates.map((date, i) => ( @@ -210,7 +226,7 @@ function ParkRow({ ); } -export function WeekCalendar({ parks, weekDates, data, grouped }: WeekCalendarProps) { +export function WeekCalendar({ parks, weekDates, data, grouped, rideCounts }: WeekCalendarProps) { const today = getTodayLocal(); const parsedDates = weekDates.map(parseDate); @@ -324,6 +340,7 @@ export function WeekCalendar({ parks, weekDates, data, grouped }: WeekCalendarPr weekDates={weekDates} parsedDates={parsedDates} parkData={data[park.id] ?? {}} + rideCounts={rideCounts} /> ))}