website refactor

This commit is contained in:
2026-01-16 01:00:03 +01:00
parent ce7be39155
commit a98e3e3166
286 changed files with 5522 additions and 5261 deletions

View File

@@ -5,6 +5,10 @@ import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Input } from '@/ui/Input';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { useAuth } from '@/lib/auth/AuthContext';
import {
AlertCircle,
@@ -22,22 +26,22 @@ import {
Users,
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { FormEvent, useCallback, useEffect, useState } from 'react';
import { LeagueWizardCommandModel } from '@/lib/command-models/leagues/LeagueWizardCommandModel';
import { useCreateLeagueWizard } from "@/lib/hooks/useLeagueWizardService";
import { useLeagueScoringPresets } from "@/lib/hooks/useLeagueScoringPresets";
import { LeagueBasicsSection } from './LeagueBasicsSection';
import { LeagueDropSection } from './LeagueDropSection';
import { useCreateLeagueWizard } from "@/hooks/useLeagueWizardService";
import { useLeagueScoringPresets } from "@/hooks/useLeagueScoringPresets";
import { LeagueBasicsSection } from '@/components/leagues/LeagueBasicsSection';
import { LeagueDropSection } from '@/components/leagues/LeagueDropSection';
import {
ChampionshipsSection,
ScoringPatternSection
} from './LeagueScoringSection';
import { LeagueStewardingSection } from './LeagueStewardingSection';
import { LeagueStructureSection } from './LeagueStructureSection';
import { LeagueTimingsSection } from './LeagueTimingsSection';
import { LeagueVisibilitySection } from './LeagueVisibilitySection';
} from '@/components/leagues/LeagueScoringSection';
import { LeagueStewardingSection } from '@/components/leagues/LeagueStewardingSection';
import { LeagueStructureSection } from '@/components/leagues/LeagueStructureSection';
import { LeagueTimingsSection } from '@/components/leagues/LeagueTimingsSection';
import { LeagueVisibilitySection } from '@/components/leagues/LeagueVisibilitySection';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
import type { Weekday } from '@/lib/types/Weekday';
@@ -237,7 +241,7 @@ function createDefaultForm(): LeagueWizardFormModel {
};
}
export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizardProps) {
export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizardProps) {
const router = useRouter();
const { session } = useAuth();
@@ -285,7 +289,8 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
// Sync presets from query to local state
useEffect(() => {
if (queryPresets) {
setPresets(queryPresets);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setPresets(queryPresets as any);
const firstPreset = queryPresets[0];
if (firstPreset && !form.scoring?.patternId) {
setForm((prev) => ({
@@ -316,7 +321,8 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
const validateStep = (currentStep: Step): boolean => {
// Convert form to LeagueWizardFormData for validation
const formData: LeagueWizardCommandModel.LeagueWizardFormData = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const formData: any = {
leagueId: form.leagueId || '',
basics: {
name: form.basics?.name || '',
@@ -365,7 +371,8 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
},
};
const stepErrors = LeagueWizardCommandModel.validateLeagueWizardStep(formData, currentStep);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stepErrors = LeagueWizardCommandModel.validateLeagueWizardStep(formData, currentStep as any);
setErrors((prev) => ({
...prev,
...stepErrors,
@@ -409,7 +416,8 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
}
// Convert form to LeagueWizardFormData for validation
const formData: LeagueWizardCommandModel.LeagueWizardFormData = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const formData: any = {
leagueId: form.leagueId || '',
basics: {
name: form.basics?.name || '',
@@ -471,7 +479,7 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
setLoading(true);
setErrors((prev) => {
const { submit, ...rest } = prev;
const { submit: _, ...rest } = prev;
return rest;
});
@@ -587,39 +595,45 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
const CurrentStepIcon = currentStepData?.icon ?? FileText;
return (
<form onSubmit={handleSubmit} className="max-w-4xl mx-auto pb-8">
<Box as="form" onSubmit={handleSubmit} maxWidth="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">
<Box mb={8}>
<Stack direction="row" align="center" gap={3} mb={3}>
<Box display="flex" h="11" w="11" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/20" border borderColor="border-primary-blue/20">
<Icon icon={Sparkles} size={5} color="text-primary-blue" />
</Box>
<Box>
<Heading level={1} fontSize={{ base: '2xl', sm: '3xl' }}>
Create a new league
</Heading>
<p className="text-sm text-gray-500">
We'll also set up your first season in {steps.length} easy steps.
</p>
<p className="text-xs text-gray-500 mt-1">
<Text size="sm" color="text-gray-500" block>
We&apos;ll also set up your first season in {steps.length} easy steps.
</Text>
<Text size="xs" color="text-gray-500" block mt={1}>
A league is your long-term brand. Each season is a block of races you can run again and again.
</p>
</div>
</div>
</div>
</Text>
</Box>
</Stack>
</Box>
{/* Desktop Progress Bar */}
<div className="hidden md:block mb-8">
<div className="relative">
<Box display={{ base: 'none', md: 'block' }} mb={8}>
<Box position="relative">
{/* Background track */}
<div className="absolute top-5 left-6 right-6 h-0.5 bg-charcoal-outline rounded-full" />
<Box position="absolute" top="5" left="6" right="6" h="0.5" bg="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)` }}
<Box
position="absolute"
top="5"
left="6"
h="0.5"
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
rounded="full"
transition
width={`calc(${((step - 1) / (steps.length - 1)) * 100}% - 48px)`}
/>
<div className="relative flex justify-between">
<Box position="relative" display="flex" justifyContent="between">
{steps.map((wizardStep) => {
const isCompleted = wizardStep.id < step;
const isCurrent = wizardStep.id === step;
@@ -627,148 +641,153 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
const StepIcon = wizardStep.icon;
return (
<button
<Box
as="button"
key={wizardStep.id}
type="button"
onClick={() => goToStep(wizardStep.id)}
disabled={!isAccessible}
className="flex flex-col items-center bg-transparent border-0 cursor-pointer disabled:cursor-not-allowed"
display="flex"
flexDirection="col"
alignItems="center"
bg="bg-transparent"
borderStyle="none"
cursor={isAccessible ? 'pointer' : 'not-allowed'}
opacity={!isAccessible ? 0.6 : 1}
>
<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 hover:scale-105'
: isAccessible
? 'bg-iron-gray text-gray-400 border-2 border-charcoal-outline hover:border-primary-blue/50'
: 'bg-iron-gray text-gray-500 border-2 border-charcoal-outline opacity-60'
}
`}
<Box
position="relative"
zIndex={10}
display="flex"
h="10"
w="10"
alignItems="center"
justifyContent="center"
rounded="full"
transition
bg={isCurrent || isCompleted ? 'bg-primary-blue' : 'bg-iron-gray'}
color={isCurrent || isCompleted ? 'text-white' : 'text-gray-400'}
border={!isCurrent && !isCompleted}
borderColor="border-charcoal-outline"
shadow={isCurrent ? '0_0_24px_rgba(25,140,255,0.5)' : undefined}
transform={isCurrent ? 'scale-110' : isCompleted ? 'hover:scale-105' : undefined}
>
{isCompleted ? (
<Check className="w-4 h-4" strokeWidth={3} />
<Icon icon={Check} size={4} strokeWidth={3} />
) : (
<StepIcon className="w-4 h-4" />
<Icon icon={StepIcon} size={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'
: isAccessible
? 'text-gray-400'
: 'text-gray-500'
}`}
</Box>
<Box mt={2} textAlign="center">
<Text
size="xs"
weight="medium"
transition
color={isCurrent ? 'text-white' : isCompleted ? 'text-primary-blue' : isAccessible ? 'text-gray-400' : 'text-gray-500'}
>
{wizardStep.label}
</p>
</div>
</button>
</Text>
</Box>
</Box>
);
})}
</div>
</div>
</div>
</Box>
</Box>
</Box>
{/* 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">
<Box display={{ base: 'block', md: 'none' }} mb={6}>
<Box display="flex" alignItems="center" justifyContent="between" mb={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={CurrentStepIcon} size={4} color="text-primary-blue" />
<Text size="sm" weight="medium" color="text-white">{currentStepData?.label}</Text>
</Stack>
<Text size="xs" color="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}%` }}
</Text>
</Box>
<Box h="1.5" bg="bg-charcoal-outline" rounded="full" overflow="hidden">
<Box
h="full"
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
rounded="full"
transition
height="full"
width={`${(step / steps.length) * 100}%`}
/>
</div>
</Box>
{/* Step dots */}
<div className="flex justify-between mt-2 px-0.5">
<Box display="flex" justifyContent="between" mt={2} px={0.5}>
{steps.map((s) => (
<div
<Box
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'
}
`}
h="1.5"
rounded="full"
transition
width={s.id === step ? '4' : '1.5'}
bg={s.id === step ? 'bg-primary-blue' : s.id < step ? 'bg-primary-blue/60' : 'bg-charcoal-outline'}
/>
))}
</div>
</div>
</Box>
</Box>
{/* Main Card */}
<Card className="relative overflow-hidden">
<Card position="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" />
<Box position="absolute" top="0" left="0" right="0" h="1" bg="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">
<div className="flex items-center gap-2 flex-wrap">
<span>{getStepTitle(step)}</span>
<span className="inline-flex items-center px-2 py-0.5 rounded-full border border-charcoal-outline bg-iron-gray/60 text-[11px] font-medium text-gray-300">
<Box display="flex" alignItems="start" gap={4} mb={6}>
<Box display="flex" h="12" w="12" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10" flexShrink={0} transition>
<Icon icon={CurrentStepIcon} size={6} color="text-primary-blue" />
</Box>
<Box flexGrow={1} minWidth="0">
<Heading level={2} fontSize={{ base: 'xl', md: '2xl' }} color="text-white">
<Stack direction="row" align="center" gap={2} flexWrap="wrap">
<Text>{getStepTitle(step)}</Text>
<Text size="xs" weight="medium" px={2} py={0.5} rounded="full" border borderColor="border-charcoal-outline" bg="bg-iron-gray/60" color="text-gray-300">
{getStepContextLabel(step)}
</span>
</div>
</Text>
</Stack>
</Heading>
<p className="text-sm text-gray-400 mt-1">
<Text size="sm" color="text-gray-400" block 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>
</Text>
</Box>
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={1.5} px={3} py={1.5} rounded="full" bg="bg-deep-graphite" border borderColor="border-charcoal-outline">
<Text size="xs" color="text-gray-500">Step</Text>
<Text size="sm" weight="semibold" color="text-white">{step}</Text>
<Text size="xs" color="text-gray-500">/ {steps.length}</Text>
</Box>
</Box>
{/* Divider */}
<div className="h-px bg-gradient-to-r from-transparent via-charcoal-outline to-transparent mb-6" />
<Box h="px" bg="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]">
<Box minHeight="320px">
{step === 1 && (
<div className="animate-fade-in space-y-8">
<Box animate="fade-in" gap={8} display="flex" flexDirection="col">
<LeagueBasicsSection
form={form}
onChange={setForm}
errors={errors.basics ?? {}}
/>
<div className="rounded-xl border border-charcoal-outline bg-iron-gray/40 p-4">
<div className="flex items-center justify-between gap-2 mb-2">
<div>
<p className="text-xs font-semibold text-gray-300 uppercase tracking-wide">
<Box rounded="xl" border borderColor="border-charcoal-outline" bg="bg-iron-gray/40" p={4}>
<Box display="flex" alignItems="center" justifyContent="between" gap={2} mb={2}>
<Box>
<Text size="xs" weight="semibold" color="text-gray-300" uppercase letterSpacing="wide">
First season
</p>
<p className="text-xs text-gray-500">
</Text>
<Text size="xs" color="text-gray-500" block>
Name the first season that will run in this league.
</p>
</div>
</div>
<div className="space-y-2 mt-2">
<label className="text-sm font-medium text-gray-300">
</Text>
</Box>
</Box>
<Box mt={2} display="flex" flexDirection="col" gap={2}>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
Season name
</label>
</Text>
<Input
value={form.seasonName ?? ''}
onChange={(e) =>
@@ -779,16 +798,16 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
}
placeholder="e.g., Season 1 (2025)"
/>
<p className="text-xs text-gray-500">
<Text size="xs" color="text-gray-500" block>
Seasons are the individual competitive runs inside your league. You can run Season 2, Season 3, or parallel seasons later.
</p>
</div>
</div>
</div>
</Text>
</Box>
</Box>
</Box>
)}
{step === 2 && (
<div className="animate-fade-in">
<Box animate="fade-in">
<LeagueVisibilitySection
form={form}
onChange={setForm}
@@ -798,55 +817,55 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
: {}
}
/>
</div>
</Box>
)}
{step === 3 && (
<div className="animate-fade-in space-y-4">
<div className="mb-2">
<p className="text-xs text-gray-500">
<Box animate="fade-in" display="flex" flexDirection="col" gap={4}>
<Box mb={2}>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</p>
<p className="text-xs text-gray-500">
</Text>
<Text size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</p>
</div>
</Text>
</Box>
<LeagueStructureSection
form={form}
onChange={setForm}
readOnly={false}
/>
</div>
</Box>
)}
{step === 4 && (
<div className="animate-fade-in space-y-4">
<div className="mb-2">
<p className="text-xs text-gray-500">
<Box animate="fade-in" display="flex" flexDirection="col" gap={4}>
<Box mb={2}>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</p>
<p className="text-xs text-gray-500">
</Text>
<Text size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</p>
</div>
</Text>
</Box>
<LeagueTimingsSection
form={form}
onChange={setForm}
errors={errors.timings ?? {}}
/>
</div>
</Box>
)}
{step === 5 && (
<div className="animate-fade-in space-y-8">
<div className="mb-2">
<p className="text-xs text-gray-500">
<Box animate="fade-in" display="flex" flexDirection="col" gap={8}>
<Box mb={2}>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</p>
<p className="text-xs text-gray-500">
</Text>
<Text size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</p>
</div>
</Text>
</Box>
{/* Scoring Pattern Selection */}
<ScoringPatternSection
scoring={form.scoring || {}}
@@ -866,81 +885,81 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
/>
{/* Divider */}
<div className="h-px bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" />
<Box h="px" bg="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">
<Box display="grid" gridCols={{ base: 1, lg: 2 }} gap={6}>
<ChampionshipsSection form={form} onChange={setForm} readOnly={presetsLoading} />
<LeagueDropSection form={form} onChange={setForm} readOnly={false} />
</div>
</Box>
{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>
<Box display="flex" alignItems="start" gap={3} rounded="lg" bg="bg-warning-amber/10" p={4} border borderColor="border-warning-amber/20">
<Icon icon={AlertCircle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
<Text size="sm" color="text-warning-amber">{errors.submit}</Text>
</Box>
)}
</div>
</Box>
)}
{step === 6 && (
<div className="animate-fade-in space-y-4">
<div className="mb-2">
<p className="text-xs text-gray-500">
<Box animate="fade-in" display="flex" flexDirection="col" gap={4}>
<Box mb={2}>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</p>
<p className="text-xs text-gray-500">
</Text>
<Text size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</p>
</div>
</Text>
</Box>
<LeagueStewardingSection
form={form}
onChange={setForm}
readOnly={false}
/>
</div>
</Box>
)}
{step === 7 && (
<div className="animate-fade-in space-y-6">
<Box animate="fade-in" display="flex" flexDirection="col" gap={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>
<Box display="flex" alignItems="start" gap={3} rounded="lg" bg="bg-warning-amber/10" p={4} border borderColor="border-warning-amber/20">
<Icon icon={AlertCircle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
<Text size="sm" color="text-warning-amber">{errors.submit}</Text>
</Box>
)}
</div>
</Box>
)}
</div>
</Box>
</Card>
{/* Navigation */}
<div className="flex justify-between items-center mt-6">
<Box display="flex" alignItems="center" justifyContent="between" mt={6}>
<Button
type="button"
variant="secondary"
disabled={step === 1 || loading}
onClick={goToPreviousStep}
className="flex items-center gap-2"
icon={<Icon icon={ChevronLeft} size={4} />}
>
<ChevronLeft className="w-4 h-4" />
<span className="hidden sm:inline">Back</span>
<Text display={{ base: 'none', md: 'inline-block' }}>Back</Text>
</Button>
<div className="flex items-center gap-3">
<Box display="flex" alignItems="center" gap={3}>
{/* Mobile step dots */}
<div className="flex sm:hidden items-center gap-1">
<Box display={{ base: 'flex', sm: 'none' }} alignItems="center" gap={1}>
{steps.map((s) => (
<div
<Box
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'}
`}
h="1.5"
rounded="full"
transition
width={s.id === step ? '3' : '1.5'}
bg={s.id === step ? 'bg-primary-blue' : s.id < step ? 'bg-primary-blue/50' : 'bg-charcoal-outline'}
/>
))}
</div>
</Box>
{step < 7 ? (
<Button
@@ -948,38 +967,34 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
variant="primary"
disabled={loading}
onClick={goToNextStep}
className="flex items-center gap-2"
icon={<Icon icon={ChevronRight} size={4} />}
flexDirection="row-reverse"
>
<span>Continue</span>
<ChevronRight className="w-4 h-4" />
<Text>Continue</Text>
</Button>
) : (
<Button
type="submit"
variant="primary"
disabled={loading}
className="flex items-center gap-2 min-w-[150px] justify-center"
minWidth="150px"
justifyContent="center"
icon={loading ? <Icon icon={Loader2} size={4} animate="spin" /> : <Icon icon={Sparkles} size={4} />}
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>Creating…</span>
</>
<Text>Creating</Text>
) : (
<>
<Sparkles className="w-4 h-4" />
<span>Create League</span>
</>
<Text>Create League</Text>
)}
</Button>
)}
</div>
</div>
</Box>
</Box>
{/* Helper text */}
<p className="text-center text-xs text-gray-500 mt-4">
<Text size="xs" color="text-gray-500" align="center" block mt={4}>
This will create your league and its first season. You can edit both later.
</p>
</form>
</Text>
</Box>
);
}
}

View File

@@ -2,47 +2,42 @@
import React from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import CreateLeagueWizard from '@/components/leagues/CreateLeagueWizard';
import { CreateLeagueWizard } from './CreateLeagueWizard';
import { Section } from '@/ui/Section';
import { Container } from '@/ui/Container';
import { SearchParamParser } from '@/lib/routing/search-params/SearchParamParser';
import { SearchParamBuilder } from '@/lib/routing/search-params/SearchParamBuilder';
type StepName = 'basics' | 'visibility' | 'structure' | 'schedule' | 'scoring' | 'stewarding' | 'review';
function normalizeStepName(raw: string | null): StepName {
switch (raw) {
case 'basics':
case 'visibility':
case 'structure':
case 'schedule':
case 'scoring':
case 'stewarding':
case 'review':
return raw;
default:
return 'basics';
}
}
export default function CreateLeaguePage() {
const router = useRouter();
const searchParams = useSearchParams();
const currentStepName = normalizeStepName(
searchParams && typeof searchParams.get === 'function'
? searchParams.get('step')
: null,
);
const wizardParams = SearchParamParser.parseWizard(searchParams as any).unwrap();
const rawStep = wizardParams.step;
let currentStepName: StepName = 'basics';
if (rawStep === 'basics' ||
rawStep === 'visibility' ||
rawStep === 'structure' ||
rawStep === 'schedule' ||
rawStep === 'scoring' ||
rawStep === 'stewarding' ||
rawStep === 'review') {
currentStepName = rawStep;
}
const handleStepChange = (stepName: StepName) => {
const params = new URLSearchParams(
searchParams && typeof searchParams.toString === 'function'
? searchParams.toString()
: '',
);
params.set('step', stepName);
const query = params.toString();
const href = query ? `/leagues/create?${query}` : '/leagues/create';
router.push(href);
const builder = new SearchParamBuilder();
// Copy existing params if needed, but here we just want to set the step
if (searchParams) {
searchParams.forEach((value, key) => {
if (key !== 'step') builder.set(key, value);
});
}
builder.step(stepName);
router.push(`/leagues/create${builder.build()}`);
};
return (