website cleanup
This commit is contained in:
@@ -1,44 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState, FormEvent, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import LeagueReviewSummary from '@/components/leagues/LeagueReviewSummary';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import Input from '@/components/ui/Input';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import {
|
||||
FileText,
|
||||
Users,
|
||||
Calendar,
|
||||
Trophy,
|
||||
AlertCircle,
|
||||
Award,
|
||||
Calendar,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
Sparkles,
|
||||
Check,
|
||||
Scale,
|
||||
Sparkles,
|
||||
Trophy,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import LeagueReviewSummary from '@/components/leagues/LeagueReviewSummary';
|
||||
import Input from '@/components/ui/Input';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FormEvent, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { LeagueWizardCommandModel } from '@/lib/command-models/leagues/LeagueWizardCommandModel';
|
||||
import { LeagueWizardService } from '@/lib/services/leagues/LeagueWizardService';
|
||||
import type { LeagueScoringPresetDTO } from '@core/racing/application/ports/LeagueScoringPresetProvider';
|
||||
import type { LeagueConfigFormModel } from '@core/racing/application';
|
||||
import { useCreateLeagueWizard } from '@/hooks/useLeagueWizardService';
|
||||
import { useLeagueScoringPresets } from '@/hooks/useLeagueScoringPresets';
|
||||
import { LeagueBasicsSection } from './LeagueBasicsSection';
|
||||
import { LeagueVisibilitySection } from './LeagueVisibilitySection';
|
||||
import { LeagueStructureSection } from './LeagueStructureSection';
|
||||
import {
|
||||
LeagueScoringSection,
|
||||
ScoringPatternSection,
|
||||
ChampionshipsSection,
|
||||
} from './LeagueScoringSection';
|
||||
import { LeagueDropSection } from './LeagueDropSection';
|
||||
import { LeagueTimingsSection } from './LeagueTimingsSection';
|
||||
import {
|
||||
ChampionshipsSection,
|
||||
ScoringPatternSection
|
||||
} from './LeagueScoringSection';
|
||||
import { LeagueStewardingSection } from './LeagueStewardingSection';
|
||||
import { LeagueStructureSection } from './LeagueStructureSection';
|
||||
import { LeagueTimingsSection } from './LeagueTimingsSection';
|
||||
import { LeagueVisibilitySection } from './LeagueVisibilitySection';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';
|
||||
import type { Weekday } from '@/lib/types/Weekday';
|
||||
import type { WizardErrors } from '@/lib/types/WizardErrors';
|
||||
|
||||
// ============================================================================
|
||||
// LOCAL STORAGE PERSISTENCE
|
||||
@@ -47,6 +49,7 @@ import { LeagueStewardingSection } from './LeagueStewardingSection';
|
||||
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));
|
||||
@@ -55,6 +58,7 @@ function saveFormToStorage(form: LeagueWizardFormModel): void {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO there is a better place for this
|
||||
function loadFormFromStorage(): LeagueWizardFormModel | null {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
@@ -152,8 +156,6 @@ function stepToStepName(step: Step): StepName {
|
||||
}
|
||||
}
|
||||
|
||||
import { WizardErrors } from '@/lib/types/WizardErrors';
|
||||
|
||||
function getDefaultSeasonStartDate(): string {
|
||||
// Default to next Saturday
|
||||
const now = new Date();
|
||||
@@ -214,9 +216,8 @@ function createDefaultForm(): LeagueWizardFormModel {
|
||||
sessionCount: 2,
|
||||
roundsPlanned: 8,
|
||||
// Default to Saturday races, weekly, starting next week
|
||||
weekdays: ['Sat'] as import('@gridpilot/racing/domain/types/Weekday').Weekday[],
|
||||
weekdays: ['Sat'] as Weekday[],
|
||||
recurrenceStrategy: 'weekly' as const,
|
||||
raceStartTime: '20:00',
|
||||
timezoneId: 'UTC',
|
||||
seasonStartDate: defaultSeasonStartDate,
|
||||
},
|
||||
@@ -277,41 +278,93 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
|
||||
}
|
||||
}, [step, isHydrated]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadPresets() {
|
||||
try {
|
||||
const query = getListLeagueScoringPresetsQuery();
|
||||
const result = await query.execute();
|
||||
setPresets(result);
|
||||
const firstPreset = result[0];
|
||||
if (firstPreset) {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
scoring: {
|
||||
...prev.scoring,
|
||||
patternId: prev.scoring.patternId || firstPreset.id,
|
||||
customScoringEnabled: prev.scoring.customScoringEnabled ?? false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
submit:
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'Failed to load scoring presets',
|
||||
}));
|
||||
} finally {
|
||||
setPresetsLoading(false);
|
||||
}
|
||||
}
|
||||
// Use the react-query hook for scoring presets
|
||||
const { data: queryPresets, error: presetsError } = useLeagueScoringPresets();
|
||||
|
||||
loadPresets();
|
||||
}, []);
|
||||
// Sync presets from query to local state
|
||||
useEffect(() => {
|
||||
if (queryPresets) {
|
||||
setPresets(queryPresets);
|
||||
const firstPreset = queryPresets[0];
|
||||
if (firstPreset && !form.scoring?.patternId) {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
scoring: {
|
||||
...prev.scoring,
|
||||
patternId: firstPreset.id,
|
||||
customScoringEnabled: false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
setPresetsLoading(false);
|
||||
}
|
||||
}, [queryPresets, form.scoring?.patternId]);
|
||||
|
||||
// Handle presets error
|
||||
useEffect(() => {
|
||||
if (presetsError) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
submit: presetsError instanceof Error ? presetsError.message : 'Failed to load scoring presets',
|
||||
}));
|
||||
}
|
||||
}, [presetsError]);
|
||||
|
||||
// Use the create league mutation
|
||||
const createLeagueMutation = useCreateLeagueWizard();
|
||||
|
||||
const validateStep = (currentStep: Step): boolean => {
|
||||
const stepErrors = LeagueWizardCommandModel.validateLeagueWizardStep(form, currentStep);
|
||||
// Convert form to LeagueWizardFormData for validation
|
||||
const formData: any = {
|
||||
leagueId: form.leagueId || '',
|
||||
basics: {
|
||||
name: form.basics?.name || '',
|
||||
description: form.basics?.description || '',
|
||||
visibility: (form.basics?.visibility as 'public' | 'private' | 'unlisted') || 'public',
|
||||
gameId: form.basics?.gameId || 'iracing',
|
||||
},
|
||||
structure: {
|
||||
mode: (form.structure?.mode as 'solo' | 'fixedTeams') || 'solo',
|
||||
maxDrivers: form.structure?.maxDrivers || 0,
|
||||
maxTeams: form.structure?.maxTeams || 0,
|
||||
driversPerTeam: form.structure?.driversPerTeam || 0,
|
||||
},
|
||||
championships: {
|
||||
enableDriverChampionship: form.championships?.enableDriverChampionship ?? true,
|
||||
enableTeamChampionship: form.championships?.enableTeamChampionship ?? false,
|
||||
enableNationsChampionship: form.championships?.enableNationsChampionship ?? false,
|
||||
enableTrophyChampionship: form.championships?.enableTrophyChampionship ?? false,
|
||||
},
|
||||
scoring: {
|
||||
patternId: form.scoring?.patternId || '',
|
||||
customScoringEnabled: form.scoring?.customScoringEnabled ?? false,
|
||||
},
|
||||
dropPolicy: {
|
||||
strategy: (form.dropPolicy?.strategy as 'none' | 'bestNResults' | 'dropWorstN') || 'bestNResults',
|
||||
n: form.dropPolicy?.n || 6,
|
||||
},
|
||||
timings: {
|
||||
practiceMinutes: form.timings?.practiceMinutes || 0,
|
||||
qualifyingMinutes: form.timings?.qualifyingMinutes || 0,
|
||||
sprintRaceMinutes: form.timings?.sprintRaceMinutes || 0,
|
||||
mainRaceMinutes: form.timings?.mainRaceMinutes || 0,
|
||||
sessionCount: form.timings?.sessionCount || 0,
|
||||
roundsPlanned: form.timings?.roundsPlanned || 0,
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: (form.stewarding?.decisionMode as 'owner_only' | 'admin_vote' | 'steward_panel') || 'admin_only',
|
||||
requiredVotes: form.stewarding?.requiredVotes || 0,
|
||||
requireDefense: form.stewarding?.requireDefense ?? false,
|
||||
defenseTimeLimit: form.stewarding?.defenseTimeLimit || 0,
|
||||
voteTimeLimit: form.stewarding?.voteTimeLimit || 0,
|
||||
protestDeadlineHours: form.stewarding?.protestDeadlineHours || 0,
|
||||
stewardingClosesHours: form.stewarding?.stewardingClosesHours || 0,
|
||||
notifyAccusedOnProtest: form.stewarding?.notifyAccusedOnProtest ?? true,
|
||||
notifyOnVoteRequired: form.stewarding?.notifyOnVoteRequired ?? true,
|
||||
},
|
||||
};
|
||||
|
||||
const stepErrors = LeagueWizardCommandModel.validateLeagueWizardStep(formData, currentStep);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
...stepErrors,
|
||||
@@ -354,7 +407,57 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
|
||||
return;
|
||||
}
|
||||
|
||||
const allErrors = LeagueWizardCommandModel.validateAllLeagueWizardSteps(form);
|
||||
// Convert form to LeagueWizardFormData for validation
|
||||
const formData: any = {
|
||||
leagueId: form.leagueId || '',
|
||||
basics: {
|
||||
name: form.basics?.name || '',
|
||||
description: form.basics?.description || '',
|
||||
visibility: (form.basics?.visibility as 'public' | 'private' | 'unlisted') || 'public',
|
||||
gameId: form.basics?.gameId || 'iracing',
|
||||
},
|
||||
structure: {
|
||||
mode: (form.structure?.mode as 'solo' | 'fixedTeams') || 'solo',
|
||||
maxDrivers: form.structure?.maxDrivers || 0,
|
||||
maxTeams: form.structure?.maxTeams || 0,
|
||||
driversPerTeam: form.structure?.driversPerTeam || 0,
|
||||
},
|
||||
championships: {
|
||||
enableDriverChampionship: form.championships?.enableDriverChampionship ?? true,
|
||||
enableTeamChampionship: form.championships?.enableTeamChampionship ?? false,
|
||||
enableNationsChampionship: form.championships?.enableNationsChampionship ?? false,
|
||||
enableTrophyChampionship: form.championships?.enableTrophyChampionship ?? false,
|
||||
},
|
||||
scoring: {
|
||||
patternId: form.scoring?.patternId || '',
|
||||
customScoringEnabled: form.scoring?.customScoringEnabled ?? false,
|
||||
},
|
||||
dropPolicy: {
|
||||
strategy: (form.dropPolicy?.strategy as 'none' | 'bestNResults' | 'dropWorstN') || 'bestNResults',
|
||||
n: form.dropPolicy?.n || 6,
|
||||
},
|
||||
timings: {
|
||||
practiceMinutes: form.timings?.practiceMinutes || 0,
|
||||
qualifyingMinutes: form.timings?.qualifyingMinutes || 0,
|
||||
sprintRaceMinutes: form.timings?.sprintRaceMinutes || 0,
|
||||
mainRaceMinutes: form.timings?.mainRaceMinutes || 0,
|
||||
sessionCount: form.timings?.sessionCount || 0,
|
||||
roundsPlanned: form.timings?.roundsPlanned || 0,
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: (form.stewarding?.decisionMode as 'owner_only' | 'admin_vote' | 'steward_panel') || 'admin_only',
|
||||
requiredVotes: form.stewarding?.requiredVotes || 0,
|
||||
requireDefense: form.stewarding?.requireDefense ?? false,
|
||||
defenseTimeLimit: form.stewarding?.defenseTimeLimit || 0,
|
||||
voteTimeLimit: form.stewarding?.voteTimeLimit || 0,
|
||||
protestDeadlineHours: form.stewarding?.protestDeadlineHours || 0,
|
||||
stewardingClosesHours: form.stewarding?.stewardingClosesHours || 0,
|
||||
notifyAccusedOnProtest: form.stewarding?.notifyAccusedOnProtest ?? true,
|
||||
notifyOnVoteRequired: form.stewarding?.notifyOnVoteRequired ?? true,
|
||||
},
|
||||
};
|
||||
|
||||
const allErrors = LeagueWizardCommandModel.validateAllLeagueWizardSteps(formData);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
...allErrors,
|
||||
@@ -372,9 +475,13 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await LeagueWizardService.createLeagueFromConfig(form, ownerId);
|
||||
// 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 =
|
||||
@@ -387,12 +494,79 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
|
||||
}
|
||||
};
|
||||
|
||||
const currentPreset =
|
||||
presets.find((p) => p.id === form.scoring.patternId) ?? null;
|
||||
|
||||
// Handler for scoring preset selection - delegates to application-level config helper
|
||||
const handleScoringPresetChange = (patternId: string) => {
|
||||
setForm((prev) => LeagueWizardCommandModel.applyScoringPresetToConfig(prev, patternId));
|
||||
setForm((prev) => {
|
||||
// Convert to LeagueWizardFormData for the command model
|
||||
const formData: any = {
|
||||
leagueId: prev.leagueId || '',
|
||||
basics: {
|
||||
name: prev.basics?.name || '',
|
||||
description: prev.basics?.description || '',
|
||||
visibility: (prev.basics?.visibility as 'public' | 'private' | 'unlisted') || 'public',
|
||||
gameId: prev.basics?.gameId || 'iracing',
|
||||
},
|
||||
structure: {
|
||||
mode: (prev.structure?.mode as 'solo' | 'fixedTeams') || 'solo',
|
||||
maxDrivers: prev.structure?.maxDrivers || 24,
|
||||
maxTeams: prev.structure?.maxTeams || 0,
|
||||
driversPerTeam: prev.structure?.driversPerTeam || 0,
|
||||
},
|
||||
championships: {
|
||||
enableDriverChampionship: prev.championships?.enableDriverChampionship ?? true,
|
||||
enableTeamChampionship: prev.championships?.enableTeamChampionship ?? false,
|
||||
enableNationsChampionship: prev.championships?.enableNationsChampionship ?? false,
|
||||
enableTrophyChampionship: prev.championships?.enableTrophyChampionship ?? false,
|
||||
},
|
||||
scoring: {
|
||||
patternId: prev.scoring?.patternId,
|
||||
customScoringEnabled: prev.scoring?.customScoringEnabled ?? false,
|
||||
},
|
||||
dropPolicy: {
|
||||
strategy: (prev.dropPolicy?.strategy as 'none' | 'bestNResults' | 'dropWorstN') || 'bestNResults',
|
||||
n: prev.dropPolicy?.n || 6,
|
||||
},
|
||||
timings: {
|
||||
practiceMinutes: prev.timings?.practiceMinutes || 0,
|
||||
qualifyingMinutes: prev.timings?.qualifyingMinutes || 0,
|
||||
sprintRaceMinutes: prev.timings?.sprintRaceMinutes || 0,
|
||||
mainRaceMinutes: prev.timings?.mainRaceMinutes || 0,
|
||||
sessionCount: prev.timings?.sessionCount || 0,
|
||||
roundsPlanned: prev.timings?.roundsPlanned || 0,
|
||||
raceDayOfWeek: prev.timings?.raceDayOfWeek || 0,
|
||||
raceTimeUtc: prev.timings?.raceTimeUtc || '',
|
||||
weekdays: (prev.timings?.weekdays as Weekday[]) || [],
|
||||
recurrenceStrategy: prev.timings?.recurrenceStrategy || '',
|
||||
timezoneId: prev.timings?.timezoneId || '',
|
||||
seasonStartDate: prev.timings?.seasonStartDate || '',
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: (prev.stewarding?.decisionMode as 'owner_only' | 'admin_vote' | 'steward_panel') || 'admin_only',
|
||||
requiredVotes: prev.stewarding?.requiredVotes || 2,
|
||||
requireDefense: prev.stewarding?.requireDefense ?? false,
|
||||
defenseTimeLimit: prev.stewarding?.defenseTimeLimit || 48,
|
||||
voteTimeLimit: prev.stewarding?.voteTimeLimit || 72,
|
||||
protestDeadlineHours: prev.stewarding?.protestDeadlineHours || 48,
|
||||
stewardingClosesHours: prev.stewarding?.stewardingClosesHours || 168,
|
||||
notifyAccusedOnProtest: prev.stewarding?.notifyAccusedOnProtest ?? true,
|
||||
notifyOnVoteRequired: prev.stewarding?.notifyOnVoteRequired ?? true,
|
||||
},
|
||||
};
|
||||
|
||||
const updated = LeagueWizardCommandModel.applyScoringPresetToConfig(formData, patternId);
|
||||
|
||||
// Convert back to LeagueWizardFormModel
|
||||
return {
|
||||
basics: updated.basics,
|
||||
structure: updated.structure,
|
||||
championships: updated.championships,
|
||||
scoring: updated.scoring,
|
||||
dropPolicy: updated.dropPolicy,
|
||||
timings: updated.timings,
|
||||
stewarding: updated.stewarding,
|
||||
seasonName: prev.seasonName,
|
||||
} as LeagueWizardFormModel;
|
||||
});
|
||||
};
|
||||
|
||||
const steps = [
|
||||
@@ -723,7 +897,7 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
|
||||
</div>
|
||||
{/* Scoring Pattern Selection */}
|
||||
<ScoringPatternSection
|
||||
scoring={form.scoring}
|
||||
scoring={form.scoring || {}}
|
||||
presets={presets}
|
||||
readOnly={presetsLoading}
|
||||
patternError={errors.scoring?.patternId ?? ''}
|
||||
@@ -733,7 +907,7 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
|
||||
...prev,
|
||||
scoring: {
|
||||
...prev.scoring,
|
||||
customScoringEnabled: !prev.scoring.customScoringEnabled,
|
||||
customScoringEnabled: !(prev.scoring?.customScoringEnabled),
|
||||
},
|
||||
}))
|
||||
}
|
||||
@@ -744,8 +918,8 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
|
||||
|
||||
{/* Championships & Drop Rules side by side on larger screens */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<ChampionshipsSection form={form} onChange={setForm} readOnly={presetsLoading} />
|
||||
<LeagueDropSection form={form} onChange={setForm} readOnly={false} />
|
||||
<ChampionshipsSection form={form} onChange={setForm as any} readOnly={presetsLoading} />
|
||||
<LeagueDropSection form={form} onChange={setForm as any} readOnly={false} />
|
||||
</div>
|
||||
|
||||
{errors.submit && (
|
||||
|
||||
Reference in New Issue
Block a user