'use client'; import React, { useEffect, useState, FormEvent, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { FileText, Users, Calendar, Trophy, Award, CheckCircle2, ChevronLeft, ChevronRight, Loader2, AlertCircle, Sparkles, Check, Scale, } 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 { getListLeagueScoringPresetsQuery, } from '@/lib/di-container'; import { validateLeagueWizardStep, validateAllLeagueWizardSteps, hasWizardErrors, createLeagueFromConfig, applyScoringPresetToConfig, } from '@/lib/leagueWizardService'; import type { LeagueScoringPresetDTO } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider'; import type { LeagueConfigFormModel } from '@gridpilot/racing/application'; import { LeagueBasicsSection } from './LeagueBasicsSection'; import { LeagueVisibilitySection } from './LeagueVisibilitySection'; import { LeagueStructureSection } from './LeagueStructureSection'; import { LeagueScoringSection, ScoringPatternSection, ChampionshipsSection, } from './LeagueScoringSection'; import { LeagueDropSection } from './LeagueDropSection'; import { LeagueTimingsSection } from './LeagueTimingsSection'; import { LeagueStewardingSection } from './LeagueStewardingSection'; // ============================================================================ // LOCAL STORAGE PERSISTENCE // ============================================================================ const STORAGE_KEY = 'gridpilot_league_wizard_draft'; const STORAGE_HIGHEST_STEP_KEY = 'gridpilot_league_wizard_highest_step'; function saveFormToStorage(form: LeagueConfigFormModel): void { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(form)); } catch { // Ignore storage errors (quota exceeded, etc.) } } function loadFormFromStorage(): LeagueConfigFormModel | null { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { return JSON.parse(stored) as LeagueConfigFormModel; } } catch { // Ignore parse errors } return null; } function clearFormStorage(): void { try { localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_HIGHEST_STEP_KEY); } catch { // Ignore storage errors } } function saveHighestStep(step: number): void { try { const current = getHighestStep(); if (step > current) { localStorage.setItem(STORAGE_HIGHEST_STEP_KEY, String(step)); } } catch { // Ignore storage errors } } function getHighestStep(): number { try { const stored = localStorage.getItem(STORAGE_HIGHEST_STEP_KEY); return stored ? parseInt(stored, 10) : 1; } catch { return 1; } } type Step = 1 | 2 | 3 | 4 | 5 | 6 | 7; type StepName = 'basics' | 'visibility' | 'structure' | 'schedule' | 'scoring' | 'stewarding' | 'review'; interface CreateLeagueWizardProps { stepName: StepName; onStepChange: (stepName: StepName) => void; } function stepNameToStep(stepName: StepName): Step { switch (stepName) { case 'basics': return 1; case 'visibility': return 2; case 'structure': return 3; case 'schedule': return 4; case 'scoring': return 5; case 'stewarding': return 6; case 'review': return 7; } } function stepToStepName(step: Step): StepName { switch (step) { case 1: return 'basics'; case 2: return 'visibility'; case 3: return 'structure'; case 4: return 'schedule'; case 5: return 'scoring'; case 6: return 'stewarding'; case 7: return 'review'; } } import type { WizardErrors } from '@/lib/leagueWizardService'; function getDefaultSeasonStartDate(): string { // Default to next Saturday const now = new Date(); const daysUntilSaturday = (6 - now.getDay() + 7) % 7 || 7; const nextSaturday = new Date(now); nextSaturday.setDate(now.getDate() + daysUntilSaturday); const [datePart] = nextSaturday.toISOString().split('T'); return datePart ?? ''; } function createDefaultForm(): LeagueConfigFormModel { const defaultPatternId = 'sprint-main-driver'; return { basics: { name: '', description: '', visibility: 'public', gameId: 'iracing', }, structure: { mode: 'solo', maxDrivers: 24, 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: 20, mainRaceMinutes: 40, sessionCount: 2, roundsPlanned: 8, // Default to Saturday races, weekly, starting next week weekdays: ['Sat'] as import('@gridpilot/racing/domain/types/Weekday').Weekday[], recurrenceStrategy: 'weekly' as const, raceStartTime: '20:00', timezoneId: 'UTC', seasonStartDate: getDefaultSeasonStartDate(), }, stewarding: { decisionMode: 'admin_only', requiredVotes: 2, requireDefense: false, defenseTimeLimit: 48, voteTimeLimit: 72, protestDeadlineHours: 48, stewardingClosesHours: 168, notifyAccusedOnProtest: true, notifyOnVoteRequired: true, }, }; } export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizardProps) { const router = useRouter(); const step = stepNameToStep(stepName); const [loading, setLoading] = useState(false); const [presetsLoading, setPresetsLoading] = useState(true); const [presets, setPresets] = useState([]); const [errors, setErrors] = useState({}); const [highestCompletedStep, setHighestCompletedStep] = useState(1); const [isHydrated, setIsHydrated] = useState(false); // Initialize form from localStorage or defaults const [form, setForm] = useState(() => createDefaultForm(), ); // Hydrate from localStorage on mount useEffect(() => { const stored = loadFormFromStorage(); if (stored) { setForm(stored); } setHighestCompletedStep(getHighestStep()); setIsHydrated(true); }, []); // Save form to localStorage whenever it changes (after hydration) useEffect(() => { if (isHydrated) { saveFormToStorage(form); } }, [form, isHydrated]); // Track highest step reached useEffect(() => { if (isHydrated) { saveHighestStep(step); setHighestCompletedStep((prev) => Math.max(prev, step)); } }, [step, isHydrated]); useEffect(() => { async function loadPresets() { try { const query = getListLeagueScoringPresetsQuery(); const result = await query.execute(); setPresets(result); const firstPreset = result[0]; if (firstPreset) { setForm((prev) => ({ ...prev, scoring: { ...prev.scoring, patternId: prev.scoring.patternId || firstPreset.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 stepErrors = validateLeagueWizardStep(form, currentStep); setErrors((prev) => ({ ...prev, ...stepErrors, })); return !hasWizardErrors(stepErrors); }; const goToNextStep = () => { if (!validateStep(step)) { return; } const nextStep = (step < 7 ? ((step + 1) as Step) : step); saveHighestStep(nextStep); setHighestCompletedStep((prev) => Math.max(prev, nextStep)); onStepChange(stepToStepName(nextStep)); }; const goToPreviousStep = () => { const prevStep = (step > 1 ? ((step - 1) as Step) : step); onStepChange(stepToStepName(prevStep)); }; // Navigate to a specific step (only if it's been reached before) const goToStep = useCallback((targetStep: Step) => { if (targetStep <= highestCompletedStep) { onStepChange(stepToStepName(targetStep)); } }, [highestCompletedStep, onStepChange]); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (loading) return; const allErrors = validateAllLeagueWizardSteps(form); setErrors((prev) => ({ ...prev, ...allErrors, })); if (hasWizardErrors(allErrors)) { onStepChange('basics'); return; } setLoading(true); setErrors((prev) => { const { submit, ...rest } = prev; return rest; }); try { const result = await createLeagueFromConfig(form); // Clear the draft on successful creation clearFormStorage(); router.push(`/leagues/${result.leagueId}`); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to create league'; setErrors((prev) => ({ ...prev, submit: message, })); setLoading(false); } }; const currentPreset = presets.find((p) => p.id === form.scoring.patternId) ?? null; // Handler for scoring preset selection - delegates to application-level config helper const handleScoringPresetChange = (patternId: string) => { setForm((prev) => applyScoringPresetToConfig(prev, patternId)); }; const steps = [ { id: 1 as Step, label: 'Basics', icon: FileText, shortLabel: 'Name' }, { id: 2 as Step, label: 'Visibility', icon: Award, shortLabel: 'Type' }, { id: 3 as Step, label: 'Structure', icon: Users, shortLabel: 'Mode' }, { id: 4 as Step, label: 'Schedule', icon: Calendar, shortLabel: 'Time' }, { id: 5 as Step, label: 'Scoring', icon: Trophy, shortLabel: 'Points' }, { id: 6 as Step, label: 'Stewarding', icon: Scale, shortLabel: 'Rules' }, { id: 7 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 your destiny'; case 3: return 'Choose the structure'; case 4: return 'Set the schedule'; case 5: return 'Scoring & championships'; case 6: return 'Stewarding & protests'; case 7: return 'Review & create'; default: return ''; } }; const getStepSubtitle = (currentStep: Step): string => { switch (currentStep) { case 1: return 'Give your league a memorable name and tell your story.'; case 2: return 'Will you compete for global rankings or race with friends?'; case 3: return 'Will drivers compete individually or as part of teams?'; case 4: return 'Configure session durations and plan your season calendar.'; case 5: return 'Select a scoring preset, enable championships, and set drop rules.'; case 6: return 'Configure how protests are handled and penalties decided.'; case 7: 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 isAccessible = wizardStep.id <= highestCompletedStep; const StepIcon = wizardStep.icon; return ( ); })}
{/* 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 && (
)} {step === 5 && (
{/* 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 === 6 && (
)} {step === 7 && (
{errors.submit && (

{errors.submit}

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

You can edit all settings after creating your league

); }