Files
gridpilot.gg/apps/website/templates/CreateLeagueWizardTemplate.tsx
2026-01-19 02:14:53 +01:00

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