From 363413cd38b7e6433182a00ba22ab6b3e38dabd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 16:47:17 +0000 Subject: [PATCH 1/3] Fix league sport-season page to show correct game times for FIFA World Cup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getUpcomingScoringEvents already fetched per-game scheduledAt times from playoffMatchGames for sorting, but discarded them from the return value. EventSchedule was showing eventStartsAt (event-level) instead of the accurate per-game times. Now getUpcomingScoringEvents includes earliestGameTime in each returned event, and EventSchedule prefers earliestGameTime over eventStartsAt — matching how UpcomingCalendarPanel works on the Upcoming Events page. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01KiVGo8gSBXe3WuRniVNKsd --- app/components/sport-season/EventSchedule.tsx | 16 ++++++++++------ app/models/scoring-event.ts | 8 +++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/components/sport-season/EventSchedule.tsx b/app/components/sport-season/EventSchedule.tsx index 64f4cd8..3b21e99 100644 --- a/app/components/sport-season/EventSchedule.tsx +++ b/app/components/sport-season/EventSchedule.tsx @@ -10,6 +10,7 @@ interface ScheduleEvent { name: string; eventDate: string | null; eventStartsAt?: Date | string | null; + earliestGameTime?: string | null; eventType: string; isComplete: boolean; completedAt?: Date | string | null; @@ -52,10 +53,13 @@ function getEventTypeLabel(eventType: string): string { export function EventSchedule({ upcomingEvents, recentEvents, leagueId, sportsSeasonId }: EventScheduleProps) { const isMounted = useHasMounted(); - function getDisplayDate(event: Pick): string { - if (isMounted && event.eventStartsAt) { - const d = new Date(event.eventStartsAt as string); - if (!isNaN(d.getTime())) return format(d, "MMM d, yyyy"); + function getDisplayDate(event: Pick): string { + if (isMounted) { + const ts = event.earliestGameTime ?? (event.eventStartsAt as string | null | undefined); + if (ts) { + const d = new Date(ts); + if (!isNaN(d.getTime())) return format(d, "MMM d, yyyy"); + } } return formatEventDate(event.eventDate); } @@ -109,8 +113,8 @@ export function EventSchedule({ upcomingEvents, recentEvents, leagueId, sportsSe

{getDisplayDate(event)} - {isMounted && event.eventStartsAt && ( - · {format(new Date(event.eventStartsAt), "h:mm a")} + {isMounted && (event.earliestGameTime ?? event.eventStartsAt) && ( + · {new Date(event.earliestGameTime ?? (event.eventStartsAt as string)).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })} )}

diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index 354fc76..d1abd32 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -353,7 +353,13 @@ export async function getUpcomingScoringEvents( return candidates.length > 0 ? Math.min(...candidates) : Infinity; }; - return events.toSorted((a, b) => getEffectiveMs(a) - getEffectiveMs(b)).slice(0, limit); + return events + .toSorted((a, b) => getEffectiveMs(a) - getEffectiveMs(b)) + .slice(0, limit) + .map((e) => ({ + ...e, + earliestGameTime: earliestGameById.get(e.id)?.toISOString() ?? null, + })); } /** From 9a04e25c20f0f29514816783bf09311c2c88da30 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 16:57:40 +0000 Subject: [PATCH 2/3] Also fetch group stage match times for sport-season event schedule The previous fix only queried playoffMatchGames.scheduledAt for bracket events. FIFA World Cup group stage matches live in groupStageMatches (linked via tournamentGroups), not playoffMatchGames. Now getUpcomingScoringEvents fetches both tables in parallel and uses the earliest scheduledAt from either source as earliestGameTime, matching what UpcomingCalendarPanel already shows on the Upcoming Events page. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01KiVGo8gSBXe3WuRniVNKsd --- app/models/scoring-event.ts | 65 ++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index d1abd32..26e31e9 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -310,33 +310,54 @@ export async function getUpcomingScoringEvents( if (events.length === 0) return []; - // Find earliest upcoming game time per event from bracket match games. + // Find earliest upcoming game time per event from both bracket match games + // and group stage matches (FIFA World Cup style). const eventIds = events.map((e) => e.id); const now = new Date(); - const gameTimeRows = await db - .select({ - eventId: schema.scoringEvents.id, - scheduledAt: schema.playoffMatchGames.scheduledAt, - }) - .from(schema.scoringEvents) - .innerJoin( - schema.playoffMatches, - eq(schema.playoffMatches.scoringEventId, schema.scoringEvents.id) - ) - .innerJoin( - schema.playoffMatchGames, - eq(schema.playoffMatchGames.playoffMatchId, schema.playoffMatches.id) - ) - .where( - and( - inArray(schema.scoringEvents.id, eventIds), - isNotNull(schema.playoffMatchGames.scheduledAt), - gte(schema.playoffMatchGames.scheduledAt, now) + + const [gameTimeRows, groupStageTimeRows] = await Promise.all([ + db + .select({ + eventId: schema.scoringEvents.id, + scheduledAt: schema.playoffMatchGames.scheduledAt, + }) + .from(schema.scoringEvents) + .innerJoin( + schema.playoffMatches, + eq(schema.playoffMatches.scoringEventId, schema.scoringEvents.id) ) - ); + .innerJoin( + schema.playoffMatchGames, + eq(schema.playoffMatchGames.playoffMatchId, schema.playoffMatches.id) + ) + .where( + and( + inArray(schema.scoringEvents.id, eventIds), + isNotNull(schema.playoffMatchGames.scheduledAt), + gte(schema.playoffMatchGames.scheduledAt, now) + ) + ), + db + .select({ + eventId: schema.tournamentGroups.scoringEventId, + scheduledAt: schema.groupStageMatches.scheduledAt, + }) + .from(schema.groupStageMatches) + .innerJoin( + schema.tournamentGroups, + eq(schema.groupStageMatches.tournamentGroupId, schema.tournamentGroups.id) + ) + .where( + and( + inArray(schema.tournamentGroups.scoringEventId, eventIds), + isNotNull(schema.groupStageMatches.scheduledAt), + gte(schema.groupStageMatches.scheduledAt, now) + ) + ), + ]); const earliestGameById = new Map(); - for (const row of gameTimeRows) { + for (const row of [...gameTimeRows, ...groupStageTimeRows]) { if (!row.scheduledAt) continue; const prev = earliestGameById.get(row.eventId); if (!prev || row.scheduledAt < prev) { From fa98d514f2a9ca72d1ee6bb6a78002bba59bd860 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 17:47:24 +0000 Subject: [PATCH 3/3] Code review fixes: earliestGameTime fallback, isNaN guard, and missing index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getUpcomingScoringEvents: fall back to eventStartsAt.toISOString() when no game-level scheduledAt exists, matching the same pattern already used in getUpcomingEventsForDraftedParticipants — prevents F1/golf events from showing a date with no time in SportSeasonCard and similar consumers - EventSchedule: replace bare toLocaleTimeString call with an isNaN-guarded block so a malformed timestamp renders nothing instead of "Invalid Date" - database/schema.ts: add index on tournament_groups(scoring_event_id) to support the inArray filter added in the previous commit (run npm run db:generate && npm run db:migrate to apply) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01KiVGo8gSBXe3WuRniVNKsd --- app/components/sport-season/EventSchedule.tsx | 10 +++++++--- app/models/scoring-event.ts | 4 +++- database/schema.ts | 4 +++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/components/sport-season/EventSchedule.tsx b/app/components/sport-season/EventSchedule.tsx index 3b21e99..013a94f 100644 --- a/app/components/sport-season/EventSchedule.tsx +++ b/app/components/sport-season/EventSchedule.tsx @@ -113,9 +113,13 @@ export function EventSchedule({ upcomingEvents, recentEvents, leagueId, sportsSe

{getDisplayDate(event)} - {isMounted && (event.earliestGameTime ?? event.eventStartsAt) && ( - · {new Date(event.earliestGameTime ?? (event.eventStartsAt as string)).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })} - )} + {isMounted && (() => { + const ts = event.earliestGameTime ?? (event.eventStartsAt as string | null | undefined); + if (!ts) return null; + const d = new Date(ts); + if (isNaN(d.getTime())) return null; + return · {d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}; + })()}

{event.eventType === "schedule_event" ? ( diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index 26e31e9..80e45f4 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -379,7 +379,9 @@ export async function getUpcomingScoringEvents( .slice(0, limit) .map((e) => ({ ...e, - earliestGameTime: earliestGameById.get(e.id)?.toISOString() ?? null, + earliestGameTime: earliestGameById.get(e.id)?.toISOString() + ?? e.eventStartsAt?.toISOString() + ?? null, })); } diff --git a/database/schema.ts b/database/schema.ts index 96fadae..8cbb2be 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -886,7 +886,9 @@ export const tournamentGroups = pgTable("tournament_groups", { .references(() => scoringEvents.id, { onDelete: "cascade" }), groupName: varchar("group_name", { length: 10 }).notNull(), // "A" through "L" createdAt: timestamp("created_at").defaultNow().notNull(), -}); +}, (t) => [ + index("tournament_groups_scoring_event_id_idx").on(t.scoringEventId), +]); export const tournamentGroupMembers = pgTable("tournament_group_members", { id: uuid("id").primaryKey().defaultRandom(),