Fix World Cup group stage display and upcoming events
Some checks failed
🚀 Deploy / 🧪 Test (pull_request) Failing after 1m29s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m16s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 49s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Some checks failed
🚀 Deploy / 🧪 Test (pull_request) Failing after 1m29s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m16s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 49s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Remove MD 1/2/3 grouping from GroupStageStandings; list matches chronologically under date separators (Jun 14, Jun 18, etc.) - Fix MatchResult layout: show kick-off time below team names instead of sandwiching the date between them - Sort group stage matches by scheduledAt ASC NULLS LAST in the model so unscheduled matches always trail scheduled ones - Add Groups/Bracket toggle to the sports season page when both a group stage and knockout bracket exist, mirroring the NBA/AFL standings toggle; sports with both standings and group stage now include all three views - Include group stage matches in getUpcomingEventsForDraftedParticipants so World Cup fixtures appear on the home page calendar - Fix isAllCompete check in UpcomingEventsCard and UpcomingCalendarPanel to treat group_stage_match as a bracket-style event (shows team badges, not "N of your picks") Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a02f603575
commit
cce1115a4d
6 changed files with 141 additions and 36 deletions
|
|
@ -63,14 +63,17 @@ function MatchResult({ match }: { match: GroupMatch }) {
|
|||
|
||||
const kickoff = match.scheduledAt ? new Date(match.scheduledAt) : null;
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<span className="truncate max-w-[80px]">{p1}</span>
|
||||
<span className="shrink-0 text-muted-foreground/60">
|
||||
{kickoff
|
||||
? kickoff.toLocaleDateString("en-US", { month: "short", day: "numeric" })
|
||||
: "vs"}
|
||||
</span>
|
||||
<span className="truncate max-w-[80px] text-right">{p2}</span>
|
||||
<div className="flex flex-col gap-0">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<span className="truncate max-w-[80px]">{p1}</span>
|
||||
<span className="shrink-0 text-muted-foreground/60">vs</span>
|
||||
<span className="truncate max-w-[80px]">{p2}</span>
|
||||
</div>
|
||||
{kickoff && (
|
||||
<span className="text-[10px] text-muted-foreground/50">
|
||||
{kickoff.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -159,24 +162,36 @@ export function GroupStageStandings({
|
|||
</div>
|
||||
|
||||
{/* Match results */}
|
||||
{group.matches.length > 0 && (
|
||||
<div className="px-4 pb-3 space-y-0.5 border-t pt-2">
|
||||
{[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<string, GroupMatch[]>();
|
||||
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 (
|
||||
<div className="px-4 pb-3 space-y-0.5 border-t pt-2">
|
||||
{[...byDay.entries()].map(([day, matches]) => (
|
||||
<div key={day} className="space-y-0.5">
|
||||
<p className="text-[10px] text-muted-foreground/70 uppercase tracking-wide pt-1">
|
||||
MD {day}
|
||||
{day}
|
||||
</p>
|
||||
{dayMatches.map((m) => (
|
||||
{matches.map((m) => (
|
||||
<MatchResult key={m.id} match={m} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<BracketView>(() => {
|
||||
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") => (
|
||||
<SportSeasonDisplay
|
||||
|
|
@ -143,7 +158,7 @@ export default function SportSeasonDetail({
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{showToggle && (
|
||||
{showAnyToggle && (
|
||||
<div className="flex gap-1 rounded-lg bg-muted p-1 sm:shrink-0">
|
||||
{TOGGLE_VIEWS.map(({ value, label }) => (
|
||||
<button
|
||||
|
|
@ -165,8 +180,11 @@ export default function SportSeasonDetail({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{showToggle ? (
|
||||
{showAnyToggle ? (
|
||||
<div>
|
||||
{view === "groups" && (
|
||||
<GroupStageStandings groups={groupStandings} ownershipMap={ownershipMap} showEmpty={true} />
|
||||
)}
|
||||
{view === "standings" && standingsDisplay}
|
||||
{view === "playoffs" && bracketDisplay("bracket")}
|
||||
{view === "finished" && bracketDisplay("rankings")}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue