The playoff_matches.isScoring column defaults to true in the database. Brackets created before this column was added (or before the migration set correct values) have isScoring=true on play-in rounds that should be false. When recalculate-floors replayed those matches, it took the "scoring round" path; since "Play-In Round 1" isn't in ROUND_CONFIG, config===null, and the loser was assigned finalPosition=0 regardless of loserAdvances — permanently eliminating teams like the Suns who had a second play-in game remaining. Fix: build a round→isScoring lookup from the bracket template before replaying matches and use it as the source of truth, falling back to the DB field only when the template doesn't define the round. This ensures non-scoring play-in rounds are always processed with isScoring=false so the loserAdvances guard fires correctly. Also revert unrelated socket changes from the previous (wrong) commit. https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB
195 lines
7 KiB
TypeScript
195 lines
7 KiB
TypeScript
import { Link } from "react-router";
|
|
import type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId";
|
|
|
|
import { loader } from "./$leagueId.sports-seasons.$sportsSeasonId.server";
|
|
import { SportSeasonDisplay } from "~/components/scoring/SportSeasonDisplay";
|
|
import { EventSchedule } from "~/components/sport-season/EventSchedule";
|
|
import { RegularSeasonStandings } from "~/components/sport-season/RegularSeasonStandings";
|
|
import { GroupStageStandings } from "~/components/sport-season/GroupStageStandings";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Badge } from "~/components/ui/badge";
|
|
import { ArrowLeft, CheckCircle2, Clock, Zap } from "lucide-react";
|
|
|
|
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
|
return [{ title: `${data?.sportsSeason?.name ?? "Sport Season"} — ${data?.league?.name ?? "League"} - Brackt` }];
|
|
}
|
|
|
|
export { loader };
|
|
|
|
function getStatusBadge(status: string) {
|
|
switch (status) {
|
|
case "active":
|
|
return (
|
|
<Badge variant="outline" className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30">
|
|
<Zap className="mr-1 h-3 w-3" />
|
|
Active
|
|
</Badge>
|
|
);
|
|
case "upcoming":
|
|
return (
|
|
<Badge variant="outline" className="bg-electric/15 text-electric border-electric/30">
|
|
<Clock className="mr-1 h-3 w-3" />
|
|
Upcoming
|
|
</Badge>
|
|
);
|
|
case "completed":
|
|
return (
|
|
<Badge variant="outline" className="bg-muted text-muted-foreground border-border">
|
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
|
Completed
|
|
</Badge>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export default function SportSeasonDetail({
|
|
loaderData,
|
|
}: Route.ComponentProps) {
|
|
const {
|
|
league,
|
|
season,
|
|
sportsSeason,
|
|
scoringPattern,
|
|
playoffMatches,
|
|
playoffRounds,
|
|
preEliminatedParticipants,
|
|
participantPoints,
|
|
partialScoreParticipantIds,
|
|
seasonStandings,
|
|
qpStandings,
|
|
teamOwnerships,
|
|
userParticipantIds,
|
|
upcomingEvents,
|
|
recentEvents,
|
|
seasonIsFinalized,
|
|
regularSeasonStandings,
|
|
participantEvs,
|
|
groupStandings,
|
|
} = loaderData;
|
|
|
|
const hasBracket = playoffMatches && playoffMatches.length > 0;
|
|
const hasStandings = regularSeasonStandings && regularSeasonStandings.length > 0;
|
|
const showOtLosses = hasStandings && regularSeasonStandings.some((s) => s.otLosses !== null);
|
|
|
|
const simulatorType = sportsSeason.sport?.simulatorType;
|
|
const standingsDisplayMode =
|
|
simulatorType === "nhl_bracket" ? "nhl-divisions" :
|
|
simulatorType === "mlb_bracket" ? "mlb-divisions" :
|
|
"flat";
|
|
// NBA: 10 teams make the postseason (6 auto-qualify + 4 play-in)
|
|
// AFL: top 10 advance to the finals series (Wildcard + QF/EF + SF + PF + GF)
|
|
// NHL: handled by nhl-divisions mode (3 per div + 2 wild cards)
|
|
// MLB: handled by mlb-divisions mode (1 per div + 3 wild cards)
|
|
const playoffSpots =
|
|
simulatorType === "nba_bracket" || simulatorType === "afl_bracket" ? 10 : 8;
|
|
|
|
// Build ownership map for RegularSeasonStandings
|
|
const ownershipMap = Object.fromEntries(
|
|
teamOwnerships.map((o) => [
|
|
o.participantId,
|
|
{ teamName: o.teamName, ownerName: o.ownerName ?? "", teamId: o.teamId },
|
|
])
|
|
);
|
|
|
|
return (
|
|
<div className="container mx-auto py-8 px-4">
|
|
<div className="mb-6">
|
|
<Button variant="ghost" size="sm" asChild className="mb-4">
|
|
<Link to={`/leagues/${league.id}`}>
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Back to League
|
|
</Link>
|
|
</Button>
|
|
|
|
<div className="flex items-start justify-between gap-4 mb-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold mb-1">
|
|
{sportsSeason.sport.name}
|
|
</h1>
|
|
<p className="text-muted-foreground">
|
|
{sportsSeason.name} • {league.name} • {season.year} Season
|
|
</p>
|
|
</div>
|
|
{getStatusBadge(sportsSeason.status)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Regular season standings — show above bracket when no bracket exists yet */}
|
|
{hasStandings && !hasBracket && (
|
|
<div className="mb-6">
|
|
<RegularSeasonStandings
|
|
standings={regularSeasonStandings}
|
|
teamOwnerships={ownershipMap}
|
|
userParticipantIds={userParticipantIds}
|
|
showOtLosses={showOtLosses}
|
|
participantEvs={participantEvs}
|
|
displayMode={standingsDisplayMode as "flat" | "nhl-divisions" | "mlb-divisions"}
|
|
playoffSpots={playoffSpots}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Event schedule - hidden for bracket sports (the bracket itself is the event) */}
|
|
{scoringPattern !== "playoff_bracket" &&
|
|
(upcomingEvents.length > 0 || recentEvents.length > 0) && (
|
|
<div className="mb-6">
|
|
<EventSchedule
|
|
upcomingEvents={upcomingEvents}
|
|
recentEvents={recentEvents}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Group stage standings — shown for FIFA-style tournaments before/during group phase */}
|
|
{groupStandings.length > 0 && (
|
|
<div className="mb-6">
|
|
<GroupStageStandings groups={groupStandings} ownershipMap={ownershipMap} showEmpty={false} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Hide bracket display when standings exist but no bracket matches have been set yet */}
|
|
{!(scoringPattern === "playoff_bracket" && hasStandings && !hasBracket) && <SportSeasonDisplay
|
|
scoringPattern={scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points"}
|
|
sportSeasonName={sportsSeason.name}
|
|
sportName={sportsSeason.sport.name}
|
|
playoffMatches={playoffMatches}
|
|
playoffRounds={playoffRounds}
|
|
preEliminatedParticipants={preEliminatedParticipants}
|
|
participantPoints={participantPoints}
|
|
partialScoreParticipantIds={partialScoreParticipantIds}
|
|
seasonStandings={seasonStandings}
|
|
seasonIsFinalized={seasonIsFinalized}
|
|
qpStandings={qpStandings}
|
|
qpIsFinalized={sportsSeason.qualifyingPointsFinalized || false}
|
|
totalMajors={sportsSeason.totalMajors}
|
|
majorsCompleted={sportsSeason.majorsCompleted || 0}
|
|
canFinalize={
|
|
(sportsSeason.majorsCompleted || 0) >=
|
|
(sportsSeason.totalMajors || 0) &&
|
|
!sportsSeason.qualifyingPointsFinalized
|
|
}
|
|
teamOwnerships={teamOwnerships}
|
|
userParticipantIds={userParticipantIds}
|
|
scoringRules={season}
|
|
showOwnership={true}
|
|
/>}
|
|
|
|
{/* Regular season standings — show below bracket once bracket exists */}
|
|
{hasStandings && hasBracket && (
|
|
<div className="mt-8">
|
|
<RegularSeasonStandings
|
|
standings={regularSeasonStandings}
|
|
teamOwnerships={ownershipMap}
|
|
userParticipantIds={userParticipantIds}
|
|
showOtLosses={showOtLosses}
|
|
participantEvs={participantEvs}
|
|
displayMode={standingsDisplayMode as "flat" | "nhl-divisions" | "mlb-divisions"}
|
|
playoffSpots={playoffSpots}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|