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[];
|
||||
}
|
||||
|
||||
const STAGE_NAMES: Record<number, string> = {
|
||||
1: "Opening Stage",
|
||||
2: "Challengers Stage",
|
||||
3: "Legends Stage",
|
||||
};
|
||||
|
||||
const STATUS_BADGE: Record<string, string> = {
|
||||
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",
|
||||
|
|
@ -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) {
|
||||
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[]>();
|
||||
for (const m of matches) {
|
||||
const r = m.matchRound ?? 0;
|
||||
if (!rounds.has(r)) rounds.set(r, []);
|
||||
rounds.get(r)!.push(m);
|
||||
let roundGroup = rounds.get(r);
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ function MatchRow({ match }: { match: MatchWithRelations }) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{match.matchday != null && (
|
||||
{match.matchday !== null && (
|
||||
<div className="text-xs text-muted-foreground mt-1">Matchday {match.matchday}</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -94,11 +94,15 @@ function MatchRow({ match }: { match: MatchWithRelations }) {
|
|||
function groupByMatchday(matches: MatchWithRelations[]) {
|
||||
const groups = new Map<string, MatchWithRelations[]>();
|
||||
for (const m of matches) {
|
||||
const key = m.matchday != null
|
||||
const key = m.matchday !== null
|
||||
? `Matchday ${m.matchday}`
|
||||
: m.scheduledAt?.toISOString().slice(0, 10) ?? "TBD";
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push(m);
|
||||
let group = groups.get(key);
|
||||
if (!group) {
|
||||
group = [];
|
||||
groups.set(key, group);
|
||||
}
|
||||
group.push(m);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,8 +381,12 @@ export default function AdminCs2Setup() {
|
|||
const rounds = new Map<number, typeof stageMatches>();
|
||||
for (const m of stageMatches) {
|
||||
const r = m.matchRound ?? 0;
|
||||
if (!rounds.has(r)) rounds.set(r, []);
|
||||
rounds.get(r)!.push(m);
|
||||
let roundGroup = rounds.get(r);
|
||||
if (!roundGroup) {
|
||||
roundGroup = [];
|
||||
rounds.set(r, roundGroup);
|
||||
}
|
||||
roundGroup.push(m);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -391,7 +395,7 @@ export default function AdminCs2Setup() {
|
|||
Stage {stage} — {STAGE_NAMES[stage] ?? ''}
|
||||
</h3>
|
||||
<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}>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wide">
|
||||
Round {round}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
updateSeasonMatch,
|
||||
deleteSeasonMatch,
|
||||
upsertMatchSubGame,
|
||||
findMatchSubGamesByMatchId,
|
||||
} from '~/models/season-match';
|
||||
import type { MatchStatus } from '~/models/season-match';
|
||||
import { Button } from '~/components/ui/button';
|
||||
|
|
@ -158,10 +157,17 @@ export default function AdminSwissMatches() {
|
|||
for (const m of matches) {
|
||||
const stage = m.matchStage ?? 0;
|
||||
const round = m.matchRound ?? 0;
|
||||
if (!byStage.has(stage)) byStage.set(stage, new Map());
|
||||
const stageMap = byStage.get(stage)!;
|
||||
if (!stageMap.has(round)) stageMap.set(round, []);
|
||||
stageMap.get(round)!.push(m);
|
||||
let stageMap = byStage.get(stage);
|
||||
if (!stageMap) {
|
||||
stageMap = new Map();
|
||||
byStage.set(stage, stageMap);
|
||||
}
|
||||
let roundList = stageMap.get(round);
|
||||
if (!roundList) {
|
||||
roundList = [];
|
||||
stageMap.set(round, roundList);
|
||||
}
|
||||
roundList.push(m);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -242,13 +248,13 @@ export default function AdminSwissMatches() {
|
|||
{matches.length === 0 ? (
|
||||
<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">
|
||||
<CardHeader>
|
||||
<CardTitle>Stage {stage} — {STAGE_NAMES[stage] ?? ''}</CardTitle>
|
||||
</CardHeader>
|
||||
<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}>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-3">Round {round}</p>
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect } from "react";
|
||||
import { useRevalidator } from "react-router";
|
||||
import { Link, useRevalidator } from "react-router";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
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 { MatchSchedule } from "~/components/scoring/MatchSchedule";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
||||
return [{ title: `${data?.sportsSeason?.name ?? "Tournament"} — Schedule & Results` }];
|
||||
|
|
@ -64,7 +63,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||
|
||||
const hasLiveMatches =
|
||||
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 {
|
||||
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) {
|
||||
const {
|
||||
sportsSeason,
|
||||
|
|
|
|||
|
|
@ -116,15 +116,16 @@ describe("PandaScoreMatchSyncAdapter", () => {
|
|||
const [m] = await new PandaScoreMatchSyncAdapter().fetchMatches("9999");
|
||||
expect(m.subGames).toHaveLength(3);
|
||||
|
||||
expect(m.subGames![0].gameNumber).toBe(1);
|
||||
expect(m.subGames![0].gameLabel).toBe("Mirage");
|
||||
expect(m.subGames![0].team1Score).toBe(16);
|
||||
expect(m.subGames![0].team2Score).toBe(14);
|
||||
expect(m.subGames![0].winnerExternalId).toBe("1");
|
||||
expect(m.subGames![0].status).toBe("complete");
|
||||
const subGames = m.subGames ?? [];
|
||||
expect(subGames[0].gameNumber).toBe(1);
|
||||
expect(subGames[0].gameLabel).toBe("Mirage");
|
||||
expect(subGames[0].team1Score).toBe(16);
|
||||
expect(subGames[0].team2Score).toBe(14);
|
||||
expect(subGames[0].winnerExternalId).toBe("1");
|
||||
expect(subGames[0].status).toBe("complete");
|
||||
|
||||
expect(m.subGames![1].gameLabel).toBe("Inferno");
|
||||
expect(m.subGames![1].winnerExternalId).toBe("2");
|
||||
expect(subGames[1].gameLabel).toBe("Inferno");
|
||||
expect(subGames[1].winnerExternalId).toBe("2");
|
||||
});
|
||||
|
||||
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 {
|
||||
if (s == null || s === "") return null;
|
||||
if (s === undefined || s === "") return null;
|
||||
const n = parseInt(s, 10);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@ export class EspnScheduleAdapter implements MatchSyncAdapter {
|
|||
|
||||
// externalSeasonId is "YYYY" year string for ESPN sports
|
||||
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 resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { database } from "~/database/context";
|
||||
import { eq, isNull, or } from "drizzle-orm";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as schema from "~/database/schema";
|
||||
import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/season-participant";
|
||||
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 { PandaScoreMatchSyncAdapter } from "./pandascore";
|
||||
import { EspnScheduleAdapter } from "./espn-schedule";
|
||||
import type { MatchSyncAdapter, MatchSyncResult, MatchRecord } from "./types";
|
||||
import type { MatchSyncAdapter, MatchSyncResult } from "./types";
|
||||
import { logger } from "~/lib/logger";
|
||||
|
||||
function getMatchSyncAdapter(simulatorType: string): MatchSyncAdapter {
|
||||
|
|
@ -61,7 +61,7 @@ export async function syncMatches(sportsSeasonId: string): Promise<MatchSyncResu
|
|||
const participants = await findParticipantsBySportsSeasonId(sportsSeasonId);
|
||||
const participantNames = participants.map((p) => p.name);
|
||||
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]));
|
||||
|
||||
|
|
@ -86,8 +86,8 @@ export async function syncMatches(sportsSeasonId: string): Promise<MatchSyncResu
|
|||
});
|
||||
|
||||
// Split matches: Swiss (matchStage set) vs playoff bracket (matchStage null)
|
||||
const swissMatches = fetchedMatches.filter((m) => m.matchStage != null);
|
||||
const bracketMatches = 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 || m.matchStage === undefined);
|
||||
|
||||
const unmatchedTeams: MatchSyncResult["unmatchedTeams"] = [];
|
||||
const errors: MatchSyncResult["errors"] = [];
|
||||
|
|
@ -235,7 +235,7 @@ export async function syncMatches(sportsSeasonId: string): Promise<MatchSyncResu
|
|||
}
|
||||
|
||||
const roundIsScoring = bracketTemplate?.rounds.find(
|
||||
(r) => r.name === playoffMatch!.round
|
||||
(r) => r.name === playoffMatch.round
|
||||
)?.isScoring;
|
||||
|
||||
await processMatchResult({
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export class PandaScoreMatchSyncAdapter implements MatchSyncAdapter {
|
|||
const subGames: SubGameRecord[] = (m.games ?? []).map((g): SubGameRecord => {
|
||||
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 winnerExternalId = g.winner?.id != null ? String(g.winner.id) : null;
|
||||
const winnerExternalId = g.winner !== null && g.winner !== undefined ? String(g.winner.id) : null;
|
||||
return {
|
||||
externalGameId: String(g.id),
|
||||
gameNumber: g.position,
|
||||
|
|
@ -123,7 +123,7 @@ export class PandaScoreMatchSyncAdapter implements MatchSyncAdapter {
|
|||
team2Name: team2?.name ?? "",
|
||||
team1Score: r1?.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),
|
||||
scheduledAt: m.scheduled_at ? new Date(m.scheduled_at) : null,
|
||||
startedAt: m.begin_at ? new Date(m.begin_at) : null,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue