From 40fc5413294848a000f1fe6c63c9aa77815e5821 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 21:25:09 +0000 Subject: [PATCH] Fix code review findings for major_tournament bracket support - Add isBracketMajor() helper (event-utils.ts) as single source of truth for distinguishing CS2/Tennis bracket events from golf manual-entry events - Replace all playoff_game-only guards in the admin event detail page with isBracketEvent, fixing: manual result forms, current results card, bracket explanation card, Bracket Finalized badge, and styling - Extend delete handler in scoring-event.ts to clean up seasonParticipantResults and recalculate standings when a major_tournament event is deleted - Add simulatorType to UpcomingParticipantEvent; thread it from home/upcoming-events loaders; fix isAllCompete in SportSeasonCard, UpcomingEventsCard, UpcomingCalendarPanel so CS2/Tennis bracket events show individual participant badges instead of counts https://claude.ai/code/session_01SFmJQxQZsKxvJ5rhbYk3za --- .../sport-season/UpcomingCalendarPanel.tsx | 3 ++- .../sport-season/UpcomingEventsCard.tsx | 3 ++- app/components/sports/SportSeasonCard.tsx | 3 ++- app/lib/event-utils.ts | 12 +++++++++ app/models/scoring-event.ts | 6 +++-- ...min.sports-seasons.$id.events.$eventId.tsx | 27 ++++++++++--------- app/routes/home.tsx | 1 + app/routes/upcoming-events.tsx | 1 + 8 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 app/lib/event-utils.ts diff --git a/app/components/sport-season/UpcomingCalendarPanel.tsx b/app/components/sport-season/UpcomingCalendarPanel.tsx index f881ffa..663c32d 100644 --- a/app/components/sport-season/UpcomingCalendarPanel.tsx +++ b/app/components/sport-season/UpcomingCalendarPanel.tsx @@ -11,6 +11,7 @@ import { } from "~/components/ui/card"; import { formatEventDate } from "~/lib/date-utils"; import type { UpcomingParticipantEvent } from "~/models/scoring-event"; +import { isBracketMajor } from "~/lib/event-utils"; export interface CalendarPanelEvent extends UpcomingParticipantEvent { sportName: string; @@ -61,7 +62,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" && event.eventType !== "group_stage_match"; + const isAllCompete = event.eventType !== "playoff_game" && event.eventType !== "group_stage_match" && !isBracketMajor(event.simulatorType); 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 fb93d79..77db30f 100644 --- a/app/components/sport-season/UpcomingEventsCard.tsx +++ b/app/components/sport-season/UpcomingEventsCard.tsx @@ -8,6 +8,7 @@ import { GradientIcon } from "~/components/ui/GradientIcon"; import { formatEventDate } from "~/lib/date-utils"; import { BRACKT_GRADIENT } from "~/lib/brand"; import type { CalendarPanelEvent } from "./UpcomingCalendarPanel"; +import { isBracketMajor } from "~/lib/event-utils"; interface Props { events: CalendarPanelEvent[]; @@ -45,7 +46,7 @@ function groupEvents(events: CalendarPanelEvent[]): GroupedEvent[] { for (const event of events) { const existing = map.get(event.id); - const isAllCompete = event.eventType !== "playoff_game" && event.eventType !== "group_stage_match"; + const isAllCompete = event.eventType !== "playoff_game" && event.eventType !== "group_stage_match" && !isBracketMajor(event.simulatorType); const leagueEntry: LeagueParticipants = { leagueId: event.leagueId ?? "unknown", diff --git a/app/components/sports/SportSeasonCard.tsx b/app/components/sports/SportSeasonCard.tsx index b9898f9..71d571f 100644 --- a/app/components/sports/SportSeasonCard.tsx +++ b/app/components/sports/SportSeasonCard.tsx @@ -11,6 +11,7 @@ import { import { Trophy, Target, Flag, Calendar, Users } from "lucide-react"; import { formatEventDate } from "~/lib/date-utils"; import type { UpcomingParticipantEvent } from "~/models/scoring-event"; +import { isBracketMajor } from "~/lib/event-utils"; import { SportIcon } from "~/components/SportIcon"; interface SportSeasonCardProps { @@ -31,7 +32,7 @@ function EventLine({ event }: { event: UpcomingParticipantEvent }) { const dateStr = gameDate ? format(gameDate, "MMM d") : formatEventDate(event.eventDate); - const isAllCompete = event.eventType !== "playoff_game"; + const isAllCompete = event.eventType !== "playoff_game" && !isBracketMajor(event.simulatorType); const count = event.relevantParticipants.length; const displayName = event.matchLabel ? `${event.name} — ${event.matchLabel}` diff --git a/app/lib/event-utils.ts b/app/lib/event-utils.ts new file mode 100644 index 0000000..e602eab --- /dev/null +++ b/app/lib/event-utils.ts @@ -0,0 +1,12 @@ +const BRACKET_MAJOR_SIMULATOR_TYPES = [ + "cs2_major_qualifying_points", + "tennis_qualifying_points", +] as const; + +/** + * Returns true for major_tournament events that use a bracket (CS2, Tennis). + * Golf (golf_qualifying_points) uses manual result entry and returns false. + */ +export function isBracketMajor(simulatorType: string | null | undefined): boolean { + return BRACKET_MAJOR_SIMULATOR_TYPES.includes(simulatorType as typeof BRACKET_MAJOR_SIMULATOR_TYPES[number]); +} diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index 162ddc8..8e474af 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -232,7 +232,7 @@ export async function deleteScoringEvent( } await db.transaction(async (tx) => { - if (event.eventType === "playoff_game") { + if (event.eventType === "playoff_game" || event.eventType === "major_tournament") { await tx .delete(schema.seasonParticipantResults) .where(eq(schema.seasonParticipantResults.sportsSeasonId, event.sportsSeasonId)); @@ -241,7 +241,7 @@ export async function deleteScoringEvent( await tx.delete(schema.scoringEvents).where(eq(schema.scoringEvents.id, eventId)); }); - if (event.eventType === "playoff_game") { + if (event.eventType === "playoff_game" || event.eventType === "major_tournament") { await recalculateAffectedLeagues(event.sportsSeasonId, db); } @@ -355,6 +355,8 @@ export interface UpcomingParticipantEvent { matchLabel: string | null; eventType: string; sportsSeasonId: string; + /** simulatorType from the parent sport — used to distinguish bracket-majors from manual-entry majors */ + simulatorType?: string | null; /** Only the current user's drafted participants involved in this event */ relevantParticipants: Array<{ id: string; name: string }>; } diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx index 882a4e6..a529cd3 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx @@ -22,6 +22,7 @@ import { import { Badge } from "~/components/ui/badge"; import { ArrowLeft, Trophy, CheckCircle2, Pencil, Trash2, Brackets, Save } from "lucide-react"; import { getEventTypeLabel } from "~/models/scoring-event"; +import { isBracketMajor } from "~/lib/event-utils"; import { Table, TableBody, @@ -97,6 +98,8 @@ export default function EventResults({ const { sportsSeason, event, participants, results, participantResults, seasonResults, notParticipatingIds } = loaderData; const [hasChanges, setHasChanges] = useState(false); + const isBracketEvent = event.eventType === "playoff_game" || isBracketMajor(sportsSeason.sport?.simulatorType); + // Create a map of participants with results for easy lookup const participantResultsMap = new Map( results.map((r: { seasonParticipant: { id: string } }) => [r.seasonParticipant.id, r]) @@ -232,7 +235,7 @@ export default function EventResults({

- {(event.eventType === "playoff_game" || event.eventType === "major_tournament") && ( + {isBracketEvent && (