import { Form, useLoaderData, useActionData, useNavigation, useFetcher, Link } from 'react-router'; import type { Route } from './+types/admin.sports-seasons.$id.events.$eventId.cs2-setup'; import { findSportsSeasonById } from '~/models/sports-season'; import { getScoringEventById } from '~/models/scoring-event'; import { findParticipantsBySportsSeasonId } from '~/models/season-participant'; import { getCs2StageResultsForEvent, upsertCs2StageAssignments, markCs2StageEliminations, clearCs2StageAssignments, } from '~/models/cs2-major-stage'; import { findSeasonMatchesByScoringEventId } from '~/models/season-match'; import { syncMatches } from '~/services/match-sync'; import { Button } from '~/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '~/components/ui/card'; import { Badge } from '~/components/ui/badge'; import { ArrowLeft, Save, Trash2, CheckCircle2, RefreshCw } from 'lucide-react'; import { useState } from 'react'; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `CS2 Stage Setup — ${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, stageResults, seasonMatches] = await Promise.all([ findParticipantsBySportsSeasonId(sportsSeasonId), getCs2StageResultsForEvent(eventId), findSeasonMatchesByScoringEventId(eventId), ]); return { sportsSeason, event, participants, stageResults, seasonMatches }; } interface ActionData { success?: boolean; message?: string; } export async function action({ request, params }: Route.ActionArgs) { const eventId = params.eventId; const formData = await request.formData(); const intent = formData.get('intent') as string; if (intent === 'reset') { await clearCs2StageAssignments(eventId); return { success: true, message: 'Stage assignments cleared.' }; } if (intent === 'assign') { const assignments: Array<{ scoringEventId: string; participantId: string; stageEntry: number }> = []; for (const [key, value] of formData.entries()) { if (key.startsWith('stage_')) { const participantId = key.slice('stage_'.length); const stageEntry = parseInt(value as string, 10); if (!isNaN(stageEntry) && stageEntry >= 1 && stageEntry <= 3) { assignments.push({ scoringEventId: eventId, participantId, stageEntry }); } } } if (assignments.length === 0) { return { success: false, message: 'No stage assignments submitted.' }; } await upsertCs2StageAssignments(assignments); return { success: true, message: `Saved ${assignments.length} stage assignments.` }; } if (intent === 'mark-eliminations') { // Parse elimination data: elim_{participantId} = winsAtElimination (0, 1, or 2) const eliminations: Array<{ participantId: string; winsAtElimination: number }> = []; for (const [key, value] of formData.entries()) { if (key.startsWith('elim_')) { const participantId = key.slice('elim_'.length); const wins = parseInt(value as string, 10); if (!isNaN(wins) && wins >= 0 && wins <= 2) { eliminations.push({ participantId, winsAtElimination: wins }); } } } await markCs2StageEliminations(eventId, eliminations); return { success: true, message: `Marked ${eliminations.length} eliminations.` }; } if (intent === 'sync-matches') { try { const result = await syncMatches(params.id); const total = result.swissCreated + result.swissUpdated; const summary = `Synced: ${total} Swiss match(es), ${result.playoffUpdated} playoff match(es).`; return { success: true, message: summary }; } catch (err) { return { success: false, message: err instanceof Error ? err.message : 'Sync failed.' }; } } return { success: false, message: 'Unknown action.' }; } const STAGE_LABELS: Record = { 1: { label: 'Stage 1', description: 'Opening Stage (16 teams, Bo1 Swiss)', maxTeams: 16, badgeVariant: 'outline' }, 2: { label: 'Stage 2', description: 'Elimination Stage (8 Challengers, Bo1/Bo3 Swiss)', maxTeams: 8, badgeVariant: 'secondary' }, 3: { label: 'Stage 3', description: 'Decider Stage (8 Legends, all Bo3 Swiss)', maxTeams: 8, badgeVariant: 'default' }, }; const RECORD_OPTIONS = [ { value: '2', label: '2-3 (advanced furthest)' }, { value: '1', label: '1-3' }, { value: '0', label: '0-3 (earliest exit)' }, ]; const MATCH_STATUS_STYLES: Record = { scheduled: 'text-muted-foreground', in_progress: 'text-amber-400 font-medium', complete: 'text-emerald-400', canceled: 'text-destructive line-through', postponed: 'text-muted-foreground italic', }; const STAGE_NAMES: Record = { 1: 'Opening Stage', 2: 'Challengers Stage', 3: 'Legends Stage', }; export default function AdminCs2Setup() { const { sportsSeason, event, participants, stageResults, seasonMatches } = useLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const isSubmitting = navigation.state === 'submitting'; const syncFetcher = useFetcher(); const isSyncing = syncFetcher.state !== 'idle'; // Stage assignment state (local, submitted via form) const [stageSelections, setStageSelections] = useState>(() => { const initial: Record = {}; stageResults.forEach(r => { initial[r.participantId] = r.stageEntry.toString(); }); return initial; }); // Elimination checkbox state — tracks which teams are checked as eliminated const [eliminatedChecked, setEliminatedChecked] = useState>(() => { const initial: Record = {}; stageResults.forEach(r => { initial[r.participantId] = r.stageEliminated !== null; }); return initial; }); // Group participants by stage for display const byStage: Record = { 1: [], 2: [], 3: [] }; for (const r of stageResults) { byStage[r.stageEntry]?.push(r); } const stageCounts = Object.fromEntries( Object.entries(byStage).map(([k, v]) => [k, v.length]) ); return (
Back to event

CS2 Stage Setup

{event.name} — {sportsSeason.name}

{actionData?.message && (
{actionData.message}
)} {/* Stage Assignment */} Stage Assignments {stageResults.length > 0 && (
)}
Assign each team to their starting stage. Stage 1 = 16 Opening Stage teams, Stage 2 = 8 Challengers (skip Stage 1), Stage 3 = 8 Legends (skip Stages 1 and 2). Total: 32 teams across all three stages.
{/* Stage summary badges */}
{[1, 2, 3].map(stage => (
{STAGE_LABELS[stage].label} {stageCounts[stage] ?? 0}/{STAGE_LABELS[stage].maxTeams} teams
))}
{/* Teams listed with stage selectors */}
Team Starting Stage
{participants.map(participant => (
{participant.name}
))}
{/* Advancement Tracking (only shown when stage assignments exist) */} {stageResults.length > 0 && ( Stage Advancement Tracking Record which teams were eliminated and their W-L record at elimination. Teams not listed as eliminated are assumed to have advanced (or are still playing). Stage 3 W-L records determine QP sub-placements (9–16).
{[1, 2, 3].map(stage => { const teamsInStage = byStage[stage]; if (teamsInStage.length === 0) return null; return (

{STAGE_LABELS[stage].label} {STAGE_LABELS[stage].description}

{teamsInStage.map(r => { const isEliminated = eliminatedChecked[r.participantId] ?? false; return (
{r.participantName} {r.stageEliminated !== null && ( eliminated {r.stageEliminatedWins}-3 )} {isEliminated && ( )} {!isEliminated &&
}
); })}
); })} )} {/* Swiss Rounds */} Swiss Rounds {sportsSeason.externalSeasonId ? `Syncing from external ID: ${sportsSeason.externalSeasonId}` : 'Set an External Season ID on the sports season to enable API sync.'} {seasonMatches.length === 0 ? (

No match data yet. Sync from the API or use the manual match entry page.

) : (
{[1, 2, 3].map(stage => { const stageMatches = seasonMatches.filter(m => m.matchStage === stage); if (stageMatches.length === 0) return null; // Group by round const rounds = new Map(); for (const m of stageMatches) { const r = m.matchRound ?? 0; let roundGroup = rounds.get(r); if (!roundGroup) { roundGroup = []; rounds.set(r, roundGroup); } roundGroup.push(m); } return (

Stage {stage} — {STAGE_NAMES[stage] ?? ''}

{[...rounds.entries()].toSorted(([a], [b]) => a - b).map(([round, matches]) => (

Round {round}

{matches.map(m => (
{m.participant1?.name ?? '?'} {m.status === 'complete' || m.status === 'in_progress' ? `${m.participant1Score ?? '–'} – ${m.participant2Score ?? '–'}` : 'vs'} {m.participant2?.name ?? '?'} {m.subGames.length > 0 && ( [{m.subGames.map(g => `${g.participant1Score}-${g.participant2Score}`).join(', ')}] )}
))}
))}
); })}
)}
{/* Champions Stage link */} {stageResults.length > 0 && ( Champions Stage (Playoffs) The 8-team single-elimination bracket. Use the bracket admin to track Quarterfinals, Semifinals, and Grand Final results. )}
); }