From e2b178221ad782c6b2100fc10ad1c40cb205e143 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Sat, 21 Mar 2026 09:44:05 -0700 Subject: [PATCH] Add oxlint linting setup with zero errors (#194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add oxlint and fix all lint errors - Install oxlint, add .oxlintrc.json with rules for TypeScript/React - Add npm run lint / lint:fix scripts - Add Claude PostToolUse hook to run oxlint on every edited file - Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array - Fix no-array-index-key (use stable keys or suppress positional cases) - Fix exhaustive-deps missing dependency in useEffect - Promote exhaustive-deps and no-array-index-key to errors - Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined) Co-Authored-By: Claude Sonnet 4.6 * Fix no-explicit-any warnings and upgrade tsconfig to ES2023 - Replace all `any` types with proper types or `unknown` across ~20 files - Add typed socket payload interfaces in draft route and useDraftSocket - Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch) - Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed - Fix cascading type errors uncovered by removing any: Map.get narrowing, participant relation types, ChartDataPoint, Partial indexing - Add ParticipantResultWithParticipant type to participant-result model - Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult) - Fix duplicate getQPStandings import in sportsSeasonId.server.ts Co-Authored-By: Claude Sonnet 4.6 * Promote no-explicit-any to error Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .claude/settings.json | 15 + .oxlintrc.json | 58 +++ app/components/DraftGrid.tsx | 13 +- app/components/__tests__/DraftGrid.test.tsx | 12 +- app/components/draft/ConnectionOverlay.tsx | 10 +- app/components/draft/DraftGridSection.tsx | 4 +- app/components/draft/RecentPicksSection.tsx | 2 +- app/components/draft/SidebarRecentPicks.tsx | 2 +- app/components/scoring/PlayoffBracket.tsx | 10 +- app/components/scoring/SeasonStandings.tsx | 4 +- app/components/scoring/SportSeasonDisplay.tsx | 2 +- app/components/sport-season/EventSchedule.tsx | 2 +- .../sport-season/RegularSeasonStandings.tsx | 22 +- .../standings/TeamScoreBreakdown.tsx | 2 +- app/entry.server.tsx | 6 +- app/hooks/useDraftSocket.ts | 8 +- app/lib/__tests__/draft-eligibility.test.ts | 6 +- app/lib/bracket-templates.ts | 2 +- app/lib/owner-map.ts | 2 +- app/lib/season-helpers.server.ts | 2 +- app/models/__tests__/afl-finals.test.ts | 12 - .../__tests__/bracket-integration.test.ts | 2 +- app/models/__tests__/bracket-seeding.test.ts | 8 +- .../__tests__/bracket-templates.test.ts | 2 +- .../participant-expected-value.test.ts | 4 +- .../__tests__/process-match-result.test.ts | 2 +- .../__tests__/qualifying-points.test.ts | 4 +- .../__tests__/scoring-event-dashboard.test.ts | 5 - app/models/__tests__/season-standings.test.ts | 2 +- .../__tests__/standings-snapshots.test.ts | 2 +- app/models/__tests__/tiebreaker-logic.test.ts | 2 +- app/models/draft-utils.ts | 2 +- app/models/league.ts | 4 +- app/models/participant-result.ts | 5 +- app/models/participant-season-result.ts | 4 +- app/models/playoff-match.ts | 25 +- app/models/qualifying-points.ts | 2 +- app/models/regular-season-standings.ts | 6 +- app/models/scoring-calculator.ts | 12 +- app/models/scoring-event.ts | 7 +- app/models/standings.ts | 12 +- app/models/tournament-group.ts | 2 +- ...sons.$id.events.$eventId.bracket.server.ts | 6 +- ...ts-seasons.$id.events.$eventId.bracket.tsx | 37 +- ...min.sports-seasons.$id.events.$eventId.tsx | 12 +- .../admin.sports-seasons.$id.events.server.ts | 4 +- .../admin.sports-seasons.$id.events.tsx | 2 +- ...min.sports-seasons.$id.expected-values.tsx | 2 +- .../admin.sports-seasons.$id.futures-odds.tsx | 4 +- .../admin.sports-seasons.$id.participants.tsx | 2 +- ...n.sports-seasons.$id.regular-standings.tsx | 6 +- .../admin.sports-seasons.$id.standings.tsx | 4 +- app/routes/admin.sports-seasons.$id.tsx | 12 +- app/routes/admin.sports-seasons.new.tsx | 8 +- app/routes/admin.standings-snapshots.tsx | 3 +- app/routes/api/webhooks/clerk.ts | 8 +- app/routes/home.tsx | 2 +- .../$leagueId.draft-board.$seasonId.tsx | 15 +- .../leagues/$leagueId.draft.$seasonId.tsx | 97 +++-- app/routes/leagues/$leagueId.server.ts | 6 +- app/routes/leagues/$leagueId.settings.tsx | 10 +- ...d.sports-seasons.$sportsSeasonId.server.ts | 18 +- ...eagueId.sports-seasons.$sportsSeasonId.tsx | 24 +- .../leagues/$leagueId.standings.$seasonId.tsx | 4 +- app/routes/leagues/$leagueId.tsx | 4 +- .../__tests__/draft-board-access.test.ts | 2 +- .../__tests__/draft-reconnection-sync.test.ts | 4 +- .../leagues/__tests__/settings-update.test.ts | 6 +- app/routes/leagues/new.tsx | 2 +- app/routes/test-socket.tsx | 3 +- .../__tests__/bracket-simulator.test.ts | 6 +- .../__tests__/probability-engine.test.ts | 1 - .../__tests__/probability-updater.test.ts | 5 + .../__tests__/ncaam-simulator.test.ts | 4 +- .../__tests__/ncaaw-simulator.test.ts | 4 +- .../__tests__/ucl-simulator.test.ts | 6 +- .../simulations/auto-racing-simulator.ts | 6 +- app/services/simulations/bracket-simulator.ts | 4 +- app/services/simulations/ncaam-simulator.ts | 2 +- app/services/simulations/ncaaw-simulator.ts | 2 +- app/services/simulations/ucl-simulator.ts | 4 +- .../standings-sync/__tests__/sync.test.ts | 2 +- app/services/standings-sync/nba.ts | 4 +- app/test/setup.ts | 2 +- app/utils/sports-data-sync.server.ts | 2 +- database/context.browser-stub.ts | 2 +- database/context.ts | 2 +- package-lock.json | 369 ++++++++++++++++++ package.json | 5 +- react-router.config.ts | 6 +- server/app.ts | 6 +- server/snapshots.ts | 2 +- server/socket.ts | 17 +- server/types.d.ts | 2 +- tsconfig.node.json | 4 +- tsconfig.server.json | 4 +- tsconfig.vite.json | 2 +- 97 files changed, 767 insertions(+), 325 deletions(-) create mode 100644 .claude/settings.json create mode 100644 .oxlintrc.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..ca25008 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "jq -r '.tool_input.file_path // empty' | xargs -I{} sh -c 'case \"{}\" in *.ts|*.tsx) ./node_modules/.bin/oxlint \"{}\" 2>&1 ;; esac'" + } + ] + } + ] + } +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..8d3c6b6 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,58 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "vitest", "unicorn", "import"], + "env": { + "browser": true, + "node": true, + "es2022": true + }, + "categories": { + "correctness": "error", + "suspicious": "warn" + }, + "rules": { + "no-console": "warn", + "no-var": "error", + "prefer-const": "error", + "eqeqeq": ["error", "always"], + "no-unused-vars": "off", + "no-undef": "off", + + "typescript/no-unused-vars": [ + "error", + { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } + ], + "typescript/no-explicit-any": "error", + "typescript/no-non-null-assertion": "warn", + "typescript/consistent-type-imports": [ + "error", + { "prefer": "type-imports" } + ], + + "react/react-in-jsx-scope": "off", + "react/rules-of-hooks": "error", + "react/exhaustive-deps": "error", + "react/jsx-key": "error", + "react/no-array-index-key": "error", + "react/self-closing-comp": "warn", + + "import/no-duplicates": "error" + }, + "overrides": [ + { + "files": ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx", "cypress/**"], + "rules": { + "no-console": "off", + "typescript/no-explicit-any": "off" + } + } + ], + "ignorePatterns": [ + "build/**", + "dist/**", + "node_modules/**", + "drizzle/**", + ".react-router/**", + "cypress/fixtures/**" + ] +} diff --git a/app/components/DraftGrid.tsx b/app/components/DraftGrid.tsx index 11b3c26..71c30f0 100644 --- a/app/components/DraftGrid.tsx +++ b/app/components/DraftGrid.tsx @@ -6,6 +6,13 @@ import { ContextMenuTrigger, } from "~/components/ui/context-menu"; +interface DraftCell { + pickNumber: number; + participant: { id: string; name: string }; + sport: { id: string; name: string }; + team: { id: string; name: string }; +} + interface DraftGridProps { draftSlots: Array<{ id: string; @@ -15,7 +22,7 @@ interface DraftGridProps { name: string; }; }>; - draftGrid: any[][]; + draftGrid: Array>; currentPick: number; teamTimers?: Record; formatTime?: (seconds: number | undefined) => string; @@ -92,11 +99,11 @@ export function DraftGrid({ const round = roundIndex + 1; const isEvenRound = round % 2 === 0; const displayPicks = isEvenRound - ? [...roundPicks].reverse() + ? [...roundPicks].toReversed() : roundPicks; return ( -
+
{displayPicks.map((cell, index) => { const actualIndex = isEvenRound ? roundPicks.length - 1 - index diff --git a/app/components/__tests__/DraftGrid.test.tsx b/app/components/__tests__/DraftGrid.test.tsx index 97d64ac..a7f069d 100644 --- a/app/components/__tests__/DraftGrid.test.tsx +++ b/app/components/__tests__/DraftGrid.test.tsx @@ -6,12 +6,12 @@ import { mockDraftSlots } from '~/test/fixtures/team'; describe('DraftGrid Component', () => { const mockGrid = [ [ - { participant: { name: 'Player 1' }, sport: { name: 'NFL' } }, + { pickNumber: 1, participant: { id: 'p1', name: 'Player 1' }, sport: { id: 's1', name: 'NFL' }, team: { id: 't1', name: 'Team 1' } }, null, ], [ null, - { participant: { name: 'Player 2' }, sport: { name: 'NBA' } }, + { pickNumber: 4, participant: { id: 'p2', name: 'Player 2' }, sport: { id: 's2', name: 'NBA' }, team: { id: 't2', name: 'Team 2' } }, ], ]; @@ -143,12 +143,12 @@ describe('DraftGrid Component', () => { it('should reverse picks on even rounds', () => { const snakeGrid = [ [ - { participant: { name: 'Pick 1' }, sport: { name: 'NFL' } }, - { participant: { name: 'Pick 2' }, sport: { name: 'NBA' } }, + { pickNumber: 1, participant: { id: 'p1', name: 'Pick 1' }, sport: { id: 's1', name: 'NFL' }, team: { id: 't1', name: 'Team 1' } }, + { pickNumber: 2, participant: { id: 'p2', name: 'Pick 2' }, sport: { id: 's2', name: 'NBA' }, team: { id: 't2', name: 'Team 2' } }, ], [ - { participant: { name: 'Pick 4' }, sport: { name: 'MLB' } }, - { participant: { name: 'Pick 3' }, sport: { name: 'NHL' } }, + { pickNumber: 3, participant: { id: 'p3', name: 'Pick 4' }, sport: { id: 's1', name: 'MLB' }, team: { id: 't2', name: 'Team 2' } }, + { pickNumber: 4, participant: { id: 'p4', name: 'Pick 3' }, sport: { id: 's2', name: 'NHL' }, team: { id: 't1', name: 'Team 1' } }, ], ]; diff --git a/app/components/draft/ConnectionOverlay.tsx b/app/components/draft/ConnectionOverlay.tsx index db34a8b..f1f6916 100644 --- a/app/components/draft/ConnectionOverlay.tsx +++ b/app/components/draft/ConnectionOverlay.tsx @@ -41,8 +41,8 @@ export function ConnectionOverlay({
) : (
-
-
+
+
)} @@ -85,15 +85,15 @@ export function ConnectionOverlay({
+ />
+ />
+ />
)}
diff --git a/app/components/draft/DraftGridSection.tsx b/app/components/draft/DraftGridSection.tsx index e2d6806..6e6602c 100644 --- a/app/components/draft/DraftGridSection.tsx +++ b/app/components/draft/DraftGridSection.tsx @@ -168,11 +168,11 @@ export const DraftGridSection = memo(function DraftGridSection({ const round = roundIndex + 1; const isEvenRound = round % 2 === 0; const displayPicks = isEvenRound - ? [...roundPicks].reverse() + ? [...roundPicks].toReversed() : roundPicks; return ( -
+
{/* Round label */}
R{round} diff --git a/app/components/draft/RecentPicksSection.tsx b/app/components/draft/RecentPicksSection.tsx index 49c7c9a..67a645d 100644 --- a/app/components/draft/RecentPicksSection.tsx +++ b/app/components/draft/RecentPicksSection.tsx @@ -27,7 +27,7 @@ export function RecentPicksSection({ picks }: RecentPicksSectionProps) {
{picks .slice() - .reverse() + .toReversed() .slice(0, 10) .map((pick) => (
{picks .slice() - .reverse() + .toReversed() .map((pick) => (
>(); - let hasScore = false; + let _hasScore = false; let bracketWinner: Participant | null = null; const lastRound = rounds[rounds.length - 1]; @@ -209,7 +209,7 @@ export function PlayoffBracket({ match.loserId === match.participant1Id ? match.participant1Score : match.participant2Score; - if (loserScore) hasScore = true; + if (loserScore) _hasScore = true; if (!losersByRound.has(match.round)) losersByRound.set(match.round, []); losersByRound.get(match.round)!.push({ participant: match.loser, @@ -574,12 +574,12 @@ export function PlayoffBracket({ )} - {rankedEntries.filter((entry) => isDraftedOrScoring(entry.participant.id)).map((entry, i) => { + {rankedEntries.filter((entry) => isDraftedOrScoring(entry.participant.id)).map((entry) => { const isOwned = userParticipantSet.has(entry.participant.id); const pts = pointsMap.get(entry.participant.id); return ( diff --git a/app/components/scoring/SeasonStandings.tsx b/app/components/scoring/SeasonStandings.tsx index 2989c85..8f0378c 100644 --- a/app/components/scoring/SeasonStandings.tsx +++ b/app/components/scoring/SeasonStandings.tsx @@ -62,7 +62,7 @@ export function SeasonStandings({ showOwnership = true, isFinalized = false, title = "Championship Standings", - description, + description: _description, }: SeasonStandingsProps) { const userParticipantSet = new Set(userParticipantIds); // Create ownership map for fast lookup @@ -191,7 +191,7 @@ export function SeasonStandings({ {standings - .sort((a, b) => a.position - b.position) + .toSorted((a, b) => a.position - b.position) .map((standing) => { const ownership = getOwnership(standing.participant.id); const isTied = checkIfTied(standing); diff --git a/app/components/scoring/SportSeasonDisplay.tsx b/app/components/scoring/SportSeasonDisplay.tsx index 2cc4df5..6224714 100644 --- a/app/components/scoring/SportSeasonDisplay.tsx +++ b/app/components/scoring/SportSeasonDisplay.tsx @@ -121,7 +121,7 @@ interface SportSeasonDisplayProps { export function SportSeasonDisplay({ scoringPattern, sportSeasonName, - sportName, + sportName: _sportName, playoffMatches = [], playoffRounds = [], preEliminatedParticipants = [], diff --git a/app/components/sport-season/EventSchedule.tsx b/app/components/sport-season/EventSchedule.tsx index 3af6972..35f5410 100644 --- a/app/components/sport-season/EventSchedule.tsx +++ b/app/components/sport-season/EventSchedule.tsx @@ -110,7 +110,7 @@ export function EventSchedule({ upcomingEvents, recentEvents }: EventSchedulePro !event.isComplete && (() => { const isPast = event.eventStartsAt ? new Date(event.eventStartsAt) < new Date() - : event.eventDate != null && event.eventDate < new Date().toISOString().split("T")[0]; + : event.eventDate !== null && event.eventDate < new Date().toISOString().split("T")[0]; return isPast && ( Results Pending diff --git a/app/components/sport-season/RegularSeasonStandings.tsx b/app/components/sport-season/RegularSeasonStandings.tsx index eaa8636..765b069 100644 --- a/app/components/sport-season/RegularSeasonStandings.tsx +++ b/app/components/sport-season/RegularSeasonStandings.tsx @@ -26,7 +26,7 @@ interface StandingRow { lastTen: string | null; homeRecord: string | null; awayRecord: string | null; - syncedAt: string | null; + syncedAt: Date | string | null; participant: { id: string; name: string; shortName?: string | null }; } @@ -61,7 +61,7 @@ interface TableSection { // ─── Formatting ─────────────────────────────────────────────────────────────── function formatWinPct(winPct: string | null, wins: number, gamesPlayed: number): string { - if (winPct != null) { + if (winPct !== null) { const pct = parseFloat(winPct); if (!isNaN(pct)) return pct.toFixed(3); } @@ -70,7 +70,7 @@ function formatWinPct(winPct: string | null, wins: number, gamesPlayed: number): } function formatGB(gamesBack: string | null): string { - if (gamesBack == null) return "—"; + if (gamesBack === null) return "—"; const gb = parseFloat(gamesBack); if (isNaN(gb) || gb === 0) return "—"; return gb % 1 === 0 ? gb.toString() : gb.toFixed(1); @@ -96,7 +96,7 @@ function buildFlatSections( conference: "", sections: [ { - rows: [...standings].sort( + rows: [...standings].toSorted( (a, b) => (a.leagueRank ?? 99) - (b.leagueRank ?? 99) ), rankMode: "conference", @@ -118,7 +118,7 @@ function buildFlatSections( conference, sections: [ { - rows: [...rows].sort( + rows: [...rows].toSorted( (a, b) => (a.conferenceRank ?? a.leagueRank ?? 99) - (b.conferenceRank ?? b.leagueRank ?? 99) @@ -154,9 +154,9 @@ function buildNhlSections( name, qualifiers: divRows .filter((r) => (r.divisionRank ?? 99) <= 3) - .sort((a, b) => (a.divisionRank ?? 99) - (b.divisionRank ?? 99)), + .toSorted((a, b) => (a.divisionRank ?? 99) - (b.divisionRank ?? 99)), })) - .sort( + .toSorted( (a, b) => (a.qualifiers[0]?.conferenceRank ?? 99) - (b.qualifiers[0]?.conferenceRank ?? 99) @@ -164,7 +164,7 @@ function buildNhlSections( const wildCard = rows .filter((r) => (r.divisionRank ?? 99) > 3) - .sort( + .toSorted( (a, b) => (a.conferenceRank ?? a.leagueRank ?? 99) - (b.conferenceRank ?? b.leagueRank ?? 99) @@ -243,7 +243,7 @@ function StandingsTable({ if (section.heading) { sectionRows.push( - + {hasEvs && ( - {ev != null ? ( + {ev !== null ? ( {parseFloat(ev).toFixed(1)} @@ -388,7 +388,7 @@ export function RegularSeasonStandings({ const lastSyncedAt = standings .map((s) => s.syncedAt) .filter(Boolean) - .sort() + .toSorted() .at(-1); const groups = diff --git a/app/components/standings/TeamScoreBreakdown.tsx b/app/components/standings/TeamScoreBreakdown.tsx index fef65bb..dde5e39 100644 --- a/app/components/standings/TeamScoreBreakdown.tsx +++ b/app/components/standings/TeamScoreBreakdown.tsx @@ -61,7 +61,7 @@ export function TeamScoreBreakdown({ // Flatten all picks sorted by sport name then pick number const allPicks = breakdown.picks .slice() - .sort((a, b) => { + .toSorted((a, b) => { const sportCmp = a.participant.sport.localeCompare(b.participant.sport); return sportCmp !== 0 ? sportCmp : a.pickNumber - b.pickNumber; }); diff --git a/app/entry.server.tsx b/app/entry.server.tsx index d6dbbf0..6240661 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -21,15 +21,15 @@ async function handleRequest( routerContext: EntryContext, // If you have middleware enabled: // loadContext: RouterContextProvider - loadContext: AppLoadContext + _loadContext: AppLoadContext ) { return new Promise((resolve, reject) => { let shellRendered = false; - let userAgent = request.headers.get("user-agent"); + const userAgent = request.headers.get("user-agent"); // Ensure requests from bots and SPA Mode renders wait for all content to load before responding // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation - let readyOption: keyof RenderToPipeableStreamOptions = + const readyOption: keyof RenderToPipeableStreamOptions = (userAgent && isbot(userAgent)) || routerContext.isSpaMode ? "onAllReady" : "onShellReady"; diff --git a/app/hooks/useDraftSocket.ts b/app/hooks/useDraftSocket.ts index 37f2bee..1daf146 100644 --- a/app/hooks/useDraftSocket.ts +++ b/app/hooks/useDraftSocket.ts @@ -9,9 +9,11 @@ interface UseDraftSocketReturn { /** Increments each time a new socket instance is created. Include in * useEffect deps so handler effects re-register on socket recreation. */ socketVersion: number; + // eslint-disable-next-line typescript/no-explicit-any -- socket.io callbacks are untyped at the hook level on: (event: string, callback: (...args: any[]) => void) => void; + // eslint-disable-next-line typescript/no-explicit-any off: (event: string, callback?: (...args: any[]) => void) => void; - emit: (event: string, ...args: any[]) => void; + emit: (event: string, ...args: unknown[]) => void; } export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocketReturn { @@ -131,15 +133,17 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke // has run (socketRef.current === null) are silently no-ops. Consumers should // only call them inside their own useEffect, not during render or synchronously // after mount. + // eslint-disable-next-line typescript/no-explicit-any -- socket.io callbacks are untyped at the hook level const on = useCallback((event: string, callback: (...args: any[]) => void) => { socketRef.current?.on(event, callback); }, []); + // eslint-disable-next-line typescript/no-explicit-any const off = useCallback((event: string, callback?: (...args: any[]) => void) => { socketRef.current?.off(event, callback); }, []); - const emit = useCallback((event: string, ...args: any[]) => { + const emit = useCallback((event: string, ...args: unknown[]) => { socketRef.current?.emit(event, ...args); }, []); diff --git a/app/lib/__tests__/draft-eligibility.test.ts b/app/lib/__tests__/draft-eligibility.test.ts index 0ba225f..bcbe71e 100644 --- a/app/lib/__tests__/draft-eligibility.test.ts +++ b/app/lib/__tests__/draft-eligibility.test.ts @@ -599,7 +599,7 @@ describe("calculateDraftEligibility", () => { // Team1 picks NFL allPicks = [createPick("team1", "nfl1", "nfl", "NFL")]; - let team2Eligibility = calculateDraftEligibility( + const team2Eligibility = calculateDraftEligibility( "team2", [], allPicks, @@ -619,7 +619,7 @@ describe("calculateDraftEligibility", () => { createPick("team1", "nfl1", "nfl", "NFL"), createPick("team2", "nfl2", "nfl", "NFL"), ]; - let team3Eligibility = calculateDraftEligibility( + const team3Eligibility = calculateDraftEligibility( "team3", [], allPicks, @@ -642,7 +642,7 @@ describe("calculateDraftEligibility", () => { ]; // Now team1 wants a second NFL (flex pick) - let team1Eligibility = calculateDraftEligibility( + const team1Eligibility = calculateDraftEligibility( "team1", [createPick("team1", "nfl1", "nfl", "NFL")], allPicks, diff --git a/app/lib/bracket-templates.ts b/app/lib/bracket-templates.ts index e1fba39..8190e06 100644 --- a/app/lib/bracket-templates.ts +++ b/app/lib/bracket-templates.ts @@ -603,7 +603,7 @@ export function getOrderedRoundsFromMatches( for (const m of matches) { roundMatchCounts.set(m.round, (roundMatchCounts.get(m.round) || 0) + 1); } - return Array.from(roundMatchCounts.keys()).sort( + return Array.from(roundMatchCounts.keys()).toSorted( (a, b) => (roundMatchCounts.get(b) || 0) - (roundMatchCounts.get(a) || 0) ); } diff --git a/app/lib/owner-map.ts b/app/lib/owner-map.ts index e7d2656..04e1350 100644 --- a/app/lib/owner-map.ts +++ b/app/lib/owner-map.ts @@ -15,7 +15,7 @@ export async function buildOwnerMap( const userByClerkId = new Map( userRows .map((u) => [u.clerkId, getUserDisplayName(u)] as const) - .filter((entry): entry is [string, string] => entry[1] != null) + .filter((entry): entry is [string, string] => entry[1] !== null && entry[1] !== undefined) ); const ownerMap: Record = {}; diff --git a/app/lib/season-helpers.server.ts b/app/lib/season-helpers.server.ts index 9043f94..8a80d45 100644 --- a/app/lib/season-helpers.server.ts +++ b/app/lib/season-helpers.server.ts @@ -1,6 +1,6 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; -import { eq, and } from "drizzle-orm"; +import { eq } from "drizzle-orm"; /** * Check if a season is complete diff --git a/app/models/__tests__/afl-finals.test.ts b/app/models/__tests__/afl-finals.test.ts index 33d07bc..1fb5201 100644 --- a/app/models/__tests__/afl-finals.test.ts +++ b/app/models/__tests__/afl-finals.test.ts @@ -182,18 +182,6 @@ describe("AFL Finals System - Phase 3.3", () => { describe("AFL Point Distribution", () => { it("has all 8 scoring placements covered", () => { - // Verify that all placements 1-8 are accounted for in AFL system - const placements = { - 1: "Grand Final winner", - 2: "Grand Final loser", - 3: "Preliminary Final loser (shared with 4th)", - 4: "Preliminary Final loser (shared with 3rd)", - 5: "Semi-Final loser (shared with 6th)", - 6: "Semi-Final loser (shared with 5th)", - 7: "Elimination Final loser (shared with 8th)", - 8: "Elimination Final loser (shared with 7th)", - }; - // Count scoring rounds const scoringRounds = AFL_10.rounds.filter(r => r.isScoring); expect(scoringRounds).toHaveLength(4); // EF, SF, PF, GF diff --git a/app/models/__tests__/bracket-integration.test.ts b/app/models/__tests__/bracket-integration.test.ts index 3b0aa6d..90bc2c1 100644 --- a/app/models/__tests__/bracket-integration.test.ts +++ b/app/models/__tests__/bracket-integration.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { SIMPLE_16, SIMPLE_32, NCAA_68 } from "~/lib/bracket-templates"; import { calculateFantasyPoints, calculateAveragedPoints, type ScoringRules } from "../scoring-rules"; diff --git a/app/models/__tests__/bracket-seeding.test.ts b/app/models/__tests__/bracket-seeding.test.ts index 3990187..05ad960 100644 --- a/app/models/__tests__/bracket-seeding.test.ts +++ b/app/models/__tests__/bracket-seeding.test.ts @@ -101,7 +101,7 @@ describe("Bracket Seeding", () => { it("uses all 16 seeds exactly once", () => { const pairs = generateStandardSeeding(16); - const allSeeds = pairs.flat().map(s => s + 1).sort((a, b) => a - b); + const allSeeds = pairs.flat().map(s => s + 1).toSorted((a, b) => a - b); expect(allSeeds).toEqual(Array.from({ length: 16 }, (_, i) => i + 1)); }); @@ -114,7 +114,7 @@ describe("Bracket Seeding", () => { const seed1 = firstMatch[0] + 1; const seed2 = firstMatch[1] + 1; - expect([seed1, seed2].sort()).toEqual([1, 16]); + expect([seed1, seed2].toSorted()).toEqual([1, 16]); }); it("has proper matchup sums for balance", () => { @@ -167,12 +167,12 @@ describe("Bracket Seeding", () => { const seed1 = firstMatch[0] + 1; const seed2 = firstMatch[1] + 1; - expect([seed1, seed2].sort()).toEqual([1, 32]); + expect([seed1, seed2].toSorted()).toEqual([1, 32]); }); it("uses all 32 seeds exactly once", () => { const pairs = generateStandardSeeding(32); - const allSeeds = pairs.flat().map(s => s + 1).sort((a, b) => a - b); + const allSeeds = pairs.flat().map(s => s + 1).toSorted((a, b) => a - b); expect(allSeeds).toEqual(Array.from({ length: 32 }, (_, i) => i + 1)); }); diff --git a/app/models/__tests__/bracket-templates.test.ts b/app/models/__tests__/bracket-templates.test.ts index d3a82f4..3434560 100644 --- a/app/models/__tests__/bracket-templates.test.ts +++ b/app/models/__tests__/bracket-templates.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; +import { describe, it, expect } from "vitest"; import { SIMPLE_4, SIMPLE_8, diff --git a/app/models/__tests__/participant-expected-value.test.ts b/app/models/__tests__/participant-expected-value.test.ts index dfcdea1..26cd891 100644 --- a/app/models/__tests__/participant-expected-value.test.ts +++ b/app/models/__tests__/participant-expected-value.test.ts @@ -12,7 +12,7 @@ import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calcul */ describe("participant-expected-value model", () => { - const defaultScoring: ScoringRules = { + const _defaultScoring: ScoringRules = { pointsFor1st: 100, pointsFor2nd: 70, pointsFor3rd: 50, @@ -23,7 +23,7 @@ describe("participant-expected-value model", () => { pointsFor8th: 15, }; - const validProbabilities: ProbabilityDistribution = { + const _validProbabilities: ProbabilityDistribution = { probFirst: 20, probSecond: 20, probThird: 15, diff --git a/app/models/__tests__/process-match-result.test.ts b/app/models/__tests__/process-match-result.test.ts index 1b9ffb5..b45bf85 100644 --- a/app/models/__tests__/process-match-result.test.ts +++ b/app/models/__tests__/process-match-result.test.ts @@ -237,7 +237,7 @@ describe("processMatchResult", () => { it("does not overwrite a finalized row with a partial score", async () => { // findFirst returns a finalized row for the first participant (loser) const existing = { id: "result-1", isPartialScore: false }; - const { db, insertedRows, updatedRows } = makeDb(existing); + const { db, updatedRows } = makeDb(existing); await processMatchResult( { ...BASE, round: "Quarterfinals", isScoring: true }, diff --git a/app/models/__tests__/qualifying-points.test.ts b/app/models/__tests__/qualifying-points.test.ts index 07abf9f..e326a81 100644 --- a/app/models/__tests__/qualifying-points.test.ts +++ b/app/models/__tests__/qualifying-points.test.ts @@ -93,7 +93,7 @@ describe("Qualifying Points Configuration", () => { { name: "Player C", qp: 38 }, ]; - const sorted = standings.sort((a, b) => b.qp - a.qp); + const sorted = standings.toSorted((a, b) => b.qp - a.qp); expect(sorted[0].name).toBe("Player B"); // 50 QP expect(sorted[1].name).toBe("Player A"); // 44 QP @@ -107,7 +107,7 @@ describe("Qualifying Points Configuration", () => { { name: "Player C", qp: 20 }, ]; - const sorted = standings.sort((a, b) => b.qp - a.qp); + const sorted = standings.toSorted((a, b) => b.qp - a.qp); // Both players with 30 QP should be before Player C expect(sorted[2].name).toBe("Player C"); diff --git a/app/models/__tests__/scoring-event-dashboard.test.ts b/app/models/__tests__/scoring-event-dashboard.test.ts index 7097b29..a8de849 100644 --- a/app/models/__tests__/scoring-event-dashboard.test.ts +++ b/app/models/__tests__/scoring-event-dashboard.test.ts @@ -6,11 +6,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; // ── DB mock ────────────────────────────────────────────────────────────────── // selectDistinct returns a fluent chain; findMany is used in step 2. const mockFindMany = vi.fn(); -const mockSelectChainBase = { - from: vi.fn(), - leftJoin: vi.fn(), - where: vi.fn(), -}; function makeSelectChain(rows: { id: string }[]) { const chain = { diff --git a/app/models/__tests__/season-standings.test.ts b/app/models/__tests__/season-standings.test.ts index d3d08bc..45c1928 100644 --- a/app/models/__tests__/season-standings.test.ts +++ b/app/models/__tests__/season-standings.test.ts @@ -81,7 +81,7 @@ describe("Season Standings (F1 Pattern)", () => { it("should correctly score season with tied positions", () => { // Simulate standings where 3rd, 4th, and 5th are tied - const standings = [ + const _standings = [ { position: 1, expected: 100 }, { position: 2, expected: 70 }, // Three tied for 3rd place - each gets placement 3 diff --git a/app/models/__tests__/standings-snapshots.test.ts b/app/models/__tests__/standings-snapshots.test.ts index 9c316ce..0761eb4 100644 --- a/app/models/__tests__/standings-snapshots.test.ts +++ b/app/models/__tests__/standings-snapshots.test.ts @@ -175,7 +175,7 @@ describe("Standings Snapshots", () => { ]; // Should be sorted by date descending (newest first) - const sorted = [...snapshots].sort((a, b) => + const sorted = [...snapshots].toSorted((a, b) => b.date.localeCompare(a.date) ); diff --git a/app/models/__tests__/tiebreaker-logic.test.ts b/app/models/__tests__/tiebreaker-logic.test.ts index f0c524a..a88e487 100644 --- a/app/models/__tests__/tiebreaker-logic.test.ts +++ b/app/models/__tests__/tiebreaker-logic.test.ts @@ -60,7 +60,7 @@ function assignRanks( }> ): Map { // Sort teams by ranking criteria - const sorted = [...teams].sort(compareTeamsForRanking); + const sorted = [...teams].toSorted(compareTeamsForRanking); const ranks = new Map(); let currentRank = 1; diff --git a/app/models/draft-utils.ts b/app/models/draft-utils.ts index 7f7eedb..dab1de8 100644 --- a/app/models/draft-utils.ts +++ b/app/models/draft-utils.ts @@ -345,7 +345,7 @@ export function getTeamForPick( pickNumber: number, draftOrder: { teamId: string; draftOrder: number }[] ): string | null { - const sortedOrder = [...draftOrder].sort((a, b) => a.draftOrder - b.draftOrder); + const sortedOrder = [...draftOrder].toSorted((a, b) => a.draftOrder - b.draftOrder); const teamCount = sortedOrder.length; if (teamCount === 0) return null; diff --git a/app/models/league.ts b/app/models/league.ts index ff1ea44..f98897c 100644 --- a/app/models/league.ts +++ b/app/models/league.ts @@ -1,4 +1,4 @@ -import { eq, desc, and } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; @@ -101,7 +101,7 @@ export async function findLeaguesWithActiveSeasonsByUserId( for (const league of [...leaguesWithTeam, ...leaguesAsCommissioner]) { leagueMap.set(league.id, league); } - return [...leagueMap.values()].sort( + return [...leagueMap.values()].toSorted( (a, b) => b.createdAt.getTime() - a.createdAt.getTime() ); } diff --git a/app/models/participant-result.ts b/app/models/participant-result.ts index a893664..72691fe 100644 --- a/app/models/participant-result.ts +++ b/app/models/participant-result.ts @@ -3,6 +3,9 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; export type ParticipantResult = typeof schema.participantResults.$inferSelect; +export type ParticipantResultWithParticipant = ParticipantResult & { + participant: { id: string; name: string } | null; +}; export type NewParticipantResult = typeof schema.participantResults.$inferInsert; export async function createParticipantResult( @@ -62,7 +65,7 @@ export async function findParticipantResultByParticipantId( export async function findParticipantResultsBySportsSeasonId( sportsSeasonId: string -): Promise { +): Promise { const db = database(); return await db.query.participantResults.findMany({ where: eq(schema.participantResults.sportsSeasonId, sportsSeasonId), diff --git a/app/models/participant-season-result.ts b/app/models/participant-season-result.ts index f96de82..cc0e6fc 100644 --- a/app/models/participant-season-result.ts +++ b/app/models/participant-season-result.ts @@ -1,6 +1,6 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; -import { eq, and, inArray, desc } from "drizzle-orm"; +import { eq, and, inArray } from "drizzle-orm"; export interface UpsertSeasonResultData { participantId: string; @@ -135,7 +135,7 @@ export async function getSeasonResults( }); // Sort by position (nulls last), then by points descending - return results.sort((a, b) => { + return results.toSorted((a, b) => { if (a.currentPosition !== null && b.currentPosition !== null) { return a.currentPosition - b.currentPosition; } diff --git a/app/models/playoff-match.ts b/app/models/playoff-match.ts index 52e3a5d..2451411 100644 --- a/app/models/playoff-match.ts +++ b/app/models/playoff-match.ts @@ -149,7 +149,6 @@ export async function generateSingleEliminationBracket( eventId: string, participantIds: string[] ): Promise { - const db = database(); const numParticipants = participantIds.length; // Validate that we have 4 or 8 participants for standard bracket @@ -218,8 +217,6 @@ export async function advanceWinner( matchId: string, winnerId: string ): Promise { - const db = database(); - // Get the match const match = await findPlayoffMatchById(matchId); if (!match) throw new Error("Match not found"); @@ -523,17 +520,6 @@ async function generateNFL14Bracket( ): Promise { const matches: NewPlayoffMatch[] = []; - // Wild Card: 6 games for seeds 3-14 (12 teams) - // Seeds 1-2 get byes and automatically advance to Divisional - const wildCardSeeding = [ - [6, 10], // #7 seed vs #11 seed (0-indexed: 6 vs 10) - [5, 11], // #6 seed vs #12 seed - [4, 12], // #5 seed vs #13 seed - [7, 9], // #8 seed vs #10 seed - [3, 13], // #4 seed vs #14 seed - [2, 14], // #3 seed vs #15 seed (Note: 14 is index for seed 15, but we only have 14 teams, so this is 3 vs 14) - ]; - // Actually for 14 teams (seeds 1-14), Wild Card should be: // Match 1: #7 (index 6) vs #10 (index 9) // Match 2: #6 (index 5) vs #11 (index 10) @@ -541,7 +527,7 @@ async function generateNFL14Bracket( // Match 4: #8 (index 7) vs #9 (index 8) // Match 5: #4 (index 3) vs #13 (index 12) // Match 6: #3 (index 2) vs #14 (index 13) - const correctedWildCardSeeding = [ + const wildCardSeeding = [ [6, 9], // #7 vs #10 [5, 10], // #6 vs #11 [4, 11], // #5 vs #12 @@ -550,8 +536,8 @@ async function generateNFL14Bracket( [2, 13], // #3 vs #14 ]; - for (let i = 0; i < correctedWildCardSeeding.length; i++) { - const [seed1, seed2] = correctedWildCardSeeding[i]; + for (let i = 0; i < wildCardSeeding.length; i++) { + const [seed1, seed2] = wildCardSeeding[i]; matches.push({ scoringEventId: eventId, round: "Wild Card", @@ -572,7 +558,7 @@ async function generateNFL14Bracket( if (divisionalRound) { for (let i = 0; i < divisionalRound.matchCount; i++) { let participant1Id: string | null = null; - let participant2Id: string | null = null; + const participant2Id: string | null = null; let seedInfo: string | null = null; // Assign bye teams to first two Divisional matches @@ -768,7 +754,6 @@ async function advanceAFLWinner( winnerId: string, loserId: string ): Promise { - const db = database(); const eventId = match.scoringEventId; // Wildcard Round: Winner advances to Elimination Finals @@ -871,8 +856,6 @@ export async function advanceWinnerTemplate( winnerId: string, template: BracketTemplate ): Promise { - const db = database(); - // Get the match const match = await findPlayoffMatchById(matchId); if (!match) throw new Error("Match not found"); diff --git a/app/models/qualifying-points.ts b/app/models/qualifying-points.ts index 10ab421..5ed8b32 100644 --- a/app/models/qualifying-points.ts +++ b/app/models/qualifying-points.ts @@ -306,7 +306,7 @@ export async function updateFinalRankings( const updates = []; // Sort by points descending - const sortedGroups = Array.from(groupedByPoints.entries()).sort((a, b) => { + const sortedGroups = Array.from(groupedByPoints.entries()).toSorted((a, b) => { return parseFloat(b[0]) - parseFloat(a[0]); }); diff --git a/app/models/regular-season-standings.ts b/app/models/regular-season-standings.ts index 78fc7c1..b332455 100644 --- a/app/models/regular-season-standings.ts +++ b/app/models/regular-season-standings.ts @@ -46,9 +46,9 @@ export async function upsertRegularSeasonStandings( losses: r.losses, otLosses: r.otLosses ?? null, ties: r.ties ?? null, - winPct: r.winPct != null ? r.winPct.toString() : null, + winPct: r.winPct !== null && r.winPct !== undefined ? r.winPct.toString() : null, gamesPlayed: r.gamesPlayed, - gamesBack: r.gamesBack != null ? r.gamesBack.toString() : null, + gamesBack: r.gamesBack !== null && r.gamesBack !== undefined ? r.gamesBack.toString() : null, conference: r.conference ?? null, division: r.division ?? null, conferenceRank: r.conferenceRank ?? null, @@ -133,7 +133,7 @@ export async function getRegularSeasonStandings( }); // Sort: conference → division → divisionRank ?? leagueRank ?? leagueRank, nulls last - return rows.sort((a, b) => { + return rows.toSorted((a, b) => { const confA = a.conference ?? "ZZZ"; const confB = b.conference ?? "ZZZ"; if (confA !== confB) return confA.localeCompare(confB); diff --git a/app/models/scoring-calculator.ts b/app/models/scoring-calculator.ts index 4ea65c9..3de3e83 100644 --- a/app/models/scoring-calculator.ts +++ b/app/models/scoring-calculator.ts @@ -1,7 +1,7 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, inArray } from "drizzle-orm"; -import { getScoringRules, calculateFantasyPoints, calculateAveragedPoints, calculateBracketPoints } from "./scoring-rules"; +import { getScoringRules, calculateFantasyPoints, calculateBracketPoints } from "./scoring-rules"; import { getSeasonResults } from "./participant-season-result"; import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; import { sendStandingsUpdateNotification, type ScoredMatch } from "~/services/discord"; @@ -597,7 +597,7 @@ export async function finalizeQualifyingPoints( } // Sort groups by QP (descending) - const sortedGroups = Array.from(groupedByQP.entries()).sort((a, b) => { + const sortedGroups = Array.from(groupedByQP.entries()).toSorted((a, b) => { return parseFloat(b[0]) - parseFloat(a[0]); }); @@ -722,7 +722,7 @@ export async function processSeasonStandings( // Get sorted positions const positions = Object.keys(positionGroups) .map(Number) - .sort((a, b) => a - b); + .toSorted((a, b) => a - b); // Assign fantasy placements (1-8) based on standings let currentPlacement = 1; @@ -874,7 +874,7 @@ export async function calculateTeamScore( // If duplicates exist due to data corruption, the first row wins — acceptable trade-off. const result = pick.participant.results[0]; - if (result && result.finalPosition != null && result.finalPosition > 0) { + if (result && result.finalPosition !== null && result.finalPosition > 0) { const isBracket = pick.participant.sportsSeason?.scoringPattern === "playoff_bracket"; let points: number; if (isBracket) { @@ -1100,7 +1100,7 @@ function assignRanks( }> ): Map { // Sort teams by ranking criteria - const sorted = [...teams].sort(compareTeamsForRanking); + const sorted = [...teams].toSorted(compareTeamsForRanking); const ranks = new Map(); let currentRank = 1; @@ -1330,7 +1330,7 @@ export async function recalculateAffectedLeagues( const usernameByClerkId = new Map( teamOwnerUsers .map((u) => [u.clerkId, getUserDisplayName(u)]) - .filter((entry): entry is [string, string] => entry[1] != null) + .filter((entry): entry is [string, string] => entry[1] !== null) ); // Build teamId → ownerId map from standings data diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index 8ca14cb..b4c2db1 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -157,7 +157,7 @@ export async function updateScoringEvent( ) { const db = providedDb || database(); - const updateData: any = { updatedAt: new Date() }; + const updateData: Partial = { updatedAt: new Date() }; if (data.name !== undefined) updateData.name = data.name; if (data.eventDate !== undefined) { @@ -450,6 +450,7 @@ export async function getUpcomingEventsForDraftedParticipants( inArray(schema.playoffMatches.participant1Id, draftedIds), // participant2Id is nullable UUID — same underlying type as participant1Id; // the notNull difference only exists at the TypeScript layer + // oxlint-disable-next-line typescript/no-explicit-any -- participant2Id is nullable UUID; notNull difference only exists at the TypeScript layer // eslint-disable-next-line @typescript-eslint/no-explicit-any inArray(schema.playoffMatches.participant2Id as any, draftedIds) ), @@ -503,7 +504,7 @@ export async function getUpcomingEventsForDraftedParticipants( pMap.set(row.participant2Id, draftedMap.get(row.participant2Id)!); } - if (row.round && row.gameNumber != null) { + if (row.round && row.gameNumber !== null) { gameKeysByEvent.get(row.id)!.add(`${row.round}|${row.gameNumber}`); } @@ -578,7 +579,7 @@ export async function getEventsForDates( // Build timestamp bounds covering the full span of the requested dates so we // can range-check the playoffMatchGames.scheduledAt timestamp column. - const sortedDates = [...dates].sort(); + const sortedDates = [...dates].toSorted(); const dateFromTimestamp = new Date(sortedDates[0] + "T00:00:00.000Z"); const dateToTimestamp = new Date(sortedDates[sortedDates.length - 1] + "T23:59:59.999Z"); diff --git a/app/models/standings.ts b/app/models/standings.ts index c98a1a2..c7b64b3 100644 --- a/app/models/standings.ts +++ b/app/models/standings.ts @@ -1,6 +1,6 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; -import { eq, and, desc } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; import type { TeamStanding, TeamStandingSnapshot, TeamStandingWithChange } from "~/types/standings"; import { calculateBracketPoints, calculateFantasyPoints } from "~/models/scoring-rules"; @@ -24,7 +24,7 @@ export async function getSeasonStandings( }); // Sort by currentRank ascending (1 is best, 2 is second, etc.) - const sorted = standings.sort((a, b) => a.currentRank - b.currentRank); + const sorted = standings.toSorted((a, b) => a.currentRank - b.currentRank); return sorted.map((standing) => ({ teamId: standing.teamId, @@ -197,7 +197,7 @@ export async function getTeamScoreBreakdown( ); }; - if (result && result.finalPosition != null && result.finalPosition > 0) { + if (result && result.finalPosition !== null && result.finalPosition > 0) { // Calculate points using bracket-averaged scoring for bracket sports if (isBracket) { const templateId = await getBracketTemplate(pick.participant.sportsSeasonId); @@ -402,7 +402,7 @@ export async function getSeasonPointProgression( }); // Organize data by date - const dateMap = new Map(); + const dateMap = new Map(); for (const snapshot of snapshots) { const date = snapshot.snapshotDate; @@ -411,12 +411,12 @@ export async function getSeasonPointProgression( dateMap.set(date, { date }); } - const dateData = dateMap.get(date); + const dateData = dateMap.get(date)!; dateData[snapshot.team.name] = parseFloat(snapshot.totalPoints); } // Convert to array and sort by date - const chartData = Array.from(dateMap.values()).sort((a, b) => + const chartData = Array.from(dateMap.values()).toSorted((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ); diff --git a/app/models/tournament-group.ts b/app/models/tournament-group.ts index b5ccd86..562cb19 100644 --- a/app/models/tournament-group.ts +++ b/app/models/tournament-group.ts @@ -1,4 +1,4 @@ -import { eq, and, inArray } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts index 03ca412..1ce9a01 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts @@ -1,5 +1,5 @@ import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket"; -import { redirect } from "react-router"; + import { findSportsSeasonById } from "~/models/sports-season"; import { findParticipantsBySportsSeasonId } from "~/models/participant"; import { getScoringEventById, updateScoringEvent } from "~/models/scoring-event"; @@ -42,7 +42,7 @@ import { } from "~/models/tournament-group"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; -import { eq, and } from "drizzle-orm"; +import { eq } from "drizzle-orm"; export async function loader({ params }: Route.LoaderArgs) { const sportsSeason = await findSportsSeasonById(params.id); @@ -79,6 +79,8 @@ export async function loader({ params }: Route.LoaderArgs) { participant2: { id: string; name: string } | null; winner: { id: string; name: string } | null; loser: { id: string; name: string } | null; + games: Array<{ id: string; gameNumber: number; scheduledAt: Date | null; status: string; winner?: { name: string } | null }>; + odds: Array<{ id: string; participantId: string; moneylineOdds: number; impliedProbability: string; oddsSource?: string | null; participant: { id: string; name: string } | null }>; }>, tournamentGroups, }; diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx index c204e67..aff3ba8 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx @@ -66,7 +66,7 @@ export default function EventBracket({ const template = templates.find((t) => t.id === selectedTemplate); // Determine if this is a group-stage event - const hasGroupStage = template?.groupStage != null; + const hasGroupStage = template?.groupStage !== null; // Determine if this template uses named regions (e.g. NCAA 68) const hasRegions = (template?.regions?.length ?? 0) > 0; @@ -151,8 +151,8 @@ export default function EventBracket({ const knockoutPopulated = useMemo(() => { if (!isGroupStageEvent) return false; // Check if any Round of 32 match has participants assigned - const r32Matches = matches.filter((m: any) => m.round === "Round of 32"); - return r32Matches.some((m: any) => m.participant1Id || m.participant2Id); + const r32Matches = matches.filter((m) => m.round === "Round of 32"); + return r32Matches.some((m) => m.participant1Id || m.participant2Id); }, [isGroupStageEvent, matches]); // Group stage stats @@ -161,7 +161,7 @@ export default function EventBracket({ let eliminated = 0; let total = 0; for (const group of tournamentGroups) { - for (const member of (group as any).members || []) { + for (const member of group.members || []) { total++; if (member.eliminated) eliminated++; } @@ -174,7 +174,7 @@ export default function EventBracket({ if (!isGroupStageEvent) return []; const advancing: Array<{ id: string; name: string }> = []; for (const group of tournamentGroups) { - for (const member of (group as any).members || []) { + for (const member of group.members || []) { if (!member.eliminated && member.participant) { advancing.push({ id: member.participant.id, @@ -258,7 +258,7 @@ export default function EventBracket({ const getPendingWinnersInRound = (round: string) => { return Object.entries(selectedWinners) .filter(([matchId]) => { - const match = matches.find((m: any) => m.id === matchId); + const match = matches.find((m) => m.id === matchId); return match && match.round === round && !match.isComplete; }); }; @@ -283,7 +283,7 @@ export default function EventBracket({ // Check if all matches are complete const allMatchesComplete = useMemo(() => { - return matches.length > 0 && matches.every((m: any) => m.isComplete); + return matches.length > 0 && matches.every((m) => m.isComplete); }, [matches]); // No bracket and no groups yet = setup phase @@ -455,6 +455,7 @@ export default function EventBracket({

{regionConfig.map((region, r) => ( + // eslint-disable-next-line react/no-array-index-key
{region.playInSeeds.map((seed, pi) => ( + // eslint-disable-next-line react/no-array-index-key
play-in seed: @@ -520,6 +523,7 @@ export default function EventBracket({ {effectiveRegions.map((region, r) => { const directOffset = regionSlotMap.directOffsets[r]; return ( + // eslint-disable-next-line react/no-array-index-key

{region.name || `Region ${r + 1}`}

{region.directSeeds.map((seedNum, pos) => { @@ -573,7 +577,7 @@ export default function EventBracket({ const idx1 = pi.startIndex; const idx2 = pi.startIndex + 1; return ( -
+

Play-In {ffIdx + 1} — {regionName} {pi.seedSlot}-seed

@@ -617,6 +621,7 @@ export default function EventBracket({ const availableParticipants = getAvailableParticipants(i); return ( + // eslint-disable-next-line react/no-array-index-key