399 lines
14 KiB
TypeScript
399 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import { FormEvent } from 'react';
|
|
import { LeagueReviewSummary } from '@/components/leagues/LeagueReviewSummary';
|
|
import { Card } from '@/ui/Card';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { Input } from '@/ui/Input';
|
|
import { Box } from '@/ui/Box';
|
|
import { Button } from '@/ui/Button';
|
|
import { Grid } from '@/ui/Grid';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Text } from '@/ui/Text';
|
|
import { Icon } from '@/ui/Icon';
|
|
import {
|
|
AlertCircle,
|
|
Check,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
FileText,
|
|
Loader2,
|
|
Sparkles,
|
|
} 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 (
|
|
<Box as="form" onSubmit={onSubmit} maxWidth="4xl" mx="auto" pb={8}>
|
|
{/* Header with icon */}
|
|
<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="primary-accent" opacity={0.2} border borderColor="primary-accent">
|
|
<Icon icon={Sparkles} size={5} color="primary-accent" />
|
|
</Box>
|
|
<Box>
|
|
<Heading level={1}>
|
|
Create a new league
|
|
</Heading>
|
|
<Text size="sm" color="text-gray-500" block>
|
|
We'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.
|
|
</Text>
|
|
</Box>
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Desktop Progress Bar */}
|
|
<Box display={{ base: 'none', md: 'block' }} mb={8}>
|
|
<Box position="relative">
|
|
{/* Background track */}
|
|
<Box position="absolute" top="5" left="6" right="6" h="0.5" bg="rgba(255,255,255,0.1)" rounded="full" />
|
|
{/* Progress fill */}
|
|
<Box
|
|
position="absolute"
|
|
top="5"
|
|
left="6"
|
|
h="0.5"
|
|
bg="primary-accent"
|
|
rounded="full"
|
|
transition
|
|
width={`${((step - 1) / (steps.length - 1)) * 100}%`}
|
|
/>
|
|
|
|
<Box 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 (
|
|
<Box
|
|
as="button"
|
|
key={wizardStep.id}
|
|
type="button"
|
|
onClick={() => onGoToStep(wizardStep.id)}
|
|
disabled={!isAccessible}
|
|
display="flex"
|
|
flexDirection="col"
|
|
alignItems="center"
|
|
bg="transparent"
|
|
cursor={isAccessible ? 'pointer' : 'not-allowed'}
|
|
opacity={!isAccessible ? 0.6 : 1}
|
|
>
|
|
<Box
|
|
position="relative"
|
|
zIndex={10}
|
|
display="flex"
|
|
h="10"
|
|
w="10"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
rounded="full"
|
|
transition
|
|
bg={isCurrent || isCompleted ? 'primary-accent' : 'rgba(255,255,255,0.1)'}
|
|
color={isCurrent || isCompleted ? 'white' : 'text-gray-400'}
|
|
>
|
|
{isCompleted ? (
|
|
<Icon icon={Check} size={4} />
|
|
) : (
|
|
<Icon icon={StepIcon} size={4} />
|
|
)}
|
|
</Box>
|
|
<Box mt={2} textAlign="center">
|
|
<Text
|
|
size="xs"
|
|
weight="medium"
|
|
transition
|
|
color={isCurrent ? 'white' : isCompleted ? 'primary-accent' : isAccessible ? 'text-gray-400' : 'text-gray-500'}
|
|
>
|
|
{wizardStep.label}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
})}
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Main Card */}
|
|
<Card position="relative" overflow="hidden">
|
|
{/* Step header */}
|
|
<Box display="flex" alignItems="start" gap={4} mb={6}>
|
|
<Box display="flex" h="12" w="12" alignItems="center" justifyContent="center" rounded="xl" bg="rgba(25,140,255,0.1)" flexShrink={0} transition>
|
|
<Icon icon={CurrentStepIcon} size={6} color="primary-accent" />
|
|
</Box>
|
|
<Box flexGrow={1} minWidth="0">
|
|
<Heading level={2} color="white">
|
|
<Stack direction="row" align="center" gap={2} flexWrap="wrap">
|
|
<Text>{getStepTitle(step)}</Text>
|
|
<Box px={2} py={0.5} rounded="full" border borderColor="rgba(255,255,255,0.1)" bg="rgba(255,255,255,0.05)">
|
|
<Text size="xs" weight="medium" color="text-gray-300">
|
|
{getStepContextLabel(step)}
|
|
</Text>
|
|
</Box>
|
|
</Stack>
|
|
</Heading>
|
|
<Text size="sm" color="text-gray-400" block mt={1}>
|
|
{getStepSubtitle(step)}
|
|
</Text>
|
|
</Box>
|
|
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={1.5} px={3} py={1.5} rounded="full" bg="rgba(0,0,0,0.2)" border borderColor="rgba(255,255,255,0.1)">
|
|
<Text size="xs" color="text-gray-500">Step</Text>
|
|
<Text size="sm" weight="semibold" color="white">{step}</Text>
|
|
<Text size="xs" color="text-gray-500">/ {steps.length}</Text>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Step content */}
|
|
<Box minHeight="320px">
|
|
{step === 1 && (
|
|
<Stack gap={8}>
|
|
<LeagueBasicsSection
|
|
form={form}
|
|
onChange={onFormChange}
|
|
errors={errors.basics ?? {}}
|
|
/>
|
|
<Box rounded="xl" border borderColor="rgba(255,255,255,0.1)" bg="rgba(255,255,255,0.05)" p={4}>
|
|
<Box mb={2}>
|
|
<Text size="xs" weight="semibold" color="text-gray-300" uppercase letterSpacing="wide">
|
|
First season
|
|
</Text>
|
|
<Text size="xs" color="text-gray-500" block>
|
|
Name the first season that will run in this league.
|
|
</Text>
|
|
</Box>
|
|
<Stack gap={2}>
|
|
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
|
|
Season name
|
|
</Text>
|
|
<Input
|
|
value={form.seasonName ?? ''}
|
|
onChange={(e) =>
|
|
onFormChange({
|
|
...form,
|
|
seasonName: e.target.value,
|
|
})
|
|
}
|
|
placeholder="e.g., Season 1 (2025)"
|
|
/>
|
|
<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.
|
|
</Text>
|
|
</Stack>
|
|
</Box>
|
|
</Stack>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<LeagueVisibilitySection
|
|
form={form}
|
|
onChange={onFormChange}
|
|
errors={
|
|
errors.basics?.visibility
|
|
? { visibility: errors.basics.visibility }
|
|
: {}
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<Stack gap={4}>
|
|
<Box>
|
|
<Text size="xs" color="text-gray-500" block>
|
|
Applies to: First season of this league.
|
|
</Text>
|
|
</Box>
|
|
<LeagueStructureSection
|
|
form={form}
|
|
onChange={onFormChange}
|
|
readOnly={false}
|
|
/>
|
|
</Stack>
|
|
)}
|
|
|
|
{step === 4 && (
|
|
<Stack gap={4}>
|
|
<Box>
|
|
<Text size="xs" color="text-gray-500" block>
|
|
Applies to: First season of this league.
|
|
</Text>
|
|
</Box>
|
|
<LeagueTimingsSection
|
|
form={form}
|
|
onChange={onFormChange}
|
|
errors={errors.timings ?? {}}
|
|
/>
|
|
</Stack>
|
|
)}
|
|
|
|
{step === 5 && (
|
|
<Stack gap={8}>
|
|
<Box>
|
|
<Text size="xs" color="text-gray-500" block>
|
|
Applies to: First season of this league.
|
|
</Text>
|
|
</Box>
|
|
<ScoringPatternSection
|
|
scoring={form.scoring || {}}
|
|
presets={presets}
|
|
readOnly={presetsLoading}
|
|
patternError={errors.scoring?.patternId ?? ''}
|
|
onChangePatternId={onScoringPresetChange}
|
|
onToggleCustomScoring={onToggleCustomScoring}
|
|
/>
|
|
|
|
<Grid responsiveGridCols={{ base: 1, lg: 2 }} gap={6}>
|
|
<ChampionshipsSection form={form} onChange={onFormChange} readOnly={presetsLoading} />
|
|
<LeagueDropSection form={form} onChange={onFormChange} readOnly={false} />
|
|
</Grid>
|
|
|
|
{errors.submit && (
|
|
<Box display="flex" alignItems="start" gap={3} rounded="lg" bg="rgba(245,158,11,0.1)" p={4} border borderColor="rgba(245,158,11,0.2)">
|
|
<Icon icon={AlertCircle} size={5} color="warning-amber" />
|
|
<Text size="sm" color="warning-amber">{errors.submit}</Text>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
)}
|
|
|
|
{step === 6 && (
|
|
<Stack gap={4}>
|
|
<Box>
|
|
<Text size="xs" color="text-gray-500" block>
|
|
Applies to: First season of this league.
|
|
</Text>
|
|
</Box>
|
|
<LeagueStewardingSection
|
|
form={form}
|
|
onChange={onFormChange}
|
|
readOnly={false}
|
|
/>
|
|
</Stack>
|
|
)}
|
|
|
|
{step === 7 && (
|
|
<Stack gap={6}>
|
|
<LeagueReviewSummary form={form} presets={presets} />
|
|
{errors.submit && (
|
|
<Box display="flex" alignItems="start" gap={3} rounded="lg" bg="rgba(245,158,11,0.1)" p={4} border borderColor="rgba(245,158,11,0.2)">
|
|
<Icon icon={AlertCircle} size={5} color="warning-amber" />
|
|
<Text size="sm" color="warning-amber">{errors.submit}</Text>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
)}
|
|
</Box>
|
|
</Card>
|
|
|
|
{/* Navigation */}
|
|
<Box display="flex" alignItems="center" justifyContent="between" mt={6}>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
disabled={step === 1 || loading}
|
|
onClick={onPreviousStep}
|
|
>
|
|
<Stack direction="row" align="center" gap={2}>
|
|
<Icon icon={ChevronLeft} size={4} />
|
|
<Text display={{ base: 'none', md: 'inline-block' }}>Back</Text>
|
|
</Stack>
|
|
</Button>
|
|
|
|
<Box display="flex" alignItems="center" gap={3}>
|
|
{step < 7 ? (
|
|
<Button
|
|
type="button"
|
|
variant="primary"
|
|
disabled={loading}
|
|
onClick={onNextStep}
|
|
>
|
|
<Stack direction="row" align="center" gap={2}>
|
|
<Text>Continue</Text>
|
|
<Icon icon={ChevronRight} size={4} />
|
|
</Stack>
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
disabled={loading}
|
|
>
|
|
<Stack direction="row" align="center" gap={2}>
|
|
{loading ? <Icon icon={Loader2} size={4} animate="spin" /> : <Icon icon={Sparkles} size={4} />}
|
|
<Text>{loading ? 'Creating…' : 'Create League'}</Text>
|
|
</Stack>
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Helper text */}
|
|
<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.
|
|
</Text>
|
|
</Box>
|
|
);
|
|
}
|