website refactor
This commit is contained in:
485
apps/website/templates/CreateLeagueWizardTemplate.tsx
Normal file
485
apps/website/templates/CreateLeagueWizardTemplate.tsx
Normal file
@@ -0,0 +1,485 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user