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
This commit is contained in:
parent
27049319b8
commit
40fc541329
8 changed files with 39 additions and 17 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
|
|
|
|||
12
app/lib/event-utils.ts
Normal file
12
app/lib/event-utils.ts
Normal file
|
|
@ -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]);
|
||||
}
|
||||
|
|
@ -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 }>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{(event.eventType === "playoff_game" || event.eventType === "major_tournament") && (
|
||||
{isBracketEvent && (
|
||||
<Button variant="outline" asChild>
|
||||
<Link to={`/admin/sports-seasons/${sportsSeason.id}/events/${event.id}/bracket`}>
|
||||
<Brackets className="mr-2 h-4 w-4" />
|
||||
|
|
@ -490,7 +493,7 @@ export default function EventResults({
|
|||
)}
|
||||
|
||||
{/* Batch Paste Results — primary entry for qualifying events */}
|
||||
{event.isQualifyingEvent && event.eventType !== "final_standings" && event.eventType !== "playoff_game" && !event.isComplete && (
|
||||
{event.isQualifyingEvent && event.eventType !== "final_standings" && !isBracketEvent && !event.isComplete && (
|
||||
<BatchResultEntry
|
||||
participants={participants}
|
||||
sportsSeasonId={sportsSeason.id}
|
||||
|
|
@ -501,7 +504,7 @@ export default function EventResults({
|
|||
)}
|
||||
|
||||
{/* Regular Result Entry for other event types */}
|
||||
{event.eventType !== "final_standings" && event.eventType !== "playoff_game" && !event.isComplete && (
|
||||
{event.eventType !== "final_standings" && !isBracketEvent && !event.isComplete && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Add Result</CardTitle>
|
||||
|
|
@ -593,8 +596,8 @@ export default function EventResults({
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{/* Current Results - Hide for playoff events since they use the bracket */}
|
||||
{event.eventType !== "playoff_game" && (
|
||||
{/* Current Results - Hide for bracket events since they use the bracket */}
|
||||
{!isBracketEvent && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -716,8 +719,8 @@ export default function EventResults({
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{/* Bracket Explanation Card for Playoff Events */}
|
||||
{event.eventType === "playoff_game" && !participantResults?.length && (
|
||||
{/* Bracket Explanation Card for Bracket Events */}
|
||||
{isBracketEvent && !participantResults?.length && (
|
||||
<Card className="border-electric/30 bg-electric/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-electric">
|
||||
|
|
@ -751,12 +754,12 @@ export default function EventResults({
|
|||
|
||||
{/* Participant Results with Fantasy Points */}
|
||||
{participantResults && participantResults.length > 0 && (
|
||||
<Card className={event.eventType === "playoff_game" ? "border-emerald-500/30" : ""}>
|
||||
<Card className={isBracketEvent ? "border-emerald-500/30" : ""}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className={event.eventType === "playoff_game" ? "text-emerald-400" : ""}>
|
||||
{event.eventType === "playoff_game" ? (
|
||||
<CardTitle className={isBracketEvent ? "text-emerald-400" : ""}>
|
||||
{isBracketEvent ? (
|
||||
<>
|
||||
<Trophy className="inline mr-2 h-5 w-5" />
|
||||
Fantasy Points Awarded (from Bracket)
|
||||
|
|
@ -766,13 +769,13 @@ export default function EventResults({
|
|||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{event.eventType === "playoff_game"
|
||||
{isBracketEvent
|
||||
? `${participantResults.length} participants assigned placements from bracket results`
|
||||
: "Points calculated from bracket placements (sorted by position)"
|
||||
}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{event.eventType === "playoff_game" && event.isComplete && (
|
||||
{isBracketEvent && event.isComplete && (
|
||||
<Badge variant="default" className="bg-emerald-500">
|
||||
<CheckCircle2 className="mr-1 h-3 w-3" />
|
||||
Bracket Finalized
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
|
||||
return events.map((event) => ({
|
||||
...event,
|
||||
simulatorType: ss.sport.simulatorType ?? null,
|
||||
sportName: ss.sport.name,
|
||||
sportSeasonName: ss.name,
|
||||
leagueName: league.name,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
|
||||
return events.map((event) => ({
|
||||
...event,
|
||||
simulatorType: ss.sport.simulatorType ?? null,
|
||||
sportName: ss.sport.name,
|
||||
sportSeasonName: ss.name,
|
||||
leagueName: league.name,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue