brackt/app/routes/admin.sports-seasons.$id.events.$eventId.swiss-matches.tsx
Claude 0595eafe31
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
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
2026-06-12 22:22:46 +00:00

382 lines
17 KiB
TypeScript

import { Form, useLoaderData, useActionData, useNavigation, Link } from 'react-router';
import type { Route } from './+types/admin.sports-seasons.$id.events.$eventId.swiss-matches';
import { findSportsSeasonById } from '~/models/sports-season';
import { getScoringEventById } from '~/models/scoring-event';
import { findParticipantsBySportsSeasonId } from '~/models/season-participant';
import {
findSeasonMatchesByScoringEventId,
upsertSeasonMatch,
updateSeasonMatch,
deleteSeasonMatch,
upsertMatchSubGame,
} from '~/models/season-match';
import type { MatchStatus } from '~/models/season-match';
import { Button } from '~/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '~/components/ui/card';
import { Badge } from '~/components/ui/badge';
import { ArrowLeft, Plus, Save, Trash2 } from 'lucide-react';
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `Swiss Matches — ${data?.event?.name ?? "Event"} - Brackt Admin` }];
}
export async function loader({ params }: Route.LoaderArgs) {
const sportsSeasonId = params.id;
const eventId = params.eventId;
const [sportsSeason, event] = await Promise.all([
findSportsSeasonById(sportsSeasonId),
getScoringEventById(eventId),
]);
if (!sportsSeason) throw new Response('Sports season not found', { status: 404 });
if (!event) throw new Response('Event not found', { status: 404 });
const [participants, matches] = await Promise.all([
findParticipantsBySportsSeasonId(sportsSeasonId),
findSeasonMatchesByScoringEventId(eventId),
]);
return { sportsSeason, event, participants, matches };
}
interface ActionData {
success?: boolean;
message?: string;
}
export async function action({ request, params }: Route.ActionArgs) {
const eventId = params.eventId;
const sportsSeasonId = params.id;
const formData = await request.formData();
const intent = formData.get('intent') as string;
if (intent === 'create-match') {
const participant1Id = formData.get('participant1Id') as string;
const participant2Id = formData.get('participant2Id') as string;
const matchStage = parseInt(formData.get('matchStage') as string, 10);
const matchRound = parseInt(formData.get('matchRound') as string, 10);
const isSeries = formData.get('isSeries') === 'true';
if (!participant1Id || !participant2Id || isNaN(matchStage) || isNaN(matchRound)) {
return { success: false, message: 'Missing required fields.' };
}
const externalMatchId = `manual_${eventId}_s${matchStage}_r${matchRound}_${participant1Id}_${participant2Id}`;
await upsertSeasonMatch({
sportsSeasonId,
scoringEventId: eventId,
participant1Id,
participant2Id,
matchStage,
matchRound,
isSeries,
status: 'scheduled',
externalMatchId,
});
return { success: true, message: 'Match created.' };
}
if (intent === 'update-result') {
const matchId = formData.get('matchId') as string;
const participant1Score = formData.get('participant1Score');
const participant2Score = formData.get('participant2Score');
const winnerId = formData.get('winnerId') as string | null;
const status = formData.get('status') as MatchStatus;
const p1Score = participant1Score ? parseInt(participant1Score as string, 10) : null;
const p2Score = participant2Score ? parseInt(participant2Score as string, 10) : null;
await updateSeasonMatch(matchId, {
participant1Score: p1Score,
participant2Score: p2Score,
winnerId: winnerId || null,
status,
completedAt: status === 'complete' ? new Date() : null,
});
// Handle sub-games (maps) if provided
const mapCount = parseInt(formData.get('mapCount') as string ?? '0', 10);
for (let i = 1; i <= mapCount; i++) {
const mapLabel = formData.get(`map${i}_label`) as string | null;
const map1Score = formData.get(`map${i}_p1`) as string | null;
const map2Score = formData.get(`map${i}_p2`) as string | null;
if (map1Score !== null && map2Score !== null) {
await upsertMatchSubGame({
seasonMatchId: matchId,
gameNumber: i,
gameLabel: mapLabel || null,
participant1Score: parseInt(map1Score, 10),
participant2Score: parseInt(map2Score, 10),
status: 'complete',
});
}
}
return { success: true, message: 'Result saved.' };
}
if (intent === 'delete-match') {
const matchId = formData.get('matchId') as string;
await deleteSeasonMatch(matchId);
return { success: true, message: 'Match deleted.' };
}
return { success: false, message: 'Unknown action.' };
}
const STAGE_NAMES: Record<number, string> = {
1: 'Opening Stage',
2: 'Challengers Stage',
3: 'Legends Stage',
};
const STATUS_BADGE: Record<MatchStatus, { label: string; variant: 'default' | 'secondary' | 'outline' | 'destructive' }> = {
scheduled: { label: 'Scheduled', variant: 'outline' },
in_progress: { label: 'Live', variant: 'default' },
complete: { label: 'Complete', variant: 'secondary' },
canceled: { label: 'Canceled', variant: 'destructive' },
postponed: { label: 'Postponed', variant: 'outline' },
};
export default function AdminSwissMatches() {
const { sportsSeason, event, participants, matches } = useLoaderData<typeof loader>();
const actionData = useActionData<ActionData>();
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';
// Group matches by stage → round
const byStage = new Map<number, Map<number, typeof matches>>();
for (const m of matches) {
const stage = m.matchStage ?? 0;
const round = m.matchRound ?? 0;
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 (
<div className="container mx-auto py-8">
<div className="mb-6">
<Link
to={`/admin/sports-seasons/${sportsSeason.id}/events/${event.id}/cs2-setup`}
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-4"
>
<ArrowLeft className="h-4 w-4" />
Back to CS2 Setup
</Link>
<h1 className="text-3xl font-bold mb-1">Swiss Match Entry</h1>
<p className="text-muted-foreground">{event.name} {sportsSeason.name}</p>
</div>
{actionData?.message && (
<div className={`mb-4 p-3 rounded-md text-sm ${actionData.success ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/30' : 'bg-destructive/10 text-destructive border border-destructive/30'}`}>
{actionData.message}
</div>
)}
{/* Create new match */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Add Match</CardTitle>
<CardDescription>Manually add a Swiss round match.</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="grid grid-cols-2 md:grid-cols-3 gap-4">
<input type="hidden" name="intent" value="create-match" />
<div className="space-y-1">
<label className="text-sm font-medium">Stage</label>
<select name="matchStage" className="h-9 w-full text-sm rounded-md border border-input bg-background px-2">
<option value="1">Stage 1 Opening</option>
<option value="2">Stage 2 Challengers</option>
<option value="3">Stage 3 Legends</option>
</select>
</div>
<div className="space-y-1">
<label className="text-sm font-medium">Round</label>
<select name="matchRound" className="h-9 w-full text-sm rounded-md border border-input bg-background px-2">
{[1, 2, 3, 4, 5].map(r => <option key={r} value={r}>Round {r}</option>)}
</select>
</div>
<div className="space-y-1">
<label className="text-sm font-medium">Format</label>
<select name="isSeries" className="h-9 w-full text-sm rounded-md border border-input bg-background px-2">
<option value="false">Bo1</option>
<option value="true">Bo3</option>
</select>
</div>
<div className="space-y-1">
<label className="text-sm font-medium">Team 1</label>
<select name="participant1Id" className="h-9 w-full text-sm rounded-md border border-input bg-background px-2">
<option value=""> select </option>
{participants.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
<div className="space-y-1">
<label className="text-sm font-medium">Team 2</label>
<select name="participant2Id" className="h-9 w-full text-sm rounded-md border border-input bg-background px-2">
<option value=""> select </option>
{participants.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
<div className="flex items-end">
<Button type="submit" disabled={isSubmitting} className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add Match
</Button>
</div>
</Form>
</CardContent>
</Card>
{/* Existing matches grouped by stage/round */}
{matches.length === 0 ? (
<p className="text-sm text-muted-foreground">No matches yet.</p>
) : (
[...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()].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">
{roundMatches.map(match => (
<div key={match.id} className="border border-border rounded-md p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium">
{match.participant1?.name ?? '?'} vs {match.participant2?.name ?? '?'}
</span>
<div className="flex items-center gap-2">
<Badge variant={STATUS_BADGE[match.status]?.variant ?? 'outline'}>
{STATUS_BADGE[match.status]?.label ?? match.status}
</Badge>
<Form method="post">
<input type="hidden" name="intent" value="delete-match" />
<input type="hidden" name="matchId" value={match.id} />
<Button type="submit" variant="ghost" size="sm" className="text-destructive h-7 px-2" disabled={isSubmitting}>
<Trash2 className="h-3 w-3" />
</Button>
</Form>
</div>
</div>
<Form method="post" className="grid grid-cols-2 md:grid-cols-4 gap-3">
<input type="hidden" name="intent" value="update-result" />
<input type="hidden" name="matchId" value={match.id} />
<input type="hidden" name="mapCount" value={match.isSeries ? '3' : '0'} />
<div className="space-y-1">
<label className="text-xs font-medium">{match.participant1?.name ?? 'P1'} score</label>
<input
type="number"
name="participant1Score"
defaultValue={match.participant1Score ?? ''}
min="0"
className="h-8 w-full text-sm rounded-md border border-input bg-background px-2"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-medium">{match.participant2?.name ?? 'P2'} score</label>
<input
type="number"
name="participant2Score"
defaultValue={match.participant2Score ?? ''}
min="0"
className="h-8 w-full text-sm rounded-md border border-input bg-background px-2"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-medium">Winner</label>
<select
name="winnerId"
defaultValue={match.winnerId ?? ''}
className="h-8 w-full text-sm rounded-md border border-input bg-background px-2"
>
<option value=""> none </option>
<option value={match.participant1Id ?? ''}>{match.participant1?.name ?? 'P1'}</option>
<option value={match.participant2Id ?? ''}>{match.participant2?.name ?? 'P2'}</option>
</select>
</div>
<div className="space-y-1">
<label className="text-xs font-medium">Status</label>
<select
name="status"
defaultValue={match.status}
className="h-8 w-full text-sm rounded-md border border-input bg-background px-2"
>
<option value="scheduled">Scheduled</option>
<option value="in_progress">In Progress</option>
<option value="complete">Complete</option>
<option value="canceled">Canceled</option>
<option value="postponed">Postponed</option>
</select>
</div>
{match.isSeries && (
<>
{[1, 2, 3].map(mapNum => {
const subGame = match.subGames.find(g => g.gameNumber === mapNum);
return (
<div key={mapNum} className="col-span-2 grid grid-cols-3 gap-2">
<input
type="text"
name={`map${mapNum}_label`}
placeholder={`Map ${mapNum} name`}
defaultValue={subGame?.gameLabel ?? ''}
className="h-8 text-xs rounded-md border border-input bg-background px-2"
/>
<input
type="number"
name={`map${mapNum}_p1`}
placeholder="P1"
defaultValue={subGame?.participant1Score ?? ''}
min="0"
className="h-8 text-xs rounded-md border border-input bg-background px-2"
/>
<input
type="number"
name={`map${mapNum}_p2`}
placeholder="P2"
defaultValue={subGame?.participant2Score ?? ''}
min="0"
className="h-8 text-xs rounded-md border border-input bg-background px-2"
/>
</div>
);
})}
</>
)}
<div className="col-span-2 md:col-span-4 flex justify-end">
<Button type="submit" size="sm" disabled={isSubmitting}>
<Save className="h-3 w-3 mr-1" />
Save
</Button>
</div>
</Form>
</div>
))}
</div>
</div>
))}
</CardContent>
</Card>
))
)}
</div>
);
}