diff --git a/app/components/sport-season/EventSchedule.tsx b/app/components/sport-season/EventSchedule.tsx index 64f4cd8..013a94f 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,9 +113,13 @@ export function EventSchedule({ upcomingEvents, recentEvents, leagueId, sportsSe

{getDisplayDate(event)} - {isMounted && event.eventStartsAt && ( - · {format(new Date(event.eventStartsAt), "h:mm a")} - )} + {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 354fc76..80e45f4 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) { @@ -353,7 +374,15 @@ 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() + ?? 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(),