'use client'; import { useEffect, useState, FormEvent } from 'react'; import { useRouter } from 'next/navigation'; import { FileText, Users, Calendar, Trophy, Award, CheckCircle2, ChevronLeft, ChevronRight, Loader2, AlertCircle, Sparkles, Check, } from 'lucide-react'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import Heading from '@/components/ui/Heading'; import LeagueReviewSummary from '@/components/leagues/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; 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(), ); 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 < 5 ? ((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) ) { 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; // Handler for scoring preset selection - updates timing defaults based on preset const handleScoringPresetChange = (patternId: string) => { const lowerPresetId = patternId.toLowerCase(); setForm((prev) => { const timings = prev.timings ?? {}; let updatedTimings = { ...timings }; // Auto-configure session durations based on preset type if (lowerPresetId.includes('sprint') || lowerPresetId.includes('double')) { updatedTimings = { ...updatedTimings, practiceMinutes: 15, qualifyingMinutes: 20, sprintRaceMinutes: 20, mainRaceMinutes: 35, sessionCount: 2, }; } else if (lowerPresetId.includes('endurance') || lowerPresetId.includes('long')) { updatedTimings = { ...updatedTimings, practiceMinutes: 30, qualifyingMinutes: 30, sprintRaceMinutes: undefined, mainRaceMinutes: 90, sessionCount: 1, }; } else { // Standard/feature format updatedTimings = { ...updatedTimings, practiceMinutes: 20, qualifyingMinutes: 30, sprintRaceMinutes: undefined, mainRaceMinutes: 40, sessionCount: 1, }; } return { ...prev, scoring: { ...prev.scoring, patternId, customScoringEnabled: false, }, timings: updatedTimings, }; }); }; const steps = [ { id: 1 as Step, label: 'Basics', icon: FileText, shortLabel: 'Name' }, { id: 2 as Step, label: 'Structure', icon: Users, shortLabel: 'Mode' }, { id: 3 as Step, label: 'Schedule', icon: Calendar, shortLabel: 'Time' }, { id: 4 as Step, label: 'Scoring', icon: Trophy, shortLabel: 'Points' }, { id: 5 as Step, label: 'Review', icon: CheckCircle2, shortLabel: 'Done' }, ]; const getStepTitle = (currentStep: Step): string => { switch (currentStep) { case 1: return 'Name your league'; case 2: return 'Choose the structure'; case 3: return 'Set the schedule'; case 4: return 'Scoring & championships'; case 5: return 'Review & create'; default: return ''; } }; const getStepSubtitle = (currentStep: Step): string => { switch (currentStep) { case 1: return 'Give your league a memorable name and choose who can join.'; case 2: return 'Will drivers compete individually or as part of teams?'; case 3: return 'Configure session durations and plan your season calendar.'; case 4: return 'Select a scoring preset, enable championships, and set drop rules.'; case 5: return 'Everything looks good? Launch your new league!'; default: return ''; } }; const currentStepData = steps.find((s) => s.id === step); const CurrentStepIcon = currentStepData?.icon ?? FileText; return (
{/* Header with icon */}
Create a new league

Set up your racing series in {steps.length} easy steps

{/* Desktop Progress Bar */}
{/* Background track */}
{/* Progress fill */}
{steps.map((wizardStep) => { const isCompleted = wizardStep.id < step; const isCurrent = wizardStep.id === step; const StepIcon = wizardStep.icon; return (
{isCompleted ? ( ) : ( )}

{wizardStep.label}

); })}
{/* Mobile Progress */}
{currentStepData?.label}
{step}/{steps.length}
{/* Step dots */}
{steps.map((s) => (
))}
{/* Main Card */} {/* Top gradient accent */}
{/* Step header */}
{getStepTitle(step)}

{getStepSubtitle(step)}

Step {step} / {steps.length}
{/* Divider */}
{/* Step content with min-height for consistency */}
{step === 1 && (
)} {step === 2 && (
)} {step === 3 && (
)} {step === 4 && (
{/* Scoring Pattern Selection */} setForm((prev) => ({ ...prev, scoring: { ...prev.scoring, customScoringEnabled: !prev.scoring.customScoringEnabled, }, })) } /> {/* Divider */}
{/* Championships & Drop Rules side by side on larger screens */}
{errors.submit && (

{errors.submit}

)}
)} {step === 5 && (
{errors.submit && (

{errors.submit}

)}
)}
{/* Navigation */}
{/* Mobile step dots */}
{steps.map((s) => (
))}
{step < 5 ? ( ) : ( )}
{/* Helper text */}

You can edit all settings after creating your league

); }