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
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:
20
app/page.tsx
20
app/page.tsx
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user