714 lines
23 KiB
TypeScript
714 lines
23 KiB
TypeScript
'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<Step>(1);
|
|
const [loading, setLoading] = useState(false);
|
|
const [presetsLoading, setPresetsLoading] = useState(true);
|
|
const [presets, setPresets] = useState<LeagueScoringPresetDTO[]>([]);
|
|
const [errors, setErrors] = useState<WizardErrors>({});
|
|
const [form, setForm] = useState<LeagueConfigFormModel>(() =>
|
|
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 (
|
|
<form onSubmit={handleSubmit} className="max-w-4xl mx-auto pb-8">
|
|
{/* Header with icon */}
|
|
<div className="mb-8">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-gradient-to-br from-primary-blue/20 to-primary-blue/5 border border-primary-blue/20">
|
|
<Sparkles className="w-5 h-5 text-primary-blue" />
|
|
</div>
|
|
<div>
|
|
<Heading level={1} className="text-2xl sm:text-3xl">
|
|
Create a new league
|
|
</Heading>
|
|
<p className="text-sm text-gray-500">
|
|
Set up your racing series in {steps.length} easy steps
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Desktop Progress Bar */}
|
|
<div className="hidden md:block mb-8">
|
|
<div className="relative">
|
|
{/* Background track */}
|
|
<div className="absolute top-5 left-6 right-6 h-0.5 bg-charcoal-outline rounded-full" />
|
|
{/* Progress fill */}
|
|
<div
|
|
className="absolute top-5 left-6 h-0.5 bg-gradient-to-r from-primary-blue to-neon-aqua rounded-full transition-all duration-500 ease-out"
|
|
style={{ width: `calc(${((step - 1) / (steps.length - 1)) * 100}% - 48px)` }}
|
|
/>
|
|
|
|
<div className="relative flex justify-between">
|
|
{steps.map((wizardStep) => {
|
|
const isCompleted = wizardStep.id < step;
|
|
const isCurrent = wizardStep.id === step;
|
|
const StepIcon = wizardStep.icon;
|
|
|
|
return (
|
|
<div key={wizardStep.id} className="flex flex-col items-center">
|
|
<div
|
|
className={`
|
|
relative z-10 flex h-10 w-10 items-center justify-center rounded-full
|
|
transition-all duration-300 ease-out
|
|
${isCurrent
|
|
? 'bg-primary-blue text-white shadow-[0_0_24px_rgba(25,140,255,0.5)] scale-110'
|
|
: isCompleted
|
|
? 'bg-primary-blue text-white'
|
|
: 'bg-iron-gray text-gray-500 border-2 border-charcoal-outline'
|
|
}
|
|
`}
|
|
>
|
|
{isCompleted ? (
|
|
<Check className="w-4 h-4" strokeWidth={3} />
|
|
) : (
|
|
<StepIcon className="w-4 h-4" />
|
|
)}
|
|
</div>
|
|
<div className="mt-2 text-center">
|
|
<p
|
|
className={`text-xs font-medium transition-colors duration-200 ${
|
|
isCurrent
|
|
? 'text-white'
|
|
: isCompleted
|
|
? 'text-primary-blue'
|
|
: 'text-gray-500'
|
|
}`}
|
|
>
|
|
{wizardStep.label}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Progress */}
|
|
<div className="md:hidden mb-6">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<CurrentStepIcon className="w-4 h-4 text-primary-blue" />
|
|
<span className="text-sm font-medium text-white">{currentStepData?.label}</span>
|
|
</div>
|
|
<span className="text-xs text-gray-500">
|
|
{step}/{steps.length}
|
|
</span>
|
|
</div>
|
|
<div className="h-1.5 bg-charcoal-outline rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-primary-blue to-neon-aqua rounded-full transition-all duration-500 ease-out"
|
|
style={{ width: `${(step / steps.length) * 100}%` }}
|
|
/>
|
|
</div>
|
|
{/* Step dots */}
|
|
<div className="flex justify-between mt-2 px-0.5">
|
|
{steps.map((s) => (
|
|
<div
|
|
key={s.id}
|
|
className={`
|
|
h-1.5 rounded-full transition-all duration-300
|
|
${s.id === step
|
|
? 'w-4 bg-primary-blue'
|
|
: s.id < step
|
|
? 'w-1.5 bg-primary-blue/60'
|
|
: 'w-1.5 bg-charcoal-outline'
|
|
}
|
|
`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Card */}
|
|
<Card className="relative overflow-hidden">
|
|
{/* Top gradient accent */}
|
|
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-transparent via-primary-blue to-transparent" />
|
|
|
|
{/* Step header */}
|
|
<div className="flex items-start gap-4 mb-6">
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-blue/10 shrink-0 transition-transform duration-300">
|
|
<CurrentStepIcon className="w-6 h-6 text-primary-blue" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<Heading level={2} className="text-xl sm:text-2xl text-white leading-tight">
|
|
{getStepTitle(step)}
|
|
</Heading>
|
|
<p className="text-sm text-gray-400 mt-1">
|
|
{getStepSubtitle(step)}
|
|
</p>
|
|
</div>
|
|
<div className="hidden sm:flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-deep-graphite border border-charcoal-outline">
|
|
<span className="text-xs text-gray-500">Step</span>
|
|
<span className="text-sm font-semibold text-white">{step}</span>
|
|
<span className="text-xs text-gray-500">/ {steps.length}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<div className="h-px bg-gradient-to-r from-transparent via-charcoal-outline to-transparent mb-6" />
|
|
|
|
{/* Step content with min-height for consistency */}
|
|
<div className="min-h-[320px]">
|
|
{step === 1 && (
|
|
<div className="animate-fade-in">
|
|
<LeagueBasicsSection
|
|
form={form}
|
|
onChange={setForm}
|
|
errors={errors.basics}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<div className="animate-fade-in">
|
|
<LeagueStructureSection
|
|
form={form}
|
|
onChange={setForm}
|
|
readOnly={false}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<div className="animate-fade-in">
|
|
<LeagueTimingsSection
|
|
form={form}
|
|
onChange={setForm}
|
|
errors={errors.timings}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{step === 4 && (
|
|
<div className="animate-fade-in space-y-8">
|
|
{/* Scoring Pattern Selection */}
|
|
<ScoringPatternSection
|
|
scoring={form.scoring}
|
|
presets={presets}
|
|
readOnly={presetsLoading}
|
|
patternError={errors.scoring?.patternId}
|
|
onChangePatternId={handleScoringPresetChange}
|
|
onToggleCustomScoring={() =>
|
|
setForm((prev) => ({
|
|
...prev,
|
|
scoring: {
|
|
...prev.scoring,
|
|
customScoringEnabled: !prev.scoring.customScoringEnabled,
|
|
},
|
|
}))
|
|
}
|
|
/>
|
|
|
|
{/* Divider */}
|
|
<div className="h-px bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" />
|
|
|
|
{/* Championships & Drop Rules side by side on larger screens */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<ChampionshipsSection form={form} onChange={setForm} readOnly={presetsLoading} />
|
|
<LeagueDropSection form={form} onChange={setForm} readOnly={false} />
|
|
</div>
|
|
|
|
{errors.submit && (
|
|
<div className="flex items-start gap-3 rounded-lg bg-warning-amber/10 p-4 border border-warning-amber/20">
|
|
<AlertCircle className="w-5 h-5 text-warning-amber shrink-0 mt-0.5" />
|
|
<p className="text-sm text-warning-amber">{errors.submit}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{step === 5 && (
|
|
<div className="animate-fade-in space-y-6">
|
|
<LeagueReviewSummary form={form} presets={presets} />
|
|
{errors.submit && (
|
|
<div className="flex items-start gap-3 rounded-lg bg-warning-amber/10 p-4 border border-warning-amber/20">
|
|
<AlertCircle className="w-5 h-5 text-warning-amber shrink-0 mt-0.5" />
|
|
<p className="text-sm text-warning-amber">{errors.submit}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Navigation */}
|
|
<div className="flex justify-between items-center mt-6">
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
disabled={step === 1 || loading}
|
|
onClick={goToPreviousStep}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
<span className="hidden sm:inline">Back</span>
|
|
</Button>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{/* Mobile step dots */}
|
|
<div className="flex sm:hidden items-center gap-1">
|
|
{steps.map((s) => (
|
|
<div
|
|
key={s.id}
|
|
className={`
|
|
h-1.5 rounded-full transition-all duration-300
|
|
${s.id === step ? 'w-3 bg-primary-blue' : s.id < step ? 'w-1.5 bg-primary-blue/50' : 'w-1.5 bg-charcoal-outline'}
|
|
`}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{step < 5 ? (
|
|
<Button
|
|
type="button"
|
|
variant="primary"
|
|
disabled={loading}
|
|
onClick={goToNextStep}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<span>Continue</span>
|
|
<ChevronRight className="w-4 h-4" />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
disabled={loading}
|
|
className="flex items-center gap-2 min-w-[150px] justify-center"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
<span>Creating…</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Sparkles className="w-4 h-4" />
|
|
<span>Create League</span>
|
|
</>
|
|
)}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Helper text */}
|
|
<p className="text-center text-xs text-gray-500 mt-4">
|
|
You can edit all settings after creating your league
|
|
</p>
|
|
</form>
|
|
);
|
|
} |