fix: resolve all oxlint errors from CI
- 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:
parent
ac33e9e223
commit
0595eafe31
9 changed files with 57 additions and 47 deletions
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue