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 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 20:38:12 -04:00
parent 43feb4cef0
commit 8e969165b4
5 changed files with 100 additions and 23 deletions

View File

@@ -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 ───────────────────────────────────────── */

View File

@@ -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<string, number> = {};
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}
/>
</div>
@@ -146,6 +191,7 @@ export default async function HomePage({ searchParams }: PageProps) {
weekDates={weekDates}
data={data}
grouped={grouped}
rideCounts={rideCounts}
/>
</div>
</>