website refactor

This commit is contained in:
2026-01-19 02:14:53 +01:00
parent 489c5f7858
commit a8731e6937
70 changed files with 2908 additions and 2423 deletions

View File

@@ -1,51 +1,26 @@
'use client';
import { useAuth } from '@/components/auth/AuthContext';
import { LeagueReviewSummary } from '@/components/leagues/LeagueReviewSummary';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import {
AlertCircle,
Award,
Calendar,
Check,
CheckCircle2,
ChevronLeft,
ChevronRight,
FileText,
Loader2,
Scale,
Sparkles,
Trophy,
Users,
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import { FormEvent, useCallback, useEffect, useState } from 'react';
import { LeagueWizardCommandModel } from '@/lib/command-models/leagues/LeagueWizardCommandModel';
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 { useLeagueScoringPresets } from "@/hooks/useLeagueScoringPresets";
import { useCreateLeagueWizard } from "@/hooks/useLeagueWizardService";
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { Weekday } from '@/lib/types/Weekday';
import type { WizardErrors } from '@/lib/types/WizardErrors';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
import { CreateLeagueWizardTemplate, Step } from '@/templates/CreateLeagueWizardTemplate';
import {
Award,
Calendar,
CheckCircle2,
FileText,
Scale,
Trophy,
Users
} from 'lucide-react';
// ============================================================================
// LOCAL STORAGE PERSISTENCE
@@ -54,16 +29,14 @@ import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScori
const STORAGE_KEY = 'gridpilot_league_wizard_draft';
const STORAGE_HIGHEST_STEP_KEY = 'gridpilot_league_wizard_highest_step';
// TODO there is a better place for this
function saveFormToStorage(form: LeagueWizardFormModel): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(form));
} catch {
// Ignore storage errors (quota exceeded, etc.)
// Ignore storage errors
}
}
// TODO there is a better place for this
function loadFormFromStorage(): LeagueWizardFormModel | null {
try {
const stored = localStorage.getItem(STORAGE_KEY);
@@ -110,8 +83,6 @@ function getHighestStep(): number {
}
}
type Step = 1 | 2 | 3 | 4 | 5 | 6 | 7;
type StepName = 'basics' | 'visibility' | 'structure' | 'schedule' | 'scoring' | 'stewarding' | 'review';
type LeagueWizardFormModel = LeagueConfigFormModel & {
@@ -125,44 +96,29 @@ interface CreateLeagueWizardProps {
function stepNameToStep(stepName: StepName): Step {
switch (stepName) {
case 'basics':
return 1;
case 'visibility':
return 2;
case 'structure':
return 3;
case 'schedule':
return 4;
case 'scoring':
return 5;
case 'stewarding':
return 6;
case 'review':
return 7;
case 'basics': return 1;
case 'visibility': return 2;
case 'structure': return 3;
case 'schedule': return 4;
case 'scoring': return 5;
case 'stewarding': return 6;
case 'review': return 7;
}
}
function stepToStepName(step: Step): StepName {
switch (step) {
case 1:
return 'basics';
case 2:
return 'visibility';
case 3:
return 'structure';
case 4:
return 'schedule';
case 5:
return 'scoring';
case 6:
return 'stewarding';
case 7:
return 'review';
case 1: return 'basics';
case 2: return 'visibility';
case 3: return 'structure';
case 4: return 'schedule';
case 5: return 'scoring';
case 6: return 'stewarding';
case 7: return 'review';
}
}
function getDefaultSeasonStartDate(): string {
// Default to next Saturday
const now = new Date();
const daysUntilSaturday = (6 - now.getDay() + 7) % 7 || 7;
const nextSaturday = new Date(now);
@@ -220,7 +176,6 @@ function createDefaultForm(): LeagueWizardFormModel {
mainRaceMinutes: 40,
sessionCount: 2,
roundsPlanned: 8,
// Default to Saturday races, weekly, starting next week
weekdays: ['Sat'] as Weekday[],
recurrenceStrategy: 'weekly' as const,
timezoneId: 'UTC',
@@ -253,12 +208,10 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
const [highestCompletedStep, setHighestCompletedStep] = useState(1);
const [isHydrated, setIsHydrated] = useState(false);
// Initialize form from localStorage or defaults
const [form, setForm] = useState<LeagueWizardFormModel>(() =>
createDefaultForm(),
);
// Hydrate from localStorage on mount
useEffect(() => {
const stored = loadFormFromStorage();
if (stored) {
@@ -268,14 +221,12 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
setIsHydrated(true);
}, []);
// Save form to localStorage whenever it changes (after hydration)
useEffect(() => {
if (isHydrated) {
saveFormToStorage(form);
}
}, [form, isHydrated]);
// Track highest step reached
useEffect(() => {
if (isHydrated) {
saveHighestStep(step);
@@ -283,13 +234,10 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
}
}, [step, isHydrated]);
// Use the react-query hook for scoring presets
const { data: queryPresets, error: presetsError } = useLeagueScoringPresets();
// Sync presets from query to local state
useEffect(() => {
if (queryPresets) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setPresets(queryPresets as any);
const firstPreset = queryPresets[0];
if (firstPreset && !form.scoring?.patternId) {
@@ -306,7 +254,6 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
}
}, [queryPresets, form.scoring?.patternId]);
// Handle presets error
useEffect(() => {
if (presetsError) {
setErrors((prev) => ({
@@ -316,12 +263,9 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
}
}, [presetsError]);
// Use the create league mutation
const createLeagueMutation = useCreateLeagueWizard();
const validateStep = (currentStep: Step): boolean => {
// Convert form to LeagueWizardFormData for validation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const formData: any = {
leagueId: form.leagueId || '',
basics: {
@@ -371,7 +315,6 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stepErrors = LeagueWizardCommandModel.validateLeagueWizardStep(formData, currentStep as any);
setErrors((prev) => ({
...prev,
@@ -395,7 +338,6 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
onStepChange(stepToStepName(prevStep));
};
// Navigate to a specific step (only if it's been reached before)
const goToStep = useCallback((targetStep: Step) => {
if (targetStep <= highestCompletedStep) {
onStepChange(stepToStepName(targetStep));
@@ -415,8 +357,6 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
return;
}
// Convert form to LeagueWizardFormData for validation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const formData: any = {
leagueId: form.leagueId || '',
basics: {
@@ -484,17 +424,11 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
});
try {
// Use the mutation to create the league
const result = await createLeagueMutation.mutateAsync({ form, ownerId });
// Clear the draft on successful creation
clearFormStorage();
// Navigate to the new league
router.push(`/leagues/${result.leagueId}`);
} catch (err) {
const message =
err instanceof Error ? err.message : 'Failed to create league';
const message = err instanceof Error ? err.message : 'Failed to create league';
setErrors((prev) => ({
...prev,
submit: message,
@@ -503,7 +437,6 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
}
};
// Handler for scoring preset selection (timings default from API)
const handleScoringPresetChange = (patternId: string) => {
setForm((prev) => {
const selectedPreset = presets.find((p) => p.id === patternId);
@@ -541,460 +474,65 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
const getStepTitle = (currentStep: Step): string => {
switch (currentStep) {
case 1:
return 'Name your league';
case 2:
return 'Choose your destiny';
case 3:
return 'Choose the structure';
case 4:
return 'Set the schedule';
case 5:
return 'Scoring & championships';
case 6:
return 'Stewarding & protests';
case 7:
return 'Review & create';
default:
return '';
case 1: return 'Name your league';
case 2: return 'Choose your destiny';
case 3: return 'Choose the structure';
case 4: return 'Set the schedule';
case 5: return 'Scoring & championships';
case 6: return 'Stewarding & protests';
case 7: return 'Review & create';
default: return '';
}
};
const getStepSubtitle = (currentStep: Step): string => {
switch (currentStep) {
case 1:
return 'Give your league a memorable name and tell your story.';
case 2:
return 'Will you compete for global rankings or race with friends?';
case 3:
return 'Define how races in this season will run.';
case 4:
return 'Plan when this seasons races happen.';
case 5:
return 'Choose how points and drop scores work for this season.';
case 6:
return 'Set how protests and stewarding work for this season.';
case 7:
return 'Review your league and first season before launching.';
default:
return '';
case 1: return 'Give your league a memorable name and tell your story.';
case 2: return 'Will you compete for global rankings or race with friends?';
case 3: return 'Define how races in this season will run.';
case 4: return 'Plan when this seasons races happen.';
case 5: return 'Choose how points and drop scores work for this season.';
case 6: return 'Set how protests and stewarding work for this season.';
case 7: return 'Review your league and first season before launching.';
default: return '';
}
};
const getStepContextLabel = (currentStep: Step): string => {
if (currentStep === 1 || currentStep === 2) {
return 'League setup';
}
if (currentStep >= 3 && currentStep <= 6) {
return 'Season setup';
}
if (currentStep === 1 || currentStep === 2) return 'League setup';
if (currentStep >= 3 && currentStep <= 6) return 'Season setup';
return 'League & Season summary';
};
const currentStepData = steps.find((s) => s.id === step);
const CurrentStepIcon = currentStepData?.icon ?? FileText;
return (
<Box as="form" onSubmit={handleSubmit} 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="bg-primary-blue/20" border borderColor="border-primary-blue/20">
<Icon icon={Sparkles} size={5} color="text-primary-blue" />
</Box>
<Box>
<Heading level={1} fontSize={{ base: '2xl', sm: '3xl' }}>
Create a new league
</Heading>
<Text size="sm" color="text-gray-500" block>
We&apos;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="bg-charcoal-outline" rounded="full" />
{/* Progress fill */}
<Box
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)`}
/>
<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={() => goToStep(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}
>
<Box
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 ? (
<Icon icon={Check} size={4} strokeWidth={3} />
) : (
<Icon icon={StepIcon} size={4} />
)}
</Box>
<Box mt={2} textAlign="center">
<Text
size="xs"
weight="medium"
transition
color={isCurrent ? 'text-white' : isCompleted ? 'text-primary-blue' : isAccessible ? 'text-gray-400' : 'text-gray-500'}
>
{wizardStep.label}
</Text>
</Box>
</Box>
);
})}
</Box>
</Box>
</Box>
{/* Mobile Progress */}
<Box display={{ base: 'block', md: 'none' }} mb={6}>
<Box display="flex" alignItems="center" justifyContent="between" mb={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={CurrentStepIcon} size={4} color="text-primary-blue" />
<Text size="sm" weight="medium" color="text-white">{currentStepData?.label}</Text>
</Stack>
<Text size="xs" color="text-gray-500">
{step}/{steps.length}
</Text>
</Box>
<Box h="1.5" bg="bg-charcoal-outline" rounded="full" overflow="hidden">
<Box
h="full"
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
rounded="full"
transition
height="full"
width={`${(step / steps.length) * 100}%`}
/>
</Box>
{/* Step dots */}
<Box display="flex" justifyContent="between" mt={2} px={0.5}>
{steps.map((s) => (
<Box
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'}
/>
))}
</Box>
</Box>
{/* Main Card */}
<Card position="relative" overflow="hidden">
{/* Top gradient accent */}
<Box position="absolute" top="0" left="0" right="0" h="1" bg="bg-gradient-to-r from-transparent via-primary-blue to-transparent" />
{/* 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="bg-primary-blue/10" flexShrink={0} transition>
<Icon icon={CurrentStepIcon} size={6} color="text-primary-blue" />
</Box>
<Box flexGrow={1} minWidth="0">
<Heading level={2} fontSize={{ base: 'xl', md: '2xl' }} color="text-white">
<Stack direction="row" align="center" gap={2} flexWrap="wrap">
<Text>{getStepTitle(step)}</Text>
<Text 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)}
</Text>
</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="bg-deep-graphite" border borderColor="border-charcoal-outline">
<Text size="xs" color="text-gray-500">Step</Text>
<Text size="sm" weight="semibold" color="text-white">{step}</Text>
<Text size="xs" color="text-gray-500">/ {steps.length}</Text>
</Box>
</Box>
{/* Divider */}
<Box h="px" bg="bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" mb={6} />
{/* Step content with min-height for consistency */}
<Box minHeight="320px">
{step === 1 && (
<Box animate="fade-in" gap={8} display="flex" flexDirection="col">
<LeagueBasicsSection
form={form}
onChange={setForm}
errors={errors.basics ?? {}}
/>
<Box rounded="xl" border borderColor="border-charcoal-outline" bg="bg-iron-gray/40" p={4}>
<Box display="flex" alignItems="center" justifyContent="between" gap={2} mb={2}>
<Box>
<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>
</Box>
<Box mt={2} display="flex" flexDirection="col" gap={2}>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
Season name
</Text>
<Input
value={form.seasonName ?? ''}
onChange={(e) =>
setForm((prev) => ({
...prev,
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>
</Box>
</Box>
</Box>
)}
{step === 2 && (
<Box animate="fade-in">
<LeagueVisibilitySection
form={form}
onChange={setForm}
errors={
errors.basics?.visibility
? { visibility: errors.basics.visibility }
: {}
}
/>
</Box>
)}
{step === 3 && (
<Box animate="fade-in" display="flex" flexDirection="col" gap={4}>
<Box mb={2}>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</Text>
<Text size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</Text>
</Box>
<LeagueStructureSection
form={form}
onChange={setForm}
readOnly={false}
/>
</Box>
)}
{step === 4 && (
<Box animate="fade-in" display="flex" flexDirection="col" gap={4}>
<Box mb={2}>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</Text>
<Text size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</Text>
</Box>
<LeagueTimingsSection
form={form}
onChange={setForm}
errors={errors.timings ?? {}}
/>
</Box>
)}
{step === 5 && (
<Box animate="fade-in" display="flex" flexDirection="col" gap={8}>
<Box mb={2}>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</Text>
<Text size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</Text>
</Box>
{/* Scoring Pattern Selection */}
<ScoringPatternSection
scoring={form.scoring || {}}
presets={presets}
readOnly={presetsLoading}
patternError={errors.scoring?.patternId ?? ''}
onChangePatternId={handleScoringPresetChange}
onToggleCustomScoring={() =>
setForm((prev) => ({
...prev,
scoring: {
...prev.scoring,
customScoringEnabled: !(prev.scoring?.customScoringEnabled),
},
}))
}
/>
{/* Divider */}
<Box h="px" bg="bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" />
{/* Championships & Drop Rules side by side on larger screens */}
<Box display="grid" gridCols={{ base: 1, lg: 2 }} gap={6}>
<ChampionshipsSection form={form} onChange={setForm} readOnly={presetsLoading} />
<LeagueDropSection form={form} onChange={setForm} readOnly={false} />
</Box>
{errors.submit && (
<Box display="flex" alignItems="start" gap={3} rounded="lg" bg="bg-warning-amber/10" p={4} border borderColor="border-warning-amber/20">
<Icon icon={AlertCircle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
<Text size="sm" color="text-warning-amber">{errors.submit}</Text>
</Box>
)}
</Box>
)}
{step === 6 && (
<Box animate="fade-in" display="flex" flexDirection="col" gap={4}>
<Box mb={2}>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</Text>
<Text size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</Text>
</Box>
<LeagueStewardingSection
form={form}
onChange={setForm}
readOnly={false}
/>
</Box>
)}
{step === 7 && (
<Box animate="fade-in" display="flex" flexDirection="col" gap={6}>
<LeagueReviewSummary form={form} presets={presets} />
{errors.submit && (
<Box display="flex" alignItems="start" gap={3} rounded="lg" bg="bg-warning-amber/10" p={4} border borderColor="border-warning-amber/20">
<Icon icon={AlertCircle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
<Text size="sm" color="text-warning-amber">{errors.submit}</Text>
</Box>
)}
</Box>
)}
</Box>
</Card>
{/* Navigation */}
<Box display="flex" alignItems="center" justifyContent="between" mt={6}>
<Button
type="button"
variant="secondary"
disabled={step === 1 || loading}
onClick={goToPreviousStep}
icon={<Icon icon={ChevronLeft} size={4} />}
>
<Text display={{ base: 'none', md: 'inline-block' }}>Back</Text>
</Button>
<Box display="flex" alignItems="center" gap={3}>
{/* Mobile step dots */}
<Box display={{ base: 'flex', sm: 'none' }} alignItems="center" gap={1}>
{steps.map((s) => (
<Box
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'}
/>
))}
</Box>
{step < 7 ? (
<Button
type="button"
variant="primary"
disabled={loading}
onClick={goToNextStep}
icon={<Icon icon={ChevronRight} size={4} />}
flexDirection="row-reverse"
>
<Text>Continue</Text>
</Button>
) : (
<Button
type="submit"
variant="primary"
disabled={loading}
minWidth="150px"
justifyContent="center"
icon={loading ? <Icon icon={Loader2} size={4} animate="spin" /> : <Icon icon={Sparkles} size={4} />}
>
{loading ? (
<Text>Creating</Text>
) : (
<Text>Create League</Text>
)}
</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>
<CreateLeagueWizardTemplate
viewData={{}}
step={step}
steps={steps}
form={form}
errors={errors}
loading={loading}
presetsLoading={presetsLoading}
presets={presets}
highestCompletedStep={highestCompletedStep}
onGoToStep={goToStep}
onFormChange={setForm}
onSubmit={handleSubmit}
onNextStep={goToNextStep}
onPreviousStep={goToPreviousStep}
onScoringPresetChange={handleScoringPresetChange}
onToggleCustomScoring={() =>
setForm((prev) => ({
...prev,
scoring: {
...prev.scoring,
customScoringEnabled: !(prev.scoring?.customScoringEnabled),
},
}))
}
getStepTitle={getStepTitle}
getStepSubtitle={getStepSubtitle}
getStepContextLabel={getStepContextLabel}
/>
);
}