'use client'; import { useEffect, useState, FormEvent } from 'react'; import { useRouter } from 'next/navigation'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import Heading from '@/components/ui/Heading'; import LeagueReviewSummary from './LeagueReviewSummary'; import { getDriverRepository, getListLeagueScoringPresetsQuery, getCreateLeagueWithSeasonAndScoringUseCase, } from '@/lib/di-container'; import type { LeagueScoringPresetDTO } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider'; import type { LeagueConfigFormModel } from '@gridpilot/racing/application'; import { LeagueBasicsSection } from './LeagueBasicsSection'; import { LeagueStructureSection } from './LeagueStructureSection'; import { LeagueScoringSection, ScoringPatternSection, ChampionshipsSection, } from './LeagueScoringSection'; import { LeagueDropSection } from './LeagueDropSection'; import { LeagueTimingsSection } from './LeagueTimingsSection'; type Step = 1 | 2 | 3 | 4 | 5 | 6; interface WizardErrors { basics?: { name?: string; visibility?: string; }; structure?: { maxDrivers?: string; maxTeams?: string; driversPerTeam?: string; }; timings?: { qualifyingMinutes?: string; mainRaceMinutes?: string; roundsPlanned?: string; }; scoring?: { patternId?: string; }; submit?: string; } function createDefaultForm(): LeagueConfigFormModel { const defaultPatternId = 'sprint-main-driver'; return { basics: { name: '', description: '', visibility: 'public', gameId: 'iracing', }, structure: { mode: 'solo', maxDrivers: 24, maxTeams: undefined, driversPerTeam: undefined, multiClassEnabled: false, }, championships: { enableDriverChampionship: true, enableTeamChampionship: false, enableNationsChampionship: false, enableTrophyChampionship: false, }, scoring: { patternId: defaultPatternId, customScoringEnabled: false, }, dropPolicy: { strategy: 'bestNResults', n: 6, }, timings: { practiceMinutes: 20, qualifyingMinutes: 30, sprintRaceMinutes: defaultPatternId === 'sprint-main-driver' ? 20 : undefined, mainRaceMinutes: 40, sessionCount: 2, roundsPlanned: 8, }, }; } export default function CreateLeagueWizard() { const router = useRouter(); const [step, setStep] = useState(1); const [loading, setLoading] = useState(false); const [presetsLoading, setPresetsLoading] = useState(true); const [presets, setPresets] = useState([]); const [errors, setErrors] = useState({}); const [form, setForm] = useState(() => createDefaultForm(), ); /** * Local-only weekend template selection for Step 3. * This does not touch domain models; it only seeds timing defaults. */ const [weekendTemplate, setWeekendTemplate] = useState(''); useEffect(() => { async function loadPresets() { try { const query = getListLeagueScoringPresetsQuery(); const result = await query.execute(); setPresets(result); if (result.length > 0) { setForm((prev) => ({ ...prev, scoring: { ...prev.scoring, patternId: prev.scoring.patternId || result[0].id, customScoringEnabled: prev.scoring.customScoringEnabled ?? false, }, })); } } catch (err) { setErrors((prev) => ({ ...prev, submit: err instanceof Error ? err.message : 'Failed to load scoring presets', })); } finally { setPresetsLoading(false); } } loadPresets(); }, []); const validateStep = (currentStep: Step): boolean => { const nextErrors: WizardErrors = {}; if (currentStep === 1) { const basicsErrors: WizardErrors['basics'] = {}; if (!form.basics.name.trim()) { basicsErrors.name = 'Name is required'; } if (!form.basics.visibility) { basicsErrors.visibility = 'Visibility is required'; } if (Object.keys(basicsErrors).length > 0) { nextErrors.basics = basicsErrors; } } if (currentStep === 2) { const structureErrors: WizardErrors['structure'] = {}; if (form.structure.mode === 'solo') { if (!form.structure.maxDrivers || form.structure.maxDrivers <= 0) { structureErrors.maxDrivers = 'Max drivers must be greater than 0 for solo leagues'; } } else if (form.structure.mode === 'fixedTeams') { if ( !form.structure.maxTeams || form.structure.maxTeams <= 0 ) { structureErrors.maxTeams = 'Max teams must be greater than 0 for team leagues'; } if ( !form.structure.driversPerTeam || form.structure.driversPerTeam <= 0 ) { structureErrors.driversPerTeam = 'Drivers per team must be greater than 0'; } } if (Object.keys(structureErrors).length > 0) { nextErrors.structure = structureErrors; } } if (currentStep === 3) { const timingsErrors: WizardErrors['timings'] = {}; if (!form.timings.qualifyingMinutes || form.timings.qualifyingMinutes <= 0) { timingsErrors.qualifyingMinutes = 'Qualifying duration must be greater than 0 minutes'; } if (!form.timings.mainRaceMinutes || form.timings.mainRaceMinutes <= 0) { timingsErrors.mainRaceMinutes = 'Main race duration must be greater than 0 minutes'; } if (Object.keys(timingsErrors).length > 0) { nextErrors.timings = timingsErrors; } } if (currentStep === 4) { const scoringErrors: WizardErrors['scoring'] = {}; if (!form.scoring.patternId && !form.scoring.customScoringEnabled) { scoringErrors.patternId = 'Select a scoring preset or enable custom scoring'; } if (Object.keys(scoringErrors).length > 0) { nextErrors.scoring = scoringErrors; } } setErrors((prev) => ({ ...prev, ...nextErrors, })); return Object.keys(nextErrors).length === 0; }; const goToNextStep = () => { if (!validateStep(step)) { return; } setStep((prev) => (prev < 6 ? ((prev + 1) as Step) : prev)); }; const goToPreviousStep = () => { setStep((prev) => (prev > 1 ? ((prev - 1) as Step) : prev)); }; const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (loading) return; if ( !validateStep(1) || !validateStep(2) || !validateStep(3) || !validateStep(4) || !validateStep(5) ) { setStep(1); return; } setLoading(true); setErrors((prev) => ({ ...prev, submit: undefined })); try { const driverRepo = getDriverRepository(); const drivers = await driverRepo.findAll(); const currentDriver = drivers[0]; if (!currentDriver) { setErrors((prev) => ({ ...prev, submit: 'No driver profile found. Please create a driver profile first.', })); setLoading(false); return; } const createUseCase = getCreateLeagueWithSeasonAndScoringUseCase(); const structure = form.structure; let maxDrivers: number | undefined; let maxTeams: number | undefined; if (structure.mode === 'solo') { maxDrivers = typeof structure.maxDrivers === 'number' ? structure.maxDrivers : undefined; maxTeams = undefined; } else { const teams = typeof structure.maxTeams === 'number' ? structure.maxTeams : 0; const perTeam = typeof structure.driversPerTeam === 'number' ? structure.driversPerTeam : 0; maxTeams = teams > 0 ? teams : undefined; maxDrivers = teams > 0 && perTeam > 0 ? teams * perTeam : undefined; } const command = { name: form.basics.name.trim(), description: form.basics.description?.trim() || undefined, visibility: form.basics.visibility, ownerId: currentDriver.id, gameId: form.basics.gameId, maxDrivers, maxTeams, enableDriverChampionship: form.championships.enableDriverChampionship, enableTeamChampionship: form.championships.enableTeamChampionship, enableNationsChampionship: form.championships.enableNationsChampionship, enableTrophyChampionship: form.championships.enableTrophyChampionship, scoringPresetId: form.scoring.patternId || undefined, } as const; const result = await createUseCase.execute(command); router.push(`/leagues/${result.leagueId}`); } catch (err) { setErrors((prev) => ({ ...prev, submit: err instanceof Error ? err.message : 'Failed to create league', })); setLoading(false); } }; const currentPreset = presets.find((p) => p.id === form.scoring.patternId) ?? null; const handleWeekendTemplateChange = (template: string) => { setWeekendTemplate(template); setForm((prev) => { const timings = prev.timings ?? {}; if (template === 'feature') { return { ...prev, timings: { ...timings, practiceMinutes: 20, qualifyingMinutes: 30, sprintRaceMinutes: undefined, mainRaceMinutes: 40, sessionCount: 1, }, }; } if (template === 'sprintFeature') { return { ...prev, timings: { ...timings, practiceMinutes: 15, qualifyingMinutes: 20, sprintRaceMinutes: 20, mainRaceMinutes: 35, sessionCount: 2, }, }; } if (template === 'endurance') { return { ...prev, timings: { ...timings, practiceMinutes: 30, qualifyingMinutes: 30, sprintRaceMinutes: undefined, mainRaceMinutes: 90, sessionCount: 1, }, }; } return prev; }); }; const steps = [ { id: 1 as Step, label: 'Basics' }, { id: 2 as Step, label: 'Structure' }, { id: 3 as Step, label: 'Schedule & timings' }, { id: 4 as Step, label: 'Scoring pattern' }, { id: 5 as Step, label: 'Championships & drops' }, { id: 6 as Step, label: 'Review & confirm' }, ]; const getStepTitle = (currentStep: Step): string => { switch (currentStep) { case 1: return 'Step 1 — Basics'; case 2: return 'Step 2 — Structure'; case 3: return 'Step 3 — Schedule & timings'; case 4: return 'Step 4 — Scoring pattern'; case 5: return 'Step 5 — Championships & drops'; case 6: return 'Step 6 — Review & confirm'; default: return ''; } }; const getStepSubtitle = (currentStep: Step): string => { switch (currentStep) { case 1: return 'Give your league a clear name, description, and visibility.'; case 2: return 'Choose whether this is a solo or team-based championship.'; case 3: return 'Roughly outline how long your weekends and season should run.'; case 4: return 'Pick a scoring pattern that matches your weekends.'; case 5: return 'Decide which championships to track and how drops behave.'; case 6: return 'Double-check the summary before creating your new league.'; default: return ''; } }; return (
Create a new league

Configure basics, structure, schedule, scoring, and drop rules in a few simple steps.

{steps.map((wizardStep, index) => { const isCompleted = wizardStep.id < step; const isCurrent = wizardStep.id === step; const baseCircleClasses = 'flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold'; const circleClasses = isCurrent ? 'bg-primary-blue text-white' : isCompleted ? 'bg-primary-blue/20 border border-primary-blue text-primary-blue' : 'bg-iron-gray border border-charcoal-outline text-gray-400'; return (
{isCompleted ? '✓' : wizardStep.id}
{wizardStep.label} {index < steps.length - 1 && ( )}
); })}
{getStepTitle(step)}

{getStepSubtitle(step)}


{step === 1 && ( )} {step === 2 && ( )} {step === 3 && ( )} {step === 4 && (
setForm((prev) => ({ ...prev, scoring: { ...prev.scoring, patternId, customScoringEnabled: false, }, })) } onToggleCustomScoring={() => setForm((prev) => ({ ...prev, scoring: { ...prev.scoring, customScoringEnabled: !prev.scoring.customScoringEnabled, }, })) } />
)} {step === 5 && (
{errors.submit && (
{errors.submit}
)}
)} {step === 6 && (
{errors.submit && (
{errors.submit}
)}
)}
{step < 6 && ( )} {step === 6 && ( )}
); }