diff --git a/app/components/sport-season/GroupStageStandings.tsx b/app/components/sport-season/GroupStageStandings.tsx
index b1e1dcc..4931914 100644
--- a/app/components/sport-season/GroupStageStandings.tsx
+++ b/app/components/sport-season/GroupStageStandings.tsx
@@ -63,14 +63,17 @@ function MatchResult({ match }: { match: GroupMatch }) {
const kickoff = match.scheduledAt ? new Date(match.scheduledAt) : null;
return (
-
-
{p1}
-
- {kickoff
- ? kickoff.toLocaleDateString("en-US", { month: "short", day: "numeric" })
- : "vs"}
-
-
{p2}
+
+
+ {p1}
+ vs
+ {p2}
+
+ {kickoff && (
+
+ {kickoff.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })}
+
+ )}
);
}
@@ -159,24 +162,36 @@ export function GroupStageStandings({
{/* Match results */}
- {group.matches.length > 0 && (
-
- {[1, 2, 3].map((day) => {
- const dayMatches = group.matches.filter((m) => m.matchday === day);
- if (dayMatches.length === 0) return null;
- return (
+ {group.matches.length > 0 && (() => {
+ const sorted = group.matches.toSorted((a, b) => {
+ if (!a.scheduledAt && !b.scheduledAt) return 0;
+ if (!a.scheduledAt) return 1;
+ if (!b.scheduledAt) return -1;
+ return new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime();
+ });
+ const byDay = new Map
();
+ for (const m of sorted) {
+ const key = m.scheduledAt
+ ? new Date(m.scheduledAt).toLocaleDateString("en-US", { month: "short", day: "numeric" })
+ : "TBD";
+ if (!byDay.has(key)) byDay.set(key, []);
+ byDay.get(key)?.push(m);
+ }
+ return (
+
+ {[...byDay.entries()].map(([day, matches]) => (
- MD {day}
+ {day}
- {dayMatches.map((m) => (
+ {matches.map((m) => (
))}
- );
- })}
-
- )}
+ ))}
+
+ );
+ })()}
))}
diff --git a/app/components/sport-season/UpcomingCalendarPanel.tsx b/app/components/sport-season/UpcomingCalendarPanel.tsx
index 71acd58..f881ffa 100644
--- a/app/components/sport-season/UpcomingCalendarPanel.tsx
+++ b/app/components/sport-season/UpcomingCalendarPanel.tsx
@@ -61,7 +61,7 @@ function EventRow({ event, showLeague }: { event: CalendarPanelEvent; showLeague
? format(gameDate, "MMM d")
: formatEventDate(event.eventDate);
const participantCount = event.relevantParticipants.length;
- const isAllCompete = event.eventType !== "playoff_game";
+ const isAllCompete = event.eventType !== "playoff_game" && event.eventType !== "group_stage_match";
const displayName = event.matchLabel
? `${event.name} — ${event.matchLabel}`
: event.name;
diff --git a/app/components/sport-season/UpcomingEventsCard.tsx b/app/components/sport-season/UpcomingEventsCard.tsx
index e965944..fb93d79 100644
--- a/app/components/sport-season/UpcomingEventsCard.tsx
+++ b/app/components/sport-season/UpcomingEventsCard.tsx
@@ -45,7 +45,7 @@ function groupEvents(events: CalendarPanelEvent[]): GroupedEvent[] {
for (const event of events) {
const existing = map.get(event.id);
- const isAllCompete = event.eventType !== "playoff_game";
+ const isAllCompete = event.eventType !== "playoff_game" && event.eventType !== "group_stage_match";
const leagueEntry: LeagueParticipants = {
leagueId: event.leagueId ?? "unknown",
diff --git a/app/models/group-stage-match.ts b/app/models/group-stage-match.ts
index 62e7e16..fa46faa 100644
--- a/app/models/group-stage-match.ts
+++ b/app/models/group-stage-match.ts
@@ -1,4 +1,4 @@
-import { and, asc, eq, gte, inArray, lte, or } from "drizzle-orm";
+import { and, asc, eq, gte, inArray, lte, or, sql } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { logger } from "~/lib/logger";
@@ -146,7 +146,7 @@ export async function findMatchesByGroupId(groupId: string) {
/**
* Fetch all matches for multiple groups in a single query.
- * Returns a Map from groupId → matches (ordered by matchday, createdAt).
+ * Returns a Map from groupId → matches (ordered by scheduledAt, createdAt).
* Use this instead of calling findMatchesByGroupId N times.
*/
export async function findMatchesByGroupIds(
@@ -157,7 +157,7 @@ export async function findMatchesByGroupIds(
const rows = await db.query.groupStageMatches.findMany({
where: inArray(schema.groupStageMatches.tournamentGroupId, groupIds),
orderBy: [
- asc(schema.groupStageMatches.matchday),
+ sql`${schema.groupStageMatches.scheduledAt} ASC NULLS LAST`,
asc(schema.groupStageMatches.createdAt),
],
with: {
@@ -183,7 +183,7 @@ export async function findMatchesByEventId(eventId: string) {
with: {
matches: {
orderBy: [
- asc(schema.groupStageMatches.matchday),
+ sql`${schema.groupStageMatches.scheduledAt} ASC NULLS LAST`,
asc(schema.groupStageMatches.createdAt),
],
with: {
diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts
index 67c4078..162ddc8 100644
--- a/app/models/scoring-event.ts
+++ b/app/models/scoring-event.ts
@@ -588,7 +588,79 @@ export async function getUpcomingEventsForDraftedParticipants(
}
}
- return Array.from(entryMap.values());
+ const bracketResults = Array.from(entryMap.values());
+
+ // Also include group stage matches for the drafted participants.
+ // Group stage matches live in groupStageMatches, not playoffMatches, so
+ // the bracket query above misses them entirely.
+ const groupStageRows = await db
+ .select({
+ matchId: schema.groupStageMatches.id,
+ groupName: schema.tournamentGroups.groupName,
+ scoringEventName: schema.scoringEvents.name,
+ sportsSeasonId: schema.scoringEvents.sportsSeasonId,
+ scheduledAt: schema.groupStageMatches.scheduledAt,
+ participant1Id: schema.groupStageMatches.participant1Id,
+ participant2Id: schema.groupStageMatches.participant2Id,
+ })
+ .from(schema.groupStageMatches)
+ .innerJoin(
+ schema.tournamentGroups,
+ eq(schema.groupStageMatches.tournamentGroupId, schema.tournamentGroups.id)
+ )
+ .innerJoin(
+ schema.scoringEvents,
+ eq(schema.tournamentGroups.scoringEventId, schema.scoringEvents.id)
+ )
+ .where(
+ and(
+ eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
+ eq(schema.groupStageMatches.isComplete, false),
+ or(
+ inArray(schema.groupStageMatches.participant1Id, draftedIds),
+ inArray(schema.groupStageMatches.participant2Id, draftedIds)
+ ),
+ isNotNull(schema.groupStageMatches.scheduledAt),
+ gte(schema.groupStageMatches.scheduledAt, dateFromTimestamp),
+ lte(schema.groupStageMatches.scheduledAt, dateToTimestamp)
+ )
+ )
+ .orderBy(asc(schema.groupStageMatches.scheduledAt));
+
+ if (groupStageRows.length > 0) {
+ const allGroupParticipantIds = [
+ ...new Set(groupStageRows.flatMap((r) => [r.participant1Id, r.participant2Id])),
+ ];
+ const groupParticipantRows = await db.query.seasonParticipants.findMany({
+ where: inArray(schema.seasonParticipants.id, allGroupParticipantIds),
+ });
+ const groupParticipantMap = new Map(groupParticipantRows.map((p) => [p.id, p]));
+
+ for (const row of groupStageRows) {
+ if (!row.scheduledAt) continue;
+ const p1 = groupParticipantMap.get(row.participant1Id);
+ const p2 = groupParticipantMap.get(row.participant2Id);
+ const relevantParticipants: Array<{ id: string; name: string }> = [];
+ if (draftedIds.includes(row.participant1Id) && p1) {
+ relevantParticipants.push({ id: row.participant1Id, name: p1.name });
+ }
+ if (draftedIds.includes(row.participant2Id) && p2) {
+ relevantParticipants.push({ id: row.participant2Id, name: p2.name });
+ }
+ bracketResults.push({
+ id: `group|${row.matchId}`,
+ name: row.scoringEventName,
+ eventDate: null,
+ earliestGameTime: row.scheduledAt.toISOString(),
+ matchLabel: `Group ${row.groupName} — ${p1?.name ?? "?"} vs ${p2?.name ?? "?"}`,
+ eventType: "group_stage_match",
+ sportsSeasonId: row.sportsSeasonId,
+ relevantParticipants,
+ });
+ }
+ }
+
+ return bracketResults;
}
export interface DashboardScoringEvent {
diff --git a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx
index 053d64b..119c31c 100644
--- a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx
+++ b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx
@@ -17,7 +17,7 @@ export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
export { loader };
-type BracketView = "standings" | "playoffs" | "finished";
+type BracketView = "standings" | "playoffs" | "finished" | "groups";
export default function SportSeasonDetail({
loaderData,
@@ -66,20 +66,35 @@ export default function SportSeasonDetail({
])
);
- // Show the 3-way toggle only for bracket sports that also have regular season standings
+ // Show the 3-way toggle for bracket sports with regular season standings (NBA/AFL/etc.)
const showToggle = hasStandings && scoringPattern === "playoff_bracket";
+ // Show Groups/Bracket toggle for tournaments with a group stage AND a knockout bracket.
+ // Independent of showToggle — a sport can have both standings and group stage.
+ const hasGroupStage = groupStandings.length > 0;
+ const showGroupStageToggle = hasGroupStage && hasBracket;
+ const showAnyToggle = showToggle || showGroupStageToggle;
const [view, setView] = useState(() => {
+ if (showGroupStageToggle) {
+ return sportsSeason.status === "completed" ? "finished" : "playoffs";
+ }
if (!hasBracket) return "standings";
if (sportsSeason.status === "completed") return "finished";
return "playoffs";
});
- const TOGGLE_VIEWS: { value: BracketView; label: string }[] = [
- { value: "standings", label: "Standings" },
- { value: "playoffs", label: "Playoffs" },
- { value: "finished", label: "Finished" },
- ];
+ const TOGGLE_VIEWS: { value: BracketView; label: string }[] = showGroupStageToggle
+ ? [
+ { value: "groups", label: "Groups" },
+ ...(showToggle ? [{ value: "standings" as BracketView, label: "Standings" }] : []),
+ { value: "playoffs", label: "Bracket" },
+ ...(sportsSeason.status === "completed" ? [{ value: "finished" as BracketView, label: "Final" }] : []),
+ ]
+ : [
+ { value: "standings", label: "Standings" },
+ { value: "playoffs", label: "Playoffs" },
+ { value: "finished", label: "Finished" },
+ ];
const bracketDisplay = (bracketMode: "bracket" | "rankings") => (
- {showToggle && (
+ {showAnyToggle && (
{TOGGLE_VIEWS.map(({ value, label }) => (
- {showToggle ? (
+ {showAnyToggle ? (
+ {view === "groups" && (
+
+ )}
{view === "standings" && standingsDisplay}
{view === "playoffs" && bracketDisplay("bracket")}
{view === "finished" && bracketDisplay("rankings")}