'use client'; import { useAuth } from '@/components/auth/AuthContext'; import { useRouter } from 'next/navigation'; import { FormEvent, useCallback, useEffect, useState } from 'react'; import { LeagueWizardCommandModel } from '@/lib/command-models/leagues/LeagueWizardCommandModel'; import { useLeagueScoringPresets } from "@/hooks/useLeagueScoringPresets"; import { useCreateLeagueWizard } from "@/hooks/useLeagueWizardService"; import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; import type { Weekday } from '@/lib/types/Weekday'; import type { WizardErrors } from '@/lib/types/WizardErrors'; import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel'; import { CreateLeagueWizardTemplate, Step } from '@/templates/CreateLeagueWizardTemplate'; import { SearchParamBuilder } from '@/lib/routing/search-params/SearchParamBuilder'; import { Award, Calendar, CheckCircle2, FileText, Scale, Trophy, Users } from 'lucide-react'; // ============================================================================ // LOCAL STORAGE PERSISTENCE // ============================================================================ const STORAGE_KEY = 'gridpilot_league_wizard_draft'; const STORAGE_HIGHEST_STEP_KEY = 'gridpilot_league_wizard_highest_step'; function saveFormToStorage(form: LeagueWizardFormModel): void { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(form)); } catch { // Ignore storage errors } } function loadFormFromStorage(): LeagueWizardFormModel | null { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { const parsed = JSON.parse(stored) as LeagueWizardFormModel; if (!parsed.seasonName) { const seasonStartDate = parsed.timings?.seasonStartDate; parsed.seasonName = getDefaultSeasonName(seasonStartDate); } return parsed; } } 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 StepName = 'basics' | 'visibility' | 'structure' | 'schedule' | 'scoring' | 'stewarding' | 'review'; type LeagueWizardFormModel = LeagueConfigFormModel & { seasonName?: string; }; 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'; } } function getDefaultSeasonStartDate(): string { 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 getDefaultSeasonName(seasonStartDate?: string): string { if (seasonStartDate) { const parsed = new Date(seasonStartDate); if (!Number.isNaN(parsed.getTime())) { const year = parsed.getFullYear(); return `Season 1 (${year})`; } } const fallbackYear = new Date().getFullYear(); return `Season 1 (${fallbackYear})`; } function createDefaultForm(): LeagueWizardFormModel { const defaultPatternId = 'sprint-main-driver'; const defaultSeasonStartDate = getDefaultSeasonStartDate(); 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, weekdays: ['Sat'] as Weekday[], recurrenceStrategy: 'weekly' as const, timezoneId: 'UTC', seasonStartDate: defaultSeasonStartDate, }, stewarding: { decisionMode: 'admin_only', requiredVotes: 2, requireDefense: false, defenseTimeLimit: 48, voteTimeLimit: 72, protestDeadlineHours: 48, stewardingClosesHours: 168, notifyAccusedOnProtest: true, notifyOnVoteRequired: true, }, seasonName: getDefaultSeasonName(defaultSeasonStartDate), }; } export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizardProps) { const router = useRouter(); const { session } = useAuth(); const handleStepChange = useCallback((newStepName: StepName) => { if (onStepChange) { onStepChange(newStepName); } else { const builder = new SearchParamBuilder(); builder.step(newStepName); router.push(`/leagues/create${builder.build()}`); } }, [onStepChange, router]); 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); const [form, setForm] = useState(() => createDefaultForm(), ); useEffect(() => { const stored = loadFormFromStorage(); if (stored) { setForm(stored); } setHighestCompletedStep(getHighestStep()); setIsHydrated(true); }, []); useEffect(() => { if (isHydrated) { saveFormToStorage(form); } }, [form, isHydrated]); useEffect(() => { if (isHydrated) { saveHighestStep(step); setHighestCompletedStep((prev) => Math.max(prev, step)); } }, [step, isHydrated]); const { data: queryPresets, error: presetsError } = useLeagueScoringPresets(); useEffect(() => { if (queryPresets) { setPresets(queryPresets as any); const firstPreset = queryPresets[0]; if (firstPreset && !form.scoring?.patternId) { setForm((prev) => ({ ...prev, scoring: { ...prev.scoring, patternId: firstPreset.id, customScoringEnabled: false, }, })); } setPresetsLoading(false); } }, [queryPresets, form.scoring?.patternId]); useEffect(() => { if (presetsError) { setErrors((prev) => ({ ...prev, submit: presetsError instanceof Error ? presetsError.message : 'Failed to load scoring presets', })); } }, [presetsError]); const createLeagueMutation = useCreateLeagueWizard(); const validateStep = (currentStep: Step): boolean => { const formData: any = { leagueId: form.leagueId || '', basics: { name: form.basics?.name || '', description: form.basics?.description || '', visibility: (form.basics?.visibility as 'public' | 'private' | 'unlisted') || 'public', gameId: form.basics?.gameId || 'iracing', }, structure: { mode: (form.structure?.mode as 'solo' | 'fixedTeams') || 'solo', maxDrivers: form.structure?.maxDrivers || 0, maxTeams: form.structure?.maxTeams || 0, driversPerTeam: form.structure?.driversPerTeam || 0, }, championships: { enableDriverChampionship: form.championships?.enableDriverChampionship ?? true, enableTeamChampionship: form.championships?.enableTeamChampionship ?? false, enableNationsChampionship: form.championships?.enableNationsChampionship ?? false, enableTrophyChampionship: form.championships?.enableTrophyChampionship ?? false, }, scoring: { patternId: form.scoring?.patternId || '', customScoringEnabled: form.scoring?.customScoringEnabled ?? false, }, dropPolicy: { strategy: (form.dropPolicy?.strategy as 'none' | 'bestNResults' | 'dropWorstN') || 'bestNResults', n: form.dropPolicy?.n || 6, }, timings: { practiceMinutes: form.timings?.practiceMinutes || 0, qualifyingMinutes: form.timings?.qualifyingMinutes || 0, sprintRaceMinutes: form.timings?.sprintRaceMinutes || 0, mainRaceMinutes: form.timings?.mainRaceMinutes || 0, sessionCount: form.timings?.sessionCount || 0, roundsPlanned: form.timings?.roundsPlanned || 0, }, stewarding: { decisionMode: (form.stewarding?.decisionMode as 'owner_only' | 'admin_vote' | 'steward_panel') || 'admin_only', requiredVotes: form.stewarding?.requiredVotes || 0, requireDefense: form.stewarding?.requireDefense ?? false, defenseTimeLimit: form.stewarding?.defenseTimeLimit || 0, voteTimeLimit: form.stewarding?.voteTimeLimit || 0, protestDeadlineHours: form.stewarding?.protestDeadlineHours || 0, stewardingClosesHours: form.stewarding?.stewardingClosesHours || 0, notifyAccusedOnProtest: form.stewarding?.notifyAccusedOnProtest ?? true, notifyOnVoteRequired: form.stewarding?.notifyOnVoteRequired ?? true, }, }; const stepErrors = LeagueWizardCommandModel.validateLeagueWizardStep(formData, currentStep as any); setErrors((prev) => ({ ...prev, ...stepErrors, })); return !LeagueWizardCommandModel.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)); handleStepChange(stepToStepName(nextStep)); }; const goToPreviousStep = () => { const prevStep = (step > 1 ? ((step - 1) as Step) : step); handleStepChange(stepToStepName(prevStep)); }; const goToStep = useCallback((targetStep: Step) => { if (targetStep <= highestCompletedStep) { handleStepChange(stepToStepName(targetStep)); } }, [highestCompletedStep, handleStepChange]); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (loading) return; const ownerId = session?.user.userId; if (!ownerId) { setErrors((prev) => ({ ...prev, submit: 'You must be logged in to create a league', })); return; } const formData: any = { leagueId: form.leagueId || '', basics: { name: form.basics?.name || '', description: form.basics?.description || '', visibility: (form.basics?.visibility as 'public' | 'private' | 'unlisted') || 'public', gameId: form.basics?.gameId || 'iracing', }, structure: { mode: (form.structure?.mode as 'solo' | 'fixedTeams') || 'solo', maxDrivers: form.structure?.maxDrivers || 0, maxTeams: form.structure?.maxTeams || 0, driversPerTeam: form.structure?.driversPerTeam || 0, }, championships: { enableDriverChampionship: form.championships?.enableDriverChampionship ?? true, enableTeamChampionship: form.championships?.enableTeamChampionship ?? false, enableNationsChampionship: form.championships?.enableNationsChampionship ?? false, enableTrophyChampionship: form.championships?.enableTrophyChampionship ?? false, }, scoring: { patternId: form.scoring?.patternId || '', customScoringEnabled: form.scoring?.customScoringEnabled ?? false, }, dropPolicy: { strategy: (form.dropPolicy?.strategy as 'none' | 'bestNResults' | 'dropWorstN') || 'bestNResults', n: form.dropPolicy?.n || 6, }, timings: { practiceMinutes: form.timings?.practiceMinutes || 0, qualifyingMinutes: form.timings?.qualifyingMinutes || 0, sprintRaceMinutes: form.timings?.sprintRaceMinutes || 0, mainRaceMinutes: form.timings?.mainRaceMinutes || 0, sessionCount: form.timings?.sessionCount || 0, roundsPlanned: form.timings?.roundsPlanned || 0, }, stewarding: { decisionMode: (form.stewarding?.decisionMode as 'owner_only' | 'admin_vote' | 'steward_panel') || 'admin_only', requiredVotes: form.stewarding?.requiredVotes || 0, requireDefense: form.stewarding?.requireDefense ?? false, defenseTimeLimit: form.stewarding?.defenseTimeLimit || 0, voteTimeLimit: form.stewarding?.voteTimeLimit || 0, protestDeadlineHours: form.stewarding?.protestDeadlineHours || 0, stewardingClosesHours: form.stewarding?.stewardingClosesHours || 0, notifyAccusedOnProtest: form.stewarding?.notifyAccusedOnProtest ?? true, notifyOnVoteRequired: form.stewarding?.notifyOnVoteRequired ?? true, }, }; const allErrors = LeagueWizardCommandModel.validateAllLeagueWizardSteps(formData); setErrors((prev) => ({ ...prev, ...allErrors, })); if (LeagueWizardCommandModel.hasWizardErrors(allErrors)) { handleStepChange('basics'); return; } setLoading(true); setErrors((prev) => { const { submit: _, ...rest } = prev; return rest; }); try { const result = await createLeagueMutation.mutateAsync({ form, ownerId }); 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 handleScoringPresetChange = (patternId: string) => { setForm((prev) => { const selectedPreset = presets.find((p) => p.id === patternId); return { ...prev, scoring: { ...prev.scoring, patternId, customScoringEnabled: false, }, timings: selectedPreset ? { ...prev.timings, practiceMinutes: prev.timings?.practiceMinutes ?? selectedPreset.defaultTimings.practiceMinutes, qualifyingMinutes: prev.timings?.qualifyingMinutes ?? selectedPreset.defaultTimings.qualifyingMinutes, sprintRaceMinutes: prev.timings?.sprintRaceMinutes ?? selectedPreset.defaultTimings.sprintRaceMinutes, mainRaceMinutes: prev.timings?.mainRaceMinutes ?? selectedPreset.defaultTimings.mainRaceMinutes, sessionCount: selectedPreset.defaultTimings.sessionCount, } : prev.timings, }; }); }; 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 'Define how races in this season will run.'; case 4: return 'Plan when this season’s races happen.'; case 5: return 'Choose how points and drop scores work for this season.'; case 6: return 'Set how protests and stewarding work for this season.'; case 7: return 'Review your league and first season before launching.'; default: return ''; } }; const getStepContextLabel = (currentStep: Step): string => { if (currentStep === 1 || currentStep === 2) return 'League setup'; if (currentStep >= 3 && currentStep <= 6) return 'Season setup'; return 'League & Season summary'; }; return ( setForm((prev) => ({ ...prev, scoring: { ...prev.scoring, customScoringEnabled: !(prev.scoring?.customScoringEnabled), }, })) } getStepTitle={getStepTitle} getStepSubtitle={getStepSubtitle} getStepContextLabel={getStepContextLabel} /> ); }