Show bracket on tennis Grand Slam major event pages
Tennis majors already store the full 128-player draw in playoff_matches (synced from Wikipedia) and the PlayoffBracket component + tennis_128 template already exist, but the public league event page only rendered a bracket for eventType === "playoff_game". Tennis majors are major_tournament events, so they fell through to the QP-only results table and the draw was never shown. Add a kind: "tennis" loader branch (gated on isBracketMajor + major_tournament) that loads the bracket (primary-keyed for shared majors) plus this window's QP results, and render both via the existing PlayoffBracket and a shared QpResultsTable extracted from the results block. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dfbb25771d
commit
764d70af3a
2 changed files with 171 additions and 50 deletions
|
|
@ -14,6 +14,7 @@ import { getCs2StageResultsForEvent } from "~/models/cs2-major-stage";
|
||||||
import { findSeasonMatchesByScoringEventId } from "~/models/season-match";
|
import { findSeasonMatchesByScoringEventId } from "~/models/season-match";
|
||||||
import { getEventResults } from "~/models/event-result";
|
import { getEventResults } from "~/models/event-result";
|
||||||
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
|
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
|
||||||
|
import { isBracketMajor } from "~/lib/event-utils";
|
||||||
import { findGroupsByEventId } from "~/models/tournament-group";
|
import { findGroupsByEventId } from "~/models/tournament-group";
|
||||||
import { findMatchesByGroupIds, computeGroupStandings } from "~/models/group-stage-match";
|
import { findMatchesByGroupIds, computeGroupStandings } from "~/models/group-stage-match";
|
||||||
import type { PlayoffMatch } from "~/models/playoff-match";
|
import type { PlayoffMatch } from "~/models/playoff-match";
|
||||||
|
|
@ -205,6 +206,69 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tennis Grand Slam major — bracket draw (primary-keyed) + this window's QP results.
|
||||||
|
// major_tournament + tennis_qualifying_points. CS2 (also a bracket major) is handled
|
||||||
|
// above; golf (golf_qualifying_points) returns false from isBracketMajor and falls
|
||||||
|
// through to the plain QP results branch.
|
||||||
|
if (isBracketMajor(simulatorType) && scoringEvent.eventType === "major_tournament") {
|
||||||
|
const [playoffMatchRows, eventResultRows] = await Promise.all([
|
||||||
|
db.query.playoffMatches.findMany({
|
||||||
|
where: eq(schema.playoffMatches.scoringEventId, structureEventId),
|
||||||
|
with: { participant1: true, participant2: true, winner: true, loser: true },
|
||||||
|
}),
|
||||||
|
// QP/results rows live on this window (local eventId), not the primary structure.
|
||||||
|
getEventResults(eventId),
|
||||||
|
]);
|
||||||
|
const playoffMatches: PlayoffMatchWithRelations[] = playoffMatchRows.map((m) => ({
|
||||||
|
...m,
|
||||||
|
createdAt: m.createdAt.toISOString(),
|
||||||
|
updatedAt: m.updatedAt.toISOString(),
|
||||||
|
}));
|
||||||
|
const templateId = structureBracketTemplateId ?? undefined;
|
||||||
|
const template = templateId ? getBracketTemplate(templateId) : undefined;
|
||||||
|
const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template);
|
||||||
|
|
||||||
|
const allResults = await db.query.seasonParticipantResults.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId),
|
||||||
|
eq(schema.seasonParticipantResults.finalPosition, 0)
|
||||||
|
),
|
||||||
|
with: { participant: true },
|
||||||
|
});
|
||||||
|
const preEliminatedParticipants = allResults
|
||||||
|
.filter((r): r is typeof r & { participant: NonNullable<typeof r.participant> } => r.participant !== null)
|
||||||
|
.map((r) => ({ id: r.participant.id, name: r.participant.name }));
|
||||||
|
|
||||||
|
const eventResults = eventResultRows
|
||||||
|
.filter((r): r is typeof r & { placement: number } => r.placement !== null && !r.notParticipating)
|
||||||
|
.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
placement: r.placement,
|
||||||
|
qualifyingPointsAwarded: r.qualifyingPointsAwarded,
|
||||||
|
rawScore: r.rawScore,
|
||||||
|
seasonParticipantId: r.seasonParticipantId,
|
||||||
|
participantName: r.seasonParticipant?.name ?? null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "tennis" as const,
|
||||||
|
league,
|
||||||
|
sportsSeason,
|
||||||
|
scoringEvent,
|
||||||
|
// Bracket structure + ownership keyed to the primary window (shared majors).
|
||||||
|
playoffMatches,
|
||||||
|
playoffRounds,
|
||||||
|
bracketTemplateId: templateId ?? null,
|
||||||
|
preEliminatedParticipants,
|
||||||
|
bracketTeamOwnerships,
|
||||||
|
bracketUserParticipantIds,
|
||||||
|
// QP results table uses this window's local ownership + participant ids.
|
||||||
|
teamOwnerships,
|
||||||
|
userParticipantIds,
|
||||||
|
eventResults,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Bracket events (tennis/golf bracket majors, soccer/UCL group+bracket)
|
// Bracket events (tennis/golf bracket majors, soccer/UCL group+bracket)
|
||||||
if (scoringEvent.eventType === "playoff_game") {
|
if (scoringEvent.eventType === "playoff_game") {
|
||||||
const playoffMatchRows = await db.query.playoffMatches.findMany({
|
const playoffMatchRows = await db.query.playoffMatches.findMany({
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,79 @@ export function meta({ data }: Route.MetaArgs) {
|
||||||
|
|
||||||
export { loader };
|
export { loader };
|
||||||
|
|
||||||
|
type QpResult = {
|
||||||
|
id: string;
|
||||||
|
placement: number;
|
||||||
|
qualifyingPointsAwarded: string | null;
|
||||||
|
rawScore: string | null;
|
||||||
|
seasonParticipantId: string;
|
||||||
|
participantName: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function QpResultsTable({
|
||||||
|
eventResults,
|
||||||
|
ownershipMap,
|
||||||
|
userParticipantIds,
|
||||||
|
}: {
|
||||||
|
eventResults: QpResult[];
|
||||||
|
ownershipMap: Record<string, { teamName: string; ownerName: string; teamId: string }>;
|
||||||
|
userParticipantIds: string[];
|
||||||
|
}) {
|
||||||
|
if (eventResults.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-8 text-center">
|
||||||
|
<p className="text-muted-foreground text-sm">Results not yet available for this event.</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Results</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-12">#</TableHead>
|
||||||
|
<TableHead>Participant</TableHead>
|
||||||
|
<TableHead>Manager</TableHead>
|
||||||
|
<TableHead className="text-right">QP</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{eventResults.map((r) => {
|
||||||
|
const ownership = ownershipMap[r.seasonParticipantId];
|
||||||
|
const isUser = userParticipantIds.includes(r.seasonParticipantId);
|
||||||
|
return (
|
||||||
|
<TableRow key={r.id} className={isUser ? "bg-electric/5" : undefined}>
|
||||||
|
<TableCell className="text-muted-foreground">{r.placement}</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{r.participantName ?? "—"}
|
||||||
|
{isUser && <span className="ml-1.5 text-xs text-electric">★</span>}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground text-sm">
|
||||||
|
{ownership?.teamName ?? "—"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{r.qualifyingPointsAwarded !== null
|
||||||
|
? parseFloat(r.qualifyingPointsAwarded).toFixed(2)
|
||||||
|
: r.rawScore !== null
|
||||||
|
? r.rawScore
|
||||||
|
: "—"}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function EventDetailPage({ loaderData }: Route.ComponentProps) {
|
export default function EventDetailPage({ loaderData }: Route.ComponentProps) {
|
||||||
const { league, sportsSeason, scoringEvent } = loaderData;
|
const { league, sportsSeason, scoringEvent } = loaderData;
|
||||||
const leagueId = league.id;
|
const leagueId = league.id;
|
||||||
|
|
@ -114,58 +187,42 @@ export default function EventDetailPage({ loaderData }: Route.ComponentProps) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Results table (QP tournaments, racing events) */}
|
{/* Tennis Grand Slam major — draw bracket + qualifying-points results */}
|
||||||
{loaderData.kind === "results" && (
|
{loaderData.kind === "tennis" && (
|
||||||
loaderData.eventResults.length > 0 ? (
|
<div className="space-y-6">
|
||||||
<Card>
|
{loaderData.playoffMatches.length > 0 ? (
|
||||||
<CardHeader>
|
<PlayoffBracket
|
||||||
<CardTitle className="text-base">Results</CardTitle>
|
matches={loaderData.playoffMatches}
|
||||||
</CardHeader>
|
rounds={loaderData.playoffRounds}
|
||||||
<CardContent className="pt-0">
|
bracketTemplateId={loaderData.bracketTemplateId}
|
||||||
<Table>
|
preEliminatedParticipants={loaderData.preEliminatedParticipants}
|
||||||
<TableHeader>
|
teamOwnerships={loaderData.bracketTeamOwnerships}
|
||||||
<TableRow>
|
userParticipantIds={loaderData.bracketUserParticipantIds}
|
||||||
<TableHead className="w-12">#</TableHead>
|
showOwnership={true}
|
||||||
<TableHead>Participant</TableHead>
|
mode="bracket"
|
||||||
<TableHead>Manager</TableHead>
|
/>
|
||||||
<TableHead className="text-right">QP</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{loaderData.eventResults.map((r) => {
|
|
||||||
const ownership = ownershipMap[r.seasonParticipantId];
|
|
||||||
const isUser = loaderData.userParticipantIds.includes(r.seasonParticipantId);
|
|
||||||
return (
|
|
||||||
<TableRow key={r.id} className={isUser ? "bg-electric/5" : undefined}>
|
|
||||||
<TableCell className="text-muted-foreground">{r.placement}</TableCell>
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
{r.participantName ?? "—"}
|
|
||||||
{isUser && <span className="ml-1.5 text-xs text-electric">★</span>}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground text-sm">
|
|
||||||
{ownership?.teamName ?? "—"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
{r.qualifyingPointsAwarded !== null
|
|
||||||
? parseFloat(r.qualifyingPointsAwarded).toFixed(2)
|
|
||||||
: r.rawScore !== null
|
|
||||||
? r.rawScore
|
|
||||||
: "—"}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="py-8 text-center">
|
<CardContent className="py-8 text-center">
|
||||||
<p className="text-muted-foreground text-sm">Results not yet available for this event.</p>
|
<p className="text-muted-foreground text-sm">Bracket not yet available.</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)}
|
||||||
|
<QpResultsTable
|
||||||
|
eventResults={loaderData.eventResults}
|
||||||
|
ownershipMap={ownershipMap}
|
||||||
|
userParticipantIds={loaderData.userParticipantIds}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Results table (QP tournaments, racing events) */}
|
||||||
|
{loaderData.kind === "results" && (
|
||||||
|
<QpResultsTable
|
||||||
|
eventResults={loaderData.eventResults}
|
||||||
|
ownershipMap={ownershipMap}
|
||||||
|
userParticipantIds={loaderData.userParticipantIds}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue