486 lines
19 KiB
TypeScript
486 lines
19 KiB
TypeScript
'use client';
|
|
|
|
import { FormEvent, ReactNode } from 'react';
|
|
import { LeagueReviewSummary } from '@/components/leagues/LeagueReviewSummary';
|
|
import {
|
|
SharedBox,
|
|
SharedButton,
|
|
SharedStack,
|
|
SharedText,
|
|
SharedIcon,
|
|
SharedContainer
|
|
} from '@/components/shared/UIComponents';
|
|
import { Card } from '@/ui/Card';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { Input } from '@/ui/Input';
|
|
import {
|
|
AlertCircle,
|
|
Award,
|
|
Calendar,
|
|
Check,
|
|
CheckCircle2,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
FileText,
|
|
Loader2,
|
|
Scale,
|
|
Sparkles,
|
|
Trophy,
|
|
Users,
|
|
} from 'lucide-react';
|
|
import { LeagueBasicsSection } from '@/components/leagues/LeagueBasicsSection';
|
|
import { LeagueDropSection } from '@/components/leagues/LeagueDropSection';
|
|
import {
|
|
ChampionshipsSection,
|
|
ScoringPatternSection
|
|
} 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 { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
|
|
import type { WizardErrors } from '@/lib/types/WizardErrors';
|
|
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
|
import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
|
|
|
export type Step = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
|
|
interface CreateLeagueWizardTemplateProps extends TemplateProps<ViewData> {
|
|
step: Step;
|
|
steps: any[];
|
|
form: any;
|
|
errors: WizardErrors;
|
|
loading: boolean;
|
|
presetsLoading: boolean;
|
|
presets: LeagueScoringPresetViewModel[];
|
|
highestCompletedStep: number;
|
|
onGoToStep: (step: Step) => void;
|
|
onFormChange: (form: any) => void;
|
|
onSubmit: (e: FormEvent) => void;
|
|
onNextStep: () => void;
|
|
onPreviousStep: () => void;
|
|
onScoringPresetChange: (id: string) => void;
|
|
onToggleCustomScoring: () => void;
|
|
getStepTitle: (step: Step) => string;
|
|
getStepSubtitle: (step: Step) => string;
|
|
getStepContextLabel: (step: Step) => string;
|
|
}
|
|
|
|
export function CreateLeagueWizardTemplate({
|
|
step,
|
|
steps,
|
|
form,
|
|
errors,
|
|
loading,
|
|
presetsLoading,
|
|
presets,
|
|
highestCompletedStep,
|
|
onGoToStep,
|
|
onFormChange,
|
|
onSubmit,
|
|
onNextStep,
|
|
onPreviousStep,
|
|
onScoringPresetChange,
|
|
onToggleCustomScoring,
|
|
getStepTitle,
|
|
getStepSubtitle,
|
|
getStepContextLabel,
|
|
}: CreateLeagueWizardTemplateProps) {
|
|
const currentStepData = steps.find((s) => s.id === step);
|
|
const CurrentStepIcon = currentStepData?.icon ?? FileText;
|
|
|
|
return (
|
|
<SharedBox as="form" onSubmit={onSubmit} maxWidth="4xl" mx="auto" pb={8}>
|
|
{/* Header with icon */}
|
|
<SharedBox mb={8}>
|
|
<SharedStack direction="row" align="center" gap={3} mb={3}>
|
|
<SharedBox display="flex" h="11" w="11" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/20" border borderColor="border-primary-blue/20">
|
|
<SharedIcon icon={Sparkles} size={5} color="text-primary-blue" />
|
|
</SharedBox>
|
|
<SharedBox>
|
|
<Heading level={1} fontSize={{ base: '2xl', sm: '3xl' }}>
|
|
Create a new league
|
|
</Heading>
|
|
<SharedText size="sm" color="text-gray-500" block>
|
|
We'll also set up your first season in {steps.length} easy steps.
|
|
</SharedText>
|
|
<SharedText 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.
|
|
</SharedText>
|
|
</SharedBox>
|
|
</SharedStack>
|
|
</SharedBox>
|
|
|
|
{/* Desktop Progress Bar */}
|
|
<SharedBox display={{ base: 'none', md: 'block' }} mb={8}>
|
|
<SharedBox position="relative">
|
|
{/* Background track */}
|
|
<SharedBox position="absolute" top="5" left="6" right="6" h="0.5" bg="bg-charcoal-outline" rounded="full" />
|
|
{/* Progress fill */}
|
|
<SharedBox
|
|
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)`}
|
|
/>
|
|
|
|
<SharedBox position="relative" display="flex" justifyContent="between">
|
|
{steps.map((wizardStep) => {
|
|
const isCompleted = wizardStep.id < step;
|
|
const isCurrent = wizardStep.id === step;
|
|
const isAccessible = wizardStep.id <= highestCompletedStep;
|
|
const StepIcon = wizardStep.icon;
|
|
|
|
return (
|
|
<SharedBox
|
|
as="button"
|
|
key={wizardStep.id}
|
|
type="button"
|
|
onClick={() => onGoToStep(wizardStep.id)}
|
|
disabled={!isAccessible}
|
|
display="flex"
|
|
flexDirection="col"
|
|
alignItems="center"
|
|
bg="bg-transparent"
|
|
borderStyle="none"
|
|
cursor={isAccessible ? 'pointer' : 'not-allowed'}
|
|
opacity={!isAccessible ? 0.6 : 1}
|
|
>
|
|
<SharedBox
|
|
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 ? (
|
|
<SharedIcon icon={Check} size={4} strokeWidth={3} />
|
|
) : (
|
|
<SharedIcon icon={StepIcon} size={4} />
|
|
)}
|
|
</SharedBox>
|
|
<SharedBox mt={2} textAlign="center">
|
|
<SharedText
|
|
size="xs"
|
|
weight="medium"
|
|
transition
|
|
color={isCurrent ? 'text-white' : isCompleted ? 'text-primary-blue' : isAccessible ? 'text-gray-400' : 'text-gray-500'}
|
|
>
|
|
{wizardStep.label}
|
|
</SharedText>
|
|
</SharedBox>
|
|
</SharedBox>
|
|
);
|
|
})}
|
|
</SharedBox>
|
|
</SharedBox>
|
|
</SharedBox>
|
|
|
|
{/* Mobile Progress */}
|
|
<SharedBox display={{ base: 'block', md: 'none' }} mb={6}>
|
|
<SharedBox display="flex" alignItems="center" justifyContent="between" mb={2}>
|
|
<SharedStack direction="row" align="center" gap={2}>
|
|
<SharedIcon icon={CurrentStepIcon} size={4} color="text-primary-blue" />
|
|
<SharedText size="sm" weight="medium" color="text-white">{currentStepData?.label}</SharedText>
|
|
</SharedStack>
|
|
<SharedText size="xs" color="text-gray-500">
|
|
{step}/{steps.length}
|
|
</SharedText>
|
|
</SharedBox>
|
|
<SharedBox h="1.5" bg="bg-charcoal-outline" rounded="full" overflow="hidden">
|
|
<SharedBox
|
|
h="full"
|
|
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
|
|
rounded="full"
|
|
transition
|
|
height="full"
|
|
width={`${(step / steps.length) * 100}%`}
|
|
/>
|
|
</SharedBox>
|
|
{/* Step dots */}
|
|
<SharedBox display="flex" justifyContent="between" mt={2} px={0.5}>
|
|
{steps.map((s) => (
|
|
<SharedBox
|
|
key={s.id}
|
|
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'}
|
|
/>
|
|
))}
|
|
</SharedBox>
|
|
</SharedBox>
|
|
|
|
{/* Main Card */}
|
|
<Card position="relative" overflow="hidden">
|
|
{/* Top gradient accent */}
|
|
<SharedBox position="absolute" top="0" left="0" right="0" h="1" bg="bg-gradient-to-r from-transparent via-primary-blue to-transparent" />
|
|
|
|
{/* Step header */}
|
|
<SharedBox display="flex" alignItems="start" gap={4} mb={6}>
|
|
<SharedBox display="flex" h="12" w="12" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10" flexShrink={0} transition>
|
|
<SharedIcon icon={CurrentStepIcon} size={6} color="text-primary-blue" />
|
|
</SharedBox>
|
|
<SharedBox flexGrow={1} minWidth="0">
|
|
<Heading level={2} fontSize={{ base: 'xl', md: '2xl' }} color="text-white">
|
|
<SharedStack direction="row" align="center" gap={2} flexWrap="wrap">
|
|
<SharedText>{getStepTitle(step)}</SharedText>
|
|
<SharedText 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)}
|
|
</SharedText>
|
|
</SharedStack>
|
|
</Heading>
|
|
<SharedText size="sm" color="text-gray-400" block mt={1}>
|
|
{getStepSubtitle(step)}
|
|
</SharedText>
|
|
</SharedBox>
|
|
<SharedBox 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">
|
|
<SharedText size="xs" color="text-gray-500">Step</SharedText>
|
|
<SharedText size="sm" weight="semibold" color="text-white">{step}</SharedText>
|
|
<SharedText size="xs" color="text-gray-500">/ {steps.length}</SharedText>
|
|
</SharedBox>
|
|
</SharedBox>
|
|
|
|
{/* Divider */}
|
|
<SharedBox h="px" bg="bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" mb={6} />
|
|
|
|
{/* Step content with min-height for consistency */}
|
|
<SharedBox minHeight="320px">
|
|
{step === 1 && (
|
|
<SharedBox animate="fade-in" gap={8} display="flex" flexDirection="col">
|
|
<LeagueBasicsSection
|
|
form={form}
|
|
onChange={onFormChange}
|
|
errors={errors.basics ?? {}}
|
|
/>
|
|
<SharedBox rounded="xl" border borderColor="border-charcoal-outline" bg="bg-iron-gray/40" p={4}>
|
|
<SharedBox display="flex" alignItems="center" justifyContent="between" gap={2} mb={2}>
|
|
<SharedBox>
|
|
<SharedText size="xs" weight="semibold" color="text-gray-300" uppercase letterSpacing="wide">
|
|
First season
|
|
</SharedText>
|
|
<SharedText size="xs" color="text-gray-500" block>
|
|
Name the first season that will run in this league.
|
|
</SharedText>
|
|
</SharedBox>
|
|
</SharedBox>
|
|
<SharedBox mt={2} display="flex" flexDirection="col" gap={2}>
|
|
<SharedText as="label" size="sm" weight="medium" color="text-gray-300" block>
|
|
Season name
|
|
</SharedText>
|
|
<Input
|
|
value={form.seasonName ?? ''}
|
|
onChange={(e) =>
|
|
onFormChange({
|
|
...form,
|
|
seasonName: e.target.value,
|
|
})
|
|
}
|
|
placeholder="e.g., Season 1 (2025)"
|
|
/>
|
|
<SharedText 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.
|
|
</SharedText>
|
|
</SharedBox>
|
|
</SharedBox>
|
|
</SharedBox>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<SharedBox animate="fade-in">
|
|
<LeagueVisibilitySection
|
|
form={form}
|
|
onChange={onFormChange}
|
|
errors={
|
|
errors.basics?.visibility
|
|
? { visibility: errors.basics.visibility }
|
|
: {}
|
|
}
|
|
/>
|
|
</SharedBox>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
|
|
<SharedBox mb={2}>
|
|
<SharedText size="xs" color="text-gray-500" block>
|
|
Applies to: First season of this league.
|
|
</SharedText>
|
|
<SharedText size="xs" color="text-gray-500" block>
|
|
These settings only affect this season. Future seasons can use different formats.
|
|
</SharedText>
|
|
</SharedBox>
|
|
<LeagueStructureSection
|
|
form={form}
|
|
onChange={onFormChange}
|
|
readOnly={false}
|
|
/>
|
|
</SharedBox>
|
|
)}
|
|
|
|
{step === 4 && (
|
|
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
|
|
<SharedBox mb={2}>
|
|
<SharedText size="xs" color="text-gray-500" block>
|
|
Applies to: First season of this league.
|
|
</SharedText>
|
|
<SharedText size="xs" color="text-gray-500" block>
|
|
These settings only affect this season. Future seasons can use different formats.
|
|
</SharedText>
|
|
</SharedBox>
|
|
<LeagueTimingsSection
|
|
form={form}
|
|
onChange={onFormChange}
|
|
errors={errors.timings ?? {}}
|
|
/>
|
|
</SharedBox>
|
|
)}
|
|
|
|
{step === 5 && (
|
|
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={8}>
|
|
<SharedBox mb={2}>
|
|
<SharedText size="xs" color="text-gray-500" block>
|
|
Applies to: First season of this league.
|
|
</SharedText>
|
|
<SharedText size="xs" color="text-gray-500" block>
|
|
These settings only affect this season. Future seasons can use different formats.
|
|
</SharedText>
|
|
</SharedBox>
|
|
{/* Scoring Pattern Selection */}
|
|
<ScoringPatternSection
|
|
scoring={form.scoring || {}}
|
|
presets={presets}
|
|
readOnly={presetsLoading}
|
|
patternError={errors.scoring?.patternId ?? ''}
|
|
onChangePatternId={onScoringPresetChange}
|
|
onToggleCustomScoring={onToggleCustomScoring}
|
|
/>
|
|
|
|
{/* Divider */}
|
|
<SharedBox h="px" bg="bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" />
|
|
|
|
{/* Championships & Drop Rules side by side on larger screens */}
|
|
<SharedBox display="grid" gridCols={{ base: 1, lg: 2 }} gap={6}>
|
|
<ChampionshipsSection form={form} onChange={onFormChange} readOnly={presetsLoading} />
|
|
<LeagueDropSection form={form} onChange={onFormChange} readOnly={false} />
|
|
</SharedBox>
|
|
|
|
{errors.submit && (
|
|
<SharedBox display="flex" alignItems="start" gap={3} rounded="lg" bg="bg-warning-amber/10" p={4} border borderColor="border-warning-amber/20">
|
|
<SharedIcon icon={AlertCircle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
|
|
<SharedText size="sm" color="text-warning-amber">{errors.submit}</SharedText>
|
|
</SharedBox>
|
|
)}
|
|
</SharedBox>
|
|
)}
|
|
|
|
{step === 6 && (
|
|
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
|
|
<SharedBox mb={2}>
|
|
<SharedText size="xs" color="text-gray-500" block>
|
|
Applies to: First season of this league.
|
|
</SharedText>
|
|
<SharedText size="xs" color="text-gray-500" block>
|
|
These settings only affect this season. Future seasons can use different formats.
|
|
</SharedText>
|
|
</SharedBox>
|
|
<LeagueStewardingSection
|
|
form={form}
|
|
onChange={onFormChange}
|
|
readOnly={false}
|
|
/>
|
|
</SharedBox>
|
|
)}
|
|
|
|
{step === 7 && (
|
|
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={6}>
|
|
<LeagueReviewSummary form={form} presets={presets} />
|
|
{errors.submit && (
|
|
<SharedBox display="flex" alignItems="start" gap={3} rounded="lg" bg="bg-warning-amber/10" p={4} border borderColor="border-warning-amber/20">
|
|
<SharedIcon icon={AlertCircle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
|
|
<SharedText size="sm" color="text-warning-amber">{errors.submit}</SharedText>
|
|
</SharedBox>
|
|
)}
|
|
</SharedBox>
|
|
)}
|
|
</SharedBox>
|
|
</Card>
|
|
|
|
{/* Navigation */}
|
|
<SharedBox display="flex" alignItems="center" justifyContent="between" mt={6}>
|
|
<SharedButton
|
|
type="button"
|
|
variant="secondary"
|
|
disabled={step === 1 || loading}
|
|
onClick={onPreviousStep}
|
|
icon={<SharedIcon icon={ChevronLeft} size={4} />}
|
|
>
|
|
<SharedText display={{ base: 'none', md: 'inline-block' }}>Back</SharedText>
|
|
</SharedButton>
|
|
|
|
<SharedBox display="flex" alignItems="center" gap={3}>
|
|
{/* Mobile step dots */}
|
|
<SharedBox display={{ base: 'flex', sm: 'none' }} alignItems="center" gap={1}>
|
|
{steps.map((s) => (
|
|
<SharedBox
|
|
key={s.id}
|
|
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'}
|
|
/>
|
|
))}
|
|
</SharedBox>
|
|
|
|
{step < 7 ? (
|
|
<SharedButton
|
|
type="button"
|
|
variant="primary"
|
|
disabled={loading}
|
|
onClick={onNextStep}
|
|
icon={<SharedIcon icon={ChevronRight} size={4} />}
|
|
>
|
|
<SharedText>Continue</SharedText>
|
|
</SharedButton>
|
|
) : (
|
|
<SharedButton
|
|
type="submit"
|
|
variant="primary"
|
|
disabled={loading}
|
|
style={{ minWidth: '150px' }}
|
|
icon={loading ? <SharedIcon icon={Loader2} size={4} animate="spin" /> : <SharedIcon icon={Sparkles} size={4} />}
|
|
>
|
|
{loading ? (
|
|
<SharedText>Creating…</SharedText>
|
|
) : (
|
|
<SharedText>Create League</SharedText>
|
|
)}
|
|
</SharedButton>
|
|
)}
|
|
</SharedBox>
|
|
</SharedBox>
|
|
|
|
{/* Helper text */}
|
|
<SharedText 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.
|
|
</SharedText>
|
|
</SharedBox>
|
|
);
|
|
}
|