Apply code review fixes to CS2 event view and schedule
Some checks failed
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m58s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Failing after 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped

- Fix "Advanced" badge in SwissStageTab: condition was `isAdvanced && stageEliminated === null`, missing teams eliminated at later stages; now just `isAdvanced`
- Default active tab to highest active Swiss stage rather than Champions whenever no playoff games have a winner yet
- Remove dead `eventResults` query from the CS2 loader branch (Cs2MajorEventView has no prop for it)
- Remove dead `teamsInStage` variable and unused `StageStatusBadge` component
- Serialize `createdAt`/`updatedAt` on playoff match rows in both CS2 and bracket loader branches (matches how seasonMatches dates are handled)
- Replace inline `formatEventDate` in event detail route with shared `~/lib/date-utils` version
- Add defensive DB-level limit to `getUpcomingScoringEvents` (max(limit*5, 50)) to cap unbounded fetch

https://claude.ai/code/session_01RYK7NCjcaah545979wupcM
This commit is contained in:
Claude 2026-06-16 01:14:10 +00:00
parent df70ef83c2
commit 398a03e8e1
No known key found for this signature in database
4 changed files with 31 additions and 58 deletions

View file

@ -54,35 +54,6 @@ const STAGE_LABELS: Record<StageTab, string> = {
champions: "Champions", champions: "Champions",
}; };
function StageStatusBadge({ result }: { result: Cs2StageResultWithName }) {
if (result.stageEliminated !== null) {
return (
<span className="flex items-center gap-1 text-xs text-red-400">
<XCircle className="h-3 w-3" />
Eliminated
</span>
);
}
const winsNeeded = 3;
const wins = result.stageEliminatedWins ?? 0;
if (result.stageEliminated === null && result.stageEntry !== null) {
// Null stageEliminated = either in progress or advanced
// If their stageEntry is less than the current stage context, they advanced
return (
<span className="flex items-center gap-1 text-xs text-emerald-400">
<CheckCircle2 className="h-3 w-3" />
Advanced
</span>
);
}
return (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
In Progress
</span>
);
}
function SwissStageTab({ function SwissStageTab({
stage, stage,
stageResults, stageResults,
@ -99,7 +70,6 @@ function SwissStageTab({
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o])); const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
const userIds = new Set(userParticipantIds); const userIds = new Set(userParticipantIds);
const teamsInStage = stageResults.filter((r) => r.stageEntry <= stage && (r.stageEliminated === null || r.stageEliminated >= stage));
const matchesInStage = matches.filter((m) => m.matchStage === stage); const matchesInStage = matches.filter((m) => m.matchStage === stage);
// Group teams by W-L record (approximated from stageEliminatedWins if eliminated, else unknown) // Group teams by W-L record (approximated from stageEliminatedWins if eliminated, else unknown)
@ -154,7 +124,7 @@ function SwissStageTab({
<span className="flex items-center gap-1 text-xs text-red-400"> <span className="flex items-center gap-1 text-xs text-red-400">
<XCircle className="h-3 w-3" /> Eliminated <XCircle className="h-3 w-3" /> Eliminated
</span> </span>
) : isAdvanced && r.stageEliminated === null ? ( ) : isAdvanced ? (
<span className="flex items-center gap-1 text-xs text-emerald-400"> <span className="flex items-center gap-1 text-xs text-emerald-400">
<CheckCircle2 className="h-3 w-3" /> Advanced <CheckCircle2 className="h-3 w-3" /> Advanced
</span> </span>
@ -218,8 +188,13 @@ export function Cs2MajorEventView({
teamOwnerships, teamOwnerships,
userParticipantIds, userParticipantIds,
}: Props) { }: Props) {
const hasChampions = playoffMatches.length > 0; const hasChampionsActivity = playoffMatches.some((m) => m.winnerId !== null);
const [activeTab, setActiveTab] = useState<StageTab>(hasChampions ? "champions" : 3); const highestActiveStage: 1 | 2 | 3 =
cs2StageResults.some((r) => r.stageEntry >= 3 || r.stageEliminated === 3) ? 3 :
cs2StageResults.some((r) => r.stageEntry >= 2 || r.stageEliminated === 2) ? 2 : 1;
const [activeTab, setActiveTab] = useState<StageTab>(
hasChampionsActivity ? "champions" : highestActiveStage
);
const tabs: StageTab[] = [1, 2, 3, "champions"]; const tabs: StageTab[] = [1, 2, 3, "champions"];
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o])); const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
@ -262,7 +237,7 @@ export function Cs2MajorEventView({
{/* Champions Stage bracket */} {/* Champions Stage bracket */}
{activeTab === "champions" && ( {activeTab === "champions" && (
hasChampions ? ( playoffMatches.length > 0 ? (
<PlayoffBracket <PlayoffBracket
matches={playoffMatches} matches={playoffMatches}
rounds={playoffRounds} rounds={playoffRounds}

View file

@ -296,13 +296,15 @@ export async function getUpcomingScoringEvents(
) { ) {
const db = providedDb || database(); const db = providedDb || database();
// Fetch all incomplete events for the season (typically few per season). // Over-fetch relative to the requested limit so the subsequent game-time
// re-sort has enough candidates without an unbounded query.
const events = await db.query.scoringEvents.findMany({ const events = await db.query.scoringEvents.findMany({
where: and( where: and(
eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId), eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
eq(schema.scoringEvents.isComplete, false) eq(schema.scoringEvents.isComplete, false)
), ),
orderBy: [asc(schema.scoringEvents.eventDate), asc(schema.scoringEvents.eventStartsAt), asc(schema.scoringEvents.createdAt)], orderBy: [asc(schema.scoringEvents.eventDate), asc(schema.scoringEvents.eventStartsAt), asc(schema.scoringEvents.createdAt)],
limit: Math.max(limit * 5, 50),
}); });
if (events.length === 0) return []; if (events.length === 0) return [];

View file

@ -99,7 +99,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
const simulatorType = sportsSeason.sport?.simulatorType ?? null; const simulatorType = sportsSeason.sport?.simulatorType ?? null;
type PlayoffMatchWithRelations = PlayoffMatch & { type PlayoffMatchWithRelations = Omit<PlayoffMatch, "createdAt" | "updatedAt"> & {
createdAt: string;
updatedAt: string;
participant1: { id: string; name: string } | null; participant1: { id: string; name: string } | null;
participant2: { id: string; name: string } | null; participant2: { id: string; name: string } | null;
winner: { id: string; name: string } | null; winner: { id: string; name: string } | null;
@ -115,17 +117,20 @@ export async function loader({ params, request }: Route.LoaderArgs) {
// CS2 Major — load Swiss stage data + bracket // CS2 Major — load Swiss stage data + bracket
if (simulatorType === "cs2_major_qualifying_points") { if (simulatorType === "cs2_major_qualifying_points") {
const [cs2StageResults, seasonMatches, playoffMatchRows, eventResultRows] = await Promise.all([ const [cs2StageResults, seasonMatches, playoffMatchRows] = await Promise.all([
getCs2StageResultsForEvent(eventId), getCs2StageResultsForEvent(eventId),
findSeasonMatchesByScoringEventId(eventId), findSeasonMatchesByScoringEventId(eventId),
db.query.playoffMatches.findMany({ db.query.playoffMatches.findMany({
where: eq(schema.playoffMatches.scoringEventId, eventId), where: eq(schema.playoffMatches.scoringEventId, eventId),
with: { participant1: true, participant2: true, winner: true, loser: true }, with: { participant1: true, participant2: true, winner: true, loser: true },
}), }),
getEventResults(eventId),
]); ]);
const playoffMatches = playoffMatchRows as PlayoffMatchWithRelations[]; const playoffMatches: PlayoffMatchWithRelations[] = playoffMatchRows.map((m) => ({
...m,
createdAt: m.createdAt.toISOString(),
updatedAt: m.updatedAt.toISOString(),
}));
const templateId = scoringEvent.bracketTemplateId ?? undefined; const templateId = scoringEvent.bracketTemplateId ?? undefined;
const template = templateId ? getBracketTemplate(templateId) : undefined; const template = templateId ? getBracketTemplate(templateId) : undefined;
const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template); const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template);
@ -147,13 +152,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
playoffMatches, playoffMatches,
playoffRounds, playoffRounds,
bracketTemplateId: templateId ?? null, bracketTemplateId: templateId ?? null,
eventResults: eventResultRows.map((r) => ({
id: r.id,
placement: r.placement,
qualifyingPointsAwarded: r.qualifyingPointsAwarded,
seasonParticipantId: r.seasonParticipantId,
participantName: r.seasonParticipant?.name ?? null,
})),
}; };
} }
@ -163,7 +161,11 @@ export async function loader({ params, request }: Route.LoaderArgs) {
where: eq(schema.playoffMatches.scoringEventId, eventId), where: eq(schema.playoffMatches.scoringEventId, eventId),
with: { participant1: true, participant2: true, winner: true, loser: true }, with: { participant1: true, participant2: true, winner: true, loser: true },
}); });
const playoffMatches = playoffMatchRows as PlayoffMatchWithRelations[]; const playoffMatches: PlayoffMatchWithRelations[] = playoffMatchRows.map((m) => ({
...m,
createdAt: m.createdAt.toISOString(),
updatedAt: m.updatedAt.toISOString(),
}));
const templateId = scoringEvent.bracketTemplateId ?? undefined; const templateId = scoringEvent.bracketTemplateId ?? undefined;
const template = templateId ? getBracketTemplate(templateId) : undefined; const template = templateId ? getBracketTemplate(templateId) : undefined;
const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template); const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template);

View file

@ -1,8 +1,8 @@
import { Link } from "react-router"; import { Link } from "react-router";
import { ArrowLeft, Calendar } from "lucide-react"; import { ArrowLeft, Calendar } from "lucide-react";
import { format } from "date-fns";
import type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId.events.$eventId"; import type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId.events.$eventId";
import { loader } from "./$leagueId.sports-seasons.$sportsSeasonId.events.$eventId.server"; import { loader } from "./$leagueId.sports-seasons.$sportsSeasonId.events.$eventId.server";
import { formatEventDate } from "~/lib/date-utils";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { import {
Card, Card,
@ -30,13 +30,6 @@ export function meta({ data }: Route.MetaArgs) {
export { loader }; export { loader };
function formatEventDate(dateStr: string | null | Date | undefined): string {
if (!dateStr) return "Date TBD";
const d = new Date(dateStr);
if (isNaN(d.getTime())) return String(dateStr);
return format(d, "MMM d, yyyy");
}
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;
@ -49,9 +42,10 @@ export default function EventDetailPage({ loaderData }: Route.ComponentProps) {
]) ])
); );
const eventDate = scoringEvent.eventStartsAt const eventDate =
? formatEventDate(scoringEvent.eventStartsAt) formatEventDate(scoringEvent.eventStartsAt as string | null) ??
: formatEventDate(scoringEvent.eventDate); formatEventDate(scoringEvent.eventDate) ??
"Date TBD";
return ( return (
<div className="container mx-auto py-8 px-4 max-w-5xl"> <div className="container mx-auto py-8 px-4 max-w-5xl">