website cleanup

This commit is contained in:
2025-12-24 13:04:18 +01:00
parent 5e491d9724
commit a7aee42409
69 changed files with 1624 additions and 938 deletions

View File

@@ -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 && (