claude/great-lovelace-r3bznh #88

Merged
chrisp merged 4 commits from claude/great-lovelace-r3bznh into main 2026-06-12 22:35:36 +00:00
9 changed files with 57 additions and 47 deletions
Showing only changes of commit 0595eafe31 - Show all commits

View file

@ -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">

View file

@ -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;
}

View file

@ -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}

View file

@ -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">

View file

@ -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,

View file

@ -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 () => {

View file

@ -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) {

View file

@ -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({

View file

@ -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,