Files
gridpilot.gg/apps/website/templates/CreateLeagueWizardTemplate.tsx
2026-01-19 18:01:30 +01:00

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>
);
}