fix: resolve all oxlint errors from CI
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m46s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m29s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped

- Remove unused imports (findMatchSubGamesByMatchId, isNull, or, MatchRecord, STAGE_NAMES)
- Replace all != / == with !== / === throughout match-sync and components
- Remove all no-non-null-assertion violations: use Map get-or-initialize
  pattern, remove guarded assertions (playoffMatch.round), use 'as' cast
  for filter-then-map pattern
- Replace .sort() with .toSorted() in 5 locations
- Merge duplicate react-router import in tournament.tsx
- Remove unused LoaderData type alias in tournament.tsx
- Rename unused 'stage' param to '_stage' in Cs2TournamentBracket
- Use ?? [] in pandascore.test.ts to eliminate non-null assertions on subGames

https://claude.ai/code/session_01WUUM7uWzFoSkGcZRhnEKG6
This commit is contained in:
Claude 2026-06-12 22:22:46 +00:00
parent ac33e9e223
commit 0595eafe31
No known key found for this signature in database
9 changed files with 57 additions and 47 deletions

View file

@ -18,12 +18,6 @@ interface Cs2TournamentBracketProps {
userParticipantIds?: string[]; userParticipantIds?: string[];
} }
const STAGE_NAMES: Record<number, string> = {
1: "Opening Stage",
2: "Challengers Stage",
3: "Legends Stage",
};
const STATUS_BADGE: Record<string, string> = { const STATUS_BADGE: Record<string, string> = {
scheduled: "bg-muted text-muted-foreground text-xs px-2 py-0.5 rounded", scheduled: "bg-muted text-muted-foreground text-xs px-2 py-0.5 rounded",
in_progress: "bg-amber-500/15 text-amber-400 border border-amber-500/30 text-xs px-2 py-0.5 rounded", in_progress: "bg-amber-500/15 text-amber-400 border border-amber-500/30 text-xs px-2 py-0.5 rounded",
@ -85,7 +79,7 @@ function MatchCard({ match }: { match: MatchWithRelations }) {
); );
} }
function SwissStageTab({ stage, matches }: { stage: number; matches: MatchWithRelations[] }) { function SwissStageTab({ stage: _stage, matches }: { stage: number; matches: MatchWithRelations[] }) {
if (matches.length === 0) { if (matches.length === 0) {
return <p className="text-sm text-muted-foreground py-4">No matches for this stage yet.</p>; return <p className="text-sm text-muted-foreground py-4">No matches for this stage yet.</p>;
} }
@ -94,11 +88,15 @@ function SwissStageTab({ stage, matches }: { stage: number; matches: MatchWithRe
const rounds = new Map<number, MatchWithRelations[]>(); const rounds = new Map<number, MatchWithRelations[]>();
for (const m of matches) { for (const m of matches) {
const r = m.matchRound ?? 0; const r = m.matchRound ?? 0;
if (!rounds.has(r)) rounds.set(r, []); let roundGroup = rounds.get(r);
rounds.get(r)!.push(m); if (!roundGroup) {
roundGroup = [];
rounds.set(r, roundGroup);
}
roundGroup.push(m);
} }
const sortedRounds = [...rounds.entries()].sort(([a], [b]) => a - b); const sortedRounds = [...rounds.entries()].toSorted(([a], [b]) => a - b);
return ( return (
<div className="space-y-6"> <div className="space-y-6">

View file

@ -74,7 +74,7 @@ function MatchRow({ match }: { match: MatchWithRelations }) {
</div> </div>
)} )}
</div> </div>
{match.matchday != null && ( {match.matchday !== null && (
<div className="text-xs text-muted-foreground mt-1">Matchday {match.matchday}</div> <div className="text-xs text-muted-foreground mt-1">Matchday {match.matchday}</div>
)} )}
</div> </div>
@ -94,11 +94,15 @@ function MatchRow({ match }: { match: MatchWithRelations }) {
function groupByMatchday(matches: MatchWithRelations[]) { function groupByMatchday(matches: MatchWithRelations[]) {
const groups = new Map<string, MatchWithRelations[]>(); const groups = new Map<string, MatchWithRelations[]>();
for (const m of matches) { for (const m of matches) {
const key = m.matchday != null const key = m.matchday !== null
? `Matchday ${m.matchday}` ? `Matchday ${m.matchday}`
: m.scheduledAt?.toISOString().slice(0, 10) ?? "TBD"; : m.scheduledAt?.toISOString().slice(0, 10) ?? "TBD";
if (!groups.has(key)) groups.set(key, []); let group = groups.get(key);
groups.get(key)!.push(m); if (!group) {
group = [];
groups.set(key, group);
}
group.push(m);
} }
return groups; return groups;
} }

View file

@ -381,8 +381,12 @@ export default function AdminCs2Setup() {
const rounds = new Map<number, typeof stageMatches>(); const rounds = new Map<number, typeof stageMatches>();
for (const m of stageMatches) { for (const m of stageMatches) {
const r = m.matchRound ?? 0; const r = m.matchRound ?? 0;
if (!rounds.has(r)) rounds.set(r, []); let roundGroup = rounds.get(r);
rounds.get(r)!.push(m); if (!roundGroup) {
roundGroup = [];
rounds.set(r, roundGroup);
}
roundGroup.push(m);
} }
return ( return (
@ -391,7 +395,7 @@ export default function AdminCs2Setup() {
Stage {stage} {STAGE_NAMES[stage] ?? ''} Stage {stage} {STAGE_NAMES[stage] ?? ''}
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
{[...rounds.entries()].sort(([a], [b]) => a - b).map(([round, matches]) => ( {[...rounds.entries()].toSorted(([a], [b]) => a - b).map(([round, matches]) => (
<div key={round}> <div key={round}>
<p className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wide"> <p className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wide">
Round {round} Round {round}

View file

@ -10,7 +10,6 @@ import {
updateSeasonMatch, updateSeasonMatch,
deleteSeasonMatch, deleteSeasonMatch,
upsertMatchSubGame, upsertMatchSubGame,
findMatchSubGamesByMatchId,
} from '~/models/season-match'; } from '~/models/season-match';
import type { MatchStatus } from '~/models/season-match'; import type { MatchStatus } from '~/models/season-match';
import { Button } from '~/components/ui/button'; import { Button } from '~/components/ui/button';
@ -158,10 +157,17 @@ export default function AdminSwissMatches() {
for (const m of matches) { for (const m of matches) {
const stage = m.matchStage ?? 0; const stage = m.matchStage ?? 0;
const round = m.matchRound ?? 0; const round = m.matchRound ?? 0;
if (!byStage.has(stage)) byStage.set(stage, new Map()); let stageMap = byStage.get(stage);
const stageMap = byStage.get(stage)!; if (!stageMap) {
if (!stageMap.has(round)) stageMap.set(round, []); stageMap = new Map();
stageMap.get(round)!.push(m); byStage.set(stage, stageMap);
}
let roundList = stageMap.get(round);
if (!roundList) {
roundList = [];
stageMap.set(round, roundList);
}
roundList.push(m);
} }
return ( return (
@ -242,13 +248,13 @@ export default function AdminSwissMatches() {
{matches.length === 0 ? ( {matches.length === 0 ? (
<p className="text-sm text-muted-foreground">No matches yet.</p> <p className="text-sm text-muted-foreground">No matches yet.</p>
) : ( ) : (
[...byStage.entries()].sort(([a], [b]) => a - b).map(([stage, rounds]) => ( [...byStage.entries()].toSorted(([a], [b]) => a - b).map(([stage, rounds]) => (
<Card key={stage} className="mb-6"> <Card key={stage} className="mb-6">
<CardHeader> <CardHeader>
<CardTitle>Stage {stage} {STAGE_NAMES[stage] ?? ''}</CardTitle> <CardTitle>Stage {stage} {STAGE_NAMES[stage] ?? ''}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{[...rounds.entries()].sort(([a], [b]) => a - b).map(([round, roundMatches]) => ( {[...rounds.entries()].toSorted(([a], [b]) => a - b).map(([round, roundMatches]) => (
<div key={round}> <div key={round}>
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-3">Round {round}</p> <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-3">Round {round}</p>
<div className="space-y-4"> <div className="space-y-4">

View file

@ -1,5 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useRevalidator } from "react-router"; import { Link, useRevalidator } from "react-router";
import { eq, inArray } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
import type { Route } from "./+types/sports-seasons.$sportsSeasonId.tournament"; import type { Route } from "./+types/sports-seasons.$sportsSeasonId.tournament";
@ -11,7 +11,6 @@ import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-t
import { Cs2TournamentBracket } from "~/components/scoring/Cs2TournamentBracket"; import { Cs2TournamentBracket } from "~/components/scoring/Cs2TournamentBracket";
import { MatchSchedule } from "~/components/scoring/MatchSchedule"; import { MatchSchedule } from "~/components/scoring/MatchSchedule";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { Link } from "react-router";
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `${data?.sportsSeason?.name ?? "Tournament"} — Schedule & Results` }]; return [{ title: `${data?.sportsSeason?.name ?? "Tournament"} — Schedule & Results` }];
@ -64,7 +63,7 @@ export async function loader({ params }: Route.LoaderArgs) {
const hasLiveMatches = const hasLiveMatches =
seasonMatches.some((m) => m.status === "in_progress") || seasonMatches.some((m) => m.status === "in_progress") ||
playoffMatches.some((m) => !m.isComplete && (m.participant1Id != null || m.participant2Id != null)); playoffMatches.some((m) => !m.isComplete && (m.participant1Id !== null || m.participant2Id !== null));
return { return {
sportsSeason, sportsSeason,
@ -77,8 +76,6 @@ export async function loader({ params }: Route.LoaderArgs) {
}; };
} }
type LoaderData = Awaited<ReturnType<typeof loader>>;
export default function TournamentPage({ loaderData }: Route.ComponentProps) { export default function TournamentPage({ loaderData }: Route.ComponentProps) {
const { const {
sportsSeason, sportsSeason,

View file

@ -116,15 +116,16 @@ describe("PandaScoreMatchSyncAdapter", () => {
const [m] = await new PandaScoreMatchSyncAdapter().fetchMatches("9999"); const [m] = await new PandaScoreMatchSyncAdapter().fetchMatches("9999");
expect(m.subGames).toHaveLength(3); expect(m.subGames).toHaveLength(3);
expect(m.subGames![0].gameNumber).toBe(1); const subGames = m.subGames ?? [];
expect(m.subGames![0].gameLabel).toBe("Mirage"); expect(subGames[0].gameNumber).toBe(1);
expect(m.subGames![0].team1Score).toBe(16); expect(subGames[0].gameLabel).toBe("Mirage");
expect(m.subGames![0].team2Score).toBe(14); expect(subGames[0].team1Score).toBe(16);
expect(m.subGames![0].winnerExternalId).toBe("1"); expect(subGames[0].team2Score).toBe(14);
expect(m.subGames![0].status).toBe("complete"); expect(subGames[0].winnerExternalId).toBe("1");
expect(subGames[0].status).toBe("complete");
expect(m.subGames![1].gameLabel).toBe("Inferno"); expect(subGames[1].gameLabel).toBe("Inferno");
expect(m.subGames![1].winnerExternalId).toBe("2"); expect(subGames[1].winnerExternalId).toBe("2");
}); });
it("maps status values correctly", async () => { it("maps status values correctly", async () => {

View file

@ -29,7 +29,7 @@ function mapStatus(name: string | undefined, completed: boolean | undefined): Ma
} }
function parseScore(s: string | undefined): number | null { function parseScore(s: string | undefined): number | null {
if (s == null || s === "") return null; if (s === undefined || s === "") return null;
const n = parseInt(s, 10); const n = parseInt(s, 10);
return Number.isFinite(n) ? n : null; return Number.isFinite(n) ? n : null;
} }
@ -44,7 +44,7 @@ export class EspnScheduleAdapter implements MatchSyncAdapter {
// externalSeasonId is "YYYY" year string for ESPN sports // externalSeasonId is "YYYY" year string for ESPN sports
async fetchMatches(externalSeasonId: string): Promise<MatchRecord[]> { async fetchMatches(externalSeasonId: string): Promise<MatchRecord[]> {
const seasonTypeParam = this.seasonType != null ? `&seasontype=${this.seasonType}` : ""; const seasonTypeParam = this.seasonType !== undefined ? `&seasontype=${this.seasonType}` : "";
const url = `https://site.api.espn.com/apis/site/v2/sports/${this.sport}/scoreboard?limit=1000&dates=${externalSeasonId}${seasonTypeParam}`; const url = `https://site.api.espn.com/apis/site/v2/sports/${this.sport}/scoreboard?limit=1000&dates=${externalSeasonId}${seasonTypeParam}`;
const resp = await fetch(url); const resp = await fetch(url);
if (!resp.ok) { if (!resp.ok) {

View file

@ -1,5 +1,5 @@
import { database } from "~/database/context"; import { database } from "~/database/context";
import { eq, isNull, or } from "drizzle-orm"; import { eq } from "drizzle-orm";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/season-participant"; import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/season-participant";
import { upsertSeasonMatchBulk, upsertMatchSubGame } from "~/models/season-match"; import { upsertSeasonMatchBulk, upsertMatchSubGame } from "~/models/season-match";
@ -9,7 +9,7 @@ import { findMatchingTeamName } from "~/lib/normalize-team-name";
import { getBracketTemplate } from "~/lib/bracket-templates"; import { getBracketTemplate } from "~/lib/bracket-templates";
import { PandaScoreMatchSyncAdapter } from "./pandascore"; import { PandaScoreMatchSyncAdapter } from "./pandascore";
import { EspnScheduleAdapter } from "./espn-schedule"; import { EspnScheduleAdapter } from "./espn-schedule";
import type { MatchSyncAdapter, MatchSyncResult, MatchRecord } from "./types"; import type { MatchSyncAdapter, MatchSyncResult } from "./types";
import { logger } from "~/lib/logger"; import { logger } from "~/lib/logger";
function getMatchSyncAdapter(simulatorType: string): MatchSyncAdapter { function getMatchSyncAdapter(simulatorType: string): MatchSyncAdapter {
@ -61,7 +61,7 @@ export async function syncMatches(sportsSeasonId: string): Promise<MatchSyncResu
const participants = await findParticipantsBySportsSeasonId(sportsSeasonId); const participants = await findParticipantsBySportsSeasonId(sportsSeasonId);
const participantNames = participants.map((p) => p.name); const participantNames = participants.map((p) => p.name);
const participantByExternalId = new Map( const participantByExternalId = new Map(
participants.filter((p) => p.externalId).map((p) => [p.externalId!, p]) participants.filter((p) => p.externalId).map((p) => [p.externalId as string, p])
); );
const participantByName = new Map(participants.map((p) => [p.name, p])); const participantByName = new Map(participants.map((p) => [p.name, p]));
@ -86,8 +86,8 @@ export async function syncMatches(sportsSeasonId: string): Promise<MatchSyncResu
}); });
// Split matches: Swiss (matchStage set) vs playoff bracket (matchStage null) // Split matches: Swiss (matchStage set) vs playoff bracket (matchStage null)
const swissMatches = fetchedMatches.filter((m) => m.matchStage != null); const swissMatches = fetchedMatches.filter((m) => m.matchStage !== null && m.matchStage !== undefined);
const bracketMatches = fetchedMatches.filter((m) => m.matchStage == null); const bracketMatches = fetchedMatches.filter((m) => m.matchStage === null || m.matchStage === undefined);
const unmatchedTeams: MatchSyncResult["unmatchedTeams"] = []; const unmatchedTeams: MatchSyncResult["unmatchedTeams"] = [];
const errors: MatchSyncResult["errors"] = []; const errors: MatchSyncResult["errors"] = [];
@ -235,7 +235,7 @@ export async function syncMatches(sportsSeasonId: string): Promise<MatchSyncResu
} }
const roundIsScoring = bracketTemplate?.rounds.find( const roundIsScoring = bracketTemplate?.rounds.find(
(r) => r.name === playoffMatch!.round (r) => r.name === playoffMatch.round
)?.isScoring; )?.isScoring;
await processMatchResult({ await processMatchResult({

View file

@ -103,7 +103,7 @@ export class PandaScoreMatchSyncAdapter implements MatchSyncAdapter {
const subGames: SubGameRecord[] = (m.games ?? []).map((g): SubGameRecord => { const subGames: SubGameRecord[] = (m.games ?? []).map((g): SubGameRecord => {
const t1score = g.teams?.find((t) => t.team.id === team1?.id)?.score ?? null; const t1score = g.teams?.find((t) => t.team.id === team1?.id)?.score ?? null;
const t2score = g.teams?.find((t) => t.team.id === team2?.id)?.score ?? null; const t2score = g.teams?.find((t) => t.team.id === team2?.id)?.score ?? null;
const winnerExternalId = g.winner?.id != null ? String(g.winner.id) : null; const winnerExternalId = g.winner !== null && g.winner !== undefined ? String(g.winner.id) : null;
return { return {
externalGameId: String(g.id), externalGameId: String(g.id),
gameNumber: g.position, gameNumber: g.position,
@ -123,7 +123,7 @@ export class PandaScoreMatchSyncAdapter implements MatchSyncAdapter {
team2Name: team2?.name ?? "", team2Name: team2?.name ?? "",
team1Score: r1?.score ?? null, team1Score: r1?.score ?? null,
team2Score: r2?.score ?? null, team2Score: r2?.score ?? null,
winnerExternalId: m.winner_id != null ? String(m.winner_id) : null, winnerExternalId: m.winner_id !== null && m.winner_id !== undefined ? String(m.winner_id) : null,
status: mapStatus(m.status), status: mapStatus(m.status),
scheduledAt: m.scheduled_at ? new Date(m.scheduled_at) : null, scheduledAt: m.scheduled_at ? new Date(m.scheduled_at) : null,
startedAt: m.begin_at ? new Date(m.begin_at) : null, startedAt: m.begin_at ? new Date(m.begin_at) : null,