feat: show open dot based on hours, Weather Delay when queue-times shows 0 rides
All checks were successful
Build and Deploy / Build & Push (push) Successful in 49s

Park open indicator now derives from scheduled hours, not ride counts.
Parks with queue-times coverage but 0 open rides (e.g. storm) show a
"⛈ Weather Delay" notice instead of a ride count on both desktop and mobile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Josh Wright
2026-04-05 14:56:54 -04:00
parent d84a15ad64
commit 32f0d05038
5 changed files with 61 additions and 10 deletions

View File

@@ -63,12 +63,16 @@ export default async function HomePage({ searchParams }: PageProps) {
let rideCounts: Record<string, number> = {}; let rideCounts: Record<string, number> = {};
let coasterCounts: Record<string, number> = {}; let coasterCounts: Record<string, number> = {};
let closingParkIds: string[] = []; let closingParkIds: string[] = [];
let openParkIds: string[] = [];
let weatherDelayParkIds: string[] = [];
if (weekDates.includes(today)) { if (weekDates.includes(today)) {
// Parks within operating hours right now (for open dot — independent of ride counts)
const openTodayParks = PARKS.filter((p) => { const openTodayParks = PARKS.filter((p) => {
const dayData = data[p.id]?.[today]; const dayData = data[p.id]?.[today];
if (!dayData?.isOpen || !QUEUE_TIMES_IDS[p.id] || !dayData.hoursLabel) return false; if (!dayData?.isOpen || !dayData.hoursLabel) return false;
return isWithinOperatingWindow(dayData.hoursLabel, p.timezone); return isWithinOperatingWindow(dayData.hoursLabel, p.timezone);
}); });
openParkIds = openTodayParks.map((p) => p.id);
closingParkIds = openTodayParks closingParkIds = openTodayParks
.filter((p) => { .filter((p) => {
const dayData = data[p.id]?.[today]; const dayData = data[p.id]?.[today];
@@ -77,17 +81,23 @@ export default async function HomePage({ searchParams }: PageProps) {
: false; : false;
}) })
.map((p) => p.id); .map((p) => p.id);
// Only fetch ride counts for parks that have queue-times coverage
const trackedParks = openTodayParks.filter((p) => QUEUE_TIMES_IDS[p.id]);
const results = await Promise.all( const results = await Promise.all(
openTodayParks.map(async (p) => { trackedParks.map(async (p) => {
const coasterSet = getCoasterSet(p.id, parkMeta); const coasterSet = getCoasterSet(p.id, parkMeta);
const result = await fetchLiveRides(QUEUE_TIMES_IDS[p.id], coasterSet, 300); const result = await fetchLiveRides(QUEUE_TIMES_IDS[p.id], coasterSet, 300);
const rideCount = result ? result.rides.filter((r) => r.isOpen).length : 0; const rideCount = result ? result.rides.filter((r) => r.isOpen).length : null;
const coasterCount = result ? result.rides.filter((r) => r.isOpen && r.isCoaster).length : 0; const coasterCount = result ? result.rides.filter((r) => r.isOpen && r.isCoaster).length : 0;
return { id: p.id, rideCount, coasterCount }; return { id: p.id, rideCount, coasterCount };
}) })
); );
// Parks with queue-times coverage but 0 open rides = likely weather delay
weatherDelayParkIds = results
.filter(({ rideCount }) => rideCount === 0)
.map(({ id }) => id);
rideCounts = Object.fromEntries( rideCounts = Object.fromEntries(
results.filter(({ rideCount }) => rideCount > 0).map(({ id, rideCount }) => [id, rideCount]) results.filter(({ rideCount }) => rideCount != null && rideCount > 0).map(({ id, rideCount }) => [id, rideCount!])
); );
coasterCounts = Object.fromEntries( coasterCounts = Object.fromEntries(
results.filter(({ coasterCount }) => coasterCount > 0).map(({ id, coasterCount }) => [id, coasterCount]) results.filter(({ coasterCount }) => coasterCount > 0).map(({ id, coasterCount }) => [id, coasterCount])
@@ -103,7 +113,9 @@ export default async function HomePage({ searchParams }: PageProps) {
data={data} data={data}
rideCounts={rideCounts} rideCounts={rideCounts}
coasterCounts={coasterCounts} coasterCounts={coasterCounts}
openParkIds={openParkIds}
closingParkIds={closingParkIds} closingParkIds={closingParkIds}
weatherDelayParkIds={weatherDelayParkIds}
hasCoasterData={hasCoasterData} hasCoasterData={hasCoasterData}
scrapedCount={scrapedCount} scrapedCount={scrapedCount}
/> />

View File

@@ -50,7 +50,9 @@ interface HomePageClientProps {
data: Record<string, Record<string, DayData>>; data: Record<string, Record<string, DayData>>;
rideCounts: Record<string, number>; rideCounts: Record<string, number>;
coasterCounts: Record<string, number>; coasterCounts: Record<string, number>;
openParkIds: string[];
closingParkIds: string[]; closingParkIds: string[];
weatherDelayParkIds: string[];
hasCoasterData: boolean; hasCoasterData: boolean;
scrapedCount: number; scrapedCount: number;
} }
@@ -63,7 +65,9 @@ export function HomePageClient({
data, data,
rideCounts, rideCounts,
coasterCounts, coasterCounts,
openParkIds,
closingParkIds, closingParkIds,
weatherDelayParkIds,
hasCoasterData, hasCoasterData,
scrapedCount, scrapedCount,
}: HomePageClientProps) { }: HomePageClientProps) {
@@ -231,7 +235,9 @@ export function HomePageClient({
today={today} today={today}
rideCounts={activeCounts} rideCounts={activeCounts}
coastersOnly={coastersOnly} coastersOnly={coastersOnly}
openParkIds={openParkIds}
closingParkIds={closingParkIds} closingParkIds={closingParkIds}
weatherDelayParkIds={weatherDelayParkIds}
/> />
</div> </div>
@@ -244,7 +250,9 @@ export function HomePageClient({
grouped={grouped} grouped={grouped}
rideCounts={activeCounts} rideCounts={activeCounts}
coastersOnly={coastersOnly} coastersOnly={coastersOnly}
openParkIds={openParkIds}
closingParkIds={closingParkIds} closingParkIds={closingParkIds}
weatherDelayParkIds={weatherDelayParkIds}
/> />
</div> </div>
</> </>

View File

@@ -10,10 +10,12 @@ interface MobileCardListProps {
today: string; today: string;
rideCounts?: Record<string, number>; rideCounts?: Record<string, number>;
coastersOnly?: boolean; coastersOnly?: boolean;
openParkIds?: string[];
closingParkIds?: string[]; closingParkIds?: string[];
weatherDelayParkIds?: string[];
} }
export function MobileCardList({ grouped, weekDates, data, today, rideCounts, coastersOnly, closingParkIds }: MobileCardListProps) { export function MobileCardList({ grouped, weekDates, data, today, rideCounts, coastersOnly, openParkIds, closingParkIds, weatherDelayParkIds }: MobileCardListProps) {
return ( return (
<div style={{ display: "flex", flexDirection: "column", gap: 20, paddingTop: 14 }}> <div style={{ display: "flex", flexDirection: "column", gap: 20, paddingTop: 14 }}>
{Array.from(grouped.entries()).map(([region, parks]) => ( {Array.from(grouped.entries()).map(([region, parks]) => (
@@ -55,7 +57,9 @@ export function MobileCardList({ grouped, weekDates, data, today, rideCounts, co
today={today} today={today}
openRideCount={rideCounts?.[park.id]} openRideCount={rideCounts?.[park.id]}
coastersOnly={coastersOnly} coastersOnly={coastersOnly}
isOpen={openParkIds?.includes(park.id)}
isClosing={closingParkIds?.includes(park.id)} isClosing={closingParkIds?.includes(park.id)}
isWeatherDelay={weatherDelayParkIds?.includes(park.id)}
/> />
))} ))}
</div> </div>

View File

@@ -10,12 +10,14 @@ interface ParkCardProps {
today: string; today: string;
openRideCount?: number; openRideCount?: number;
coastersOnly?: boolean; coastersOnly?: boolean;
isOpen?: boolean;
isClosing?: boolean; isClosing?: boolean;
isWeatherDelay?: boolean;
} }
const DOW = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const DOW = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
export function ParkCard({ park, weekDates, parkData, today, openRideCount, coastersOnly, isClosing }: ParkCardProps) { export function ParkCard({ park, weekDates, parkData, today, openRideCount, coastersOnly, isOpen, isClosing, isWeatherDelay }: ParkCardProps) {
const openDays = weekDates.filter((d) => parkData[d]?.isOpen && parkData[d]?.hoursLabel); const openDays = weekDates.filter((d) => parkData[d]?.isOpen && parkData[d]?.hoursLabel);
const tzAbbr = getTimezoneAbbr(park.timezone); const tzAbbr = getTimezoneAbbr(park.timezone);
const isOpenToday = openDays.includes(today); const isOpenToday = openDays.includes(today);
@@ -87,6 +89,16 @@ export function ParkCard({ park, weekDates, parkData, today, openRideCount, coas
Closed today Closed today
</div> </div>
)} )}
{isOpenToday && isWeatherDelay && (
<div style={{
fontSize: "0.65rem",
color: "var(--color-text-muted)",
fontWeight: 500,
textAlign: "right",
}}>
Weather Delay
</div>
)}
{isOpenToday && openRideCount !== undefined && ( {isOpenToday && openRideCount !== undefined && (
<div style={{ <div style={{
fontSize: "0.65rem", fontSize: "0.65rem",

View File

@@ -12,7 +12,9 @@ interface WeekCalendarProps {
grouped?: Map<Region, Park[]>; // pre-grouped parks (if provided, renders region headers) grouped?: Map<Region, Park[]>; // pre-grouped parks (if provided, renders region headers)
rideCounts?: Record<string, number>; // parkId → open ride/coaster count for today rideCounts?: Record<string, number>; // parkId → open ride/coaster count for today
coastersOnly?: boolean; coastersOnly?: boolean;
closingParkIds?: string[]; // parks in the post-close wind-down buffer openParkIds?: string[];
closingParkIds?: string[];
weatherDelayParkIds?: string[];
} }
const DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
@@ -189,7 +191,9 @@ function ParkRow({
parkData, parkData,
rideCounts, rideCounts,
coastersOnly, coastersOnly,
openParkIds,
closingParkIds, closingParkIds,
weatherDelayParkIds,
}: { }: {
park: Park; park: Park;
parkIdx: number; parkIdx: number;
@@ -198,11 +202,15 @@ function ParkRow({
parkData: Record<string, DayData>; parkData: Record<string, DayData>;
rideCounts?: Record<string, number>; rideCounts?: Record<string, number>;
coastersOnly?: boolean; coastersOnly?: boolean;
openParkIds?: string[];
closingParkIds?: string[]; closingParkIds?: string[];
weatherDelayParkIds?: string[];
}) { }) {
const rowBg = parkIdx % 2 === 0 ? "var(--color-bg)" : "var(--color-surface)"; const rowBg = parkIdx % 2 === 0 ? "var(--color-bg)" : "var(--color-surface)";
const tzAbbr = getTimezoneAbbr(park.timezone); const tzAbbr = getTimezoneAbbr(park.timezone);
const isOpen = openParkIds?.includes(park.id) ?? false;
const isClosing = closingParkIds?.includes(park.id) ?? false; const isClosing = closingParkIds?.includes(park.id) ?? false;
const isWeatherDelay = weatherDelayParkIds?.includes(park.id) ?? false;
return ( return (
<tr <tr
className="park-row" className="park-row"
@@ -232,7 +240,7 @@ function ParkRow({
<span style={{ fontWeight: 500, fontSize: "0.85rem", lineHeight: 1.2, color: "var(--color-text)", whiteSpace: "nowrap" }}> <span style={{ fontWeight: 500, fontSize: "0.85rem", lineHeight: 1.2, color: "var(--color-text)", whiteSpace: "nowrap" }}>
{park.name} {park.name}
</span> </span>
{rideCounts?.[park.id] !== undefined && ( {isOpen && (
<span style={{ <span style={{
width: 7, width: 7,
height: 7, height: 7,
@@ -249,7 +257,12 @@ function ParkRow({
{park.location.city}, {park.location.state} {park.location.city}, {park.location.state}
</div> </div>
</div> </div>
{rideCounts?.[park.id] !== undefined && ( {isWeatherDelay && (
<div style={{ fontSize: "0.7rem", color: "var(--color-text-muted)", fontWeight: 500, textAlign: "right" }}>
Weather Delay
</div>
)}
{!isWeatherDelay && rideCounts?.[park.id] !== undefined && (
<div style={{ fontSize: "0.72rem", color: isClosing ? "var(--color-closing-hours)" : "var(--color-open-hours)", fontWeight: 600, textAlign: "right" }}> <div style={{ fontSize: "0.72rem", color: isClosing ? "var(--color-closing-hours)" : "var(--color-open-hours)", fontWeight: 600, textAlign: "right" }}>
{rideCounts[park.id]} {coastersOnly {rideCounts[park.id]} {coastersOnly
? (rideCounts[park.id] === 1 ? "coaster" : "coasters") ? (rideCounts[park.id] === 1 ? "coaster" : "coasters")
@@ -271,7 +284,7 @@ function ParkRow({
); );
} }
export function WeekCalendar({ parks, weekDates, data, grouped, rideCounts, coastersOnly, closingParkIds }: WeekCalendarProps) { export function WeekCalendar({ parks, weekDates, data, grouped, rideCounts, coastersOnly, openParkIds, closingParkIds, weatherDelayParkIds }: WeekCalendarProps) {
const today = getTodayLocal(); const today = getTodayLocal();
const parsedDates = weekDates.map(parseDate); const parsedDates = weekDates.map(parseDate);
@@ -387,7 +400,9 @@ export function WeekCalendar({ parks, weekDates, data, grouped, rideCounts, coas
parkData={data[park.id] ?? {}} parkData={data[park.id] ?? {}}
rideCounts={rideCounts} rideCounts={rideCounts}
coastersOnly={coastersOnly} coastersOnly={coastersOnly}
openParkIds={openParkIds}
closingParkIds={closingParkIds} closingParkIds={closingParkIds}
weatherDelayParkIds={weatherDelayParkIds}
/> />
))} ))}
</Fragment> </Fragment>