website cleanup
This commit is contained in:
@@ -4,7 +4,8 @@ import { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Input from '../ui/Input';
|
||||
import Button from '../ui/Button';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
|
||||
interface FormErrors {
|
||||
name?: string;
|
||||
@@ -49,6 +50,9 @@ export default function CreateLeagueForm() {
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const { session } = useAuth();
|
||||
const { driverService, leagueService } = useServices();
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -56,12 +60,16 @@ export default function CreateLeagueForm() {
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
if (!session?.user.userId) {
|
||||
setErrors({ submit: 'You must be logged in to create a league' });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const driverRepo = getDriverRepository();
|
||||
const drivers = await driverRepo.findAll();
|
||||
const currentDriver = drivers[0];
|
||||
// Get current driver
|
||||
const currentDriver = await driverService.getDriverProfile(session.user.userId);
|
||||
|
||||
if (!currentDriver) {
|
||||
setErrors({ submit: 'No driver profile found. Please create a profile first.' });
|
||||
@@ -69,22 +77,16 @@ export default function CreateLeagueForm() {
|
||||
return;
|
||||
}
|
||||
|
||||
const leagueRepo = getLeagueRepository();
|
||||
|
||||
const league = League.create({
|
||||
id: crypto.randomUUID(),
|
||||
// Create league using the league service
|
||||
const input = {
|
||||
name: formData.name.trim(),
|
||||
description: formData.description.trim(),
|
||||
ownerId: currentDriver.id,
|
||||
settings: {
|
||||
pointsSystem: formData.pointsSystem,
|
||||
sessionDuration: formData.sessionDuration,
|
||||
qualifyingFormat: 'open',
|
||||
},
|
||||
});
|
||||
visibility: 'public' as const,
|
||||
ownerId: session.user.userId,
|
||||
};
|
||||
|
||||
await leagueRepo.create(league);
|
||||
router.push(`/leagues/${league.id}`);
|
||||
const result = await leagueService.createLeague(input);
|
||||
router.push(`/leagues/${result.leagueId}`);
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
setErrors({
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from 'react';
|
||||
import { FileText, Gamepad2, AlertCircle, Check } from 'lucide-react';
|
||||
import Input from '@/components/ui/Input';
|
||||
import type { LeagueConfigFormModel } from '@core/racing/application';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
|
||||
interface LeagueBasicsSectionProps {
|
||||
form: LeagueConfigFormModel;
|
||||
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
ChevronRight,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import type { LeagueSummaryViewModel } from '@core/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter';
|
||||
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
||||
import { getLeagueCoverClasses } from '@/lib/leagueCovers';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
|
||||
interface LeagueCardProps {
|
||||
league: LeagueSummaryViewModel;
|
||||
@@ -71,9 +72,9 @@ function isNewLeague(createdAt: string | Date): boolean {
|
||||
}
|
||||
|
||||
export default function LeagueCard({ league, onClick }: LeagueCardProps) {
|
||||
const imageService = getImageService();
|
||||
const coverUrl = imageService.getLeagueCover(league.id);
|
||||
const logoUrl = imageService.getLeagueLogo(league.id);
|
||||
const { mediaService } = useServices();
|
||||
const coverUrl = mediaService.getLeagueCover(league.id);
|
||||
const logoUrl = mediaService.getLeagueLogo(league.id);
|
||||
|
||||
const ChampionshipIcon = getChampionshipIcon(league.scoring?.primaryChampionshipType);
|
||||
const championshipLabel = getChampionshipLabel(league.scoring?.primaryChampionshipType);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { TrendingDown, Check, HelpCircle, X, Zap } from 'lucide-react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { LeagueConfigFormModel } from '@core/racing/application';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
|
||||
// ============================================================================
|
||||
// INFO FLYOUT (duplicated for self-contained component)
|
||||
@@ -245,7 +245,7 @@ export function LeagueDropSection({
|
||||
readOnly,
|
||||
}: LeagueDropSectionProps) {
|
||||
const disabled = readOnly || !onChange;
|
||||
const dropPolicy = form.dropPolicy;
|
||||
const dropPolicy = form.dropPolicy || { strategy: 'none' as const };
|
||||
const [showDropFlyout, setShowDropFlyout] = useState(false);
|
||||
const [activeDropRuleFlyout, setActiveDropRuleFlyout] = useState<DropStrategy | null>(null);
|
||||
const dropInfoRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import DriverIdentity from '../drivers/DriverIdentity';
|
||||
import { useEffectiveDriverId } from '../../hooks/useEffectiveDriverId';
|
||||
import { useServices } from '../../lib/services/ServiceProvider';
|
||||
import type { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
import type { MembershipRole } from '@core/racing/domain/entities/MembershipRole';
|
||||
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
import type { MembershipRole } from '@/lib/types/MembershipRole';
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
@@ -66,10 +66,16 @@ export default function LeagueMembers({
|
||||
};
|
||||
|
||||
const getRoleOrder = (role: MembershipRole): number => {
|
||||
const order = { owner: 0, admin: 1, steward: 2, member: 3 };
|
||||
const order: Record<MembershipRole, number> = { owner: 0, admin: 1, steward: 2, member: 3 };
|
||||
return order[role];
|
||||
};
|
||||
|
||||
const getDriverStats = (driverId: string): { rating: number; wins: number; overallRank: number } | null => {
|
||||
// This would typically come from a driver stats service
|
||||
// For now, return null as the original implementation was missing
|
||||
return null;
|
||||
};
|
||||
|
||||
const sortedMembers = [...members].sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'role':
|
||||
@@ -105,6 +111,8 @@ export default function LeagueMembers({
|
||||
return 'bg-blue-500/10 text-blue-400 border-blue-500/30';
|
||||
case 'member':
|
||||
return 'bg-primary-blue/10 text-primary-blue border-primary-blue/30';
|
||||
default:
|
||||
return 'bg-gray-500/10 text-gray-400 border-gray-500/30';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Trophy, Award, Check, Zap, Settings, Globe, Medal, Plus, Minus, RotateCcw, HelpCircle, X } from 'lucide-react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { LeagueScoringPresetDTO } from '@core/racing/application/ports/LeagueScoringPresetProvider';
|
||||
import type { LeagueConfigFormModel } from '@core/racing/application';
|
||||
import type { LeagueScoringPresetDTO } from '@/hooks/useLeagueScoringPresets';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
|
||||
// ============================================================================
|
||||
// INFO FLYOUT COMPONENT
|
||||
|
||||
@@ -7,7 +7,7 @@ import Button from '../ui/Button';
|
||||
import Input from '../ui/Input';
|
||||
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { PendingSponsorshipRequestsPresenter } from '@/lib/presenters/PendingSponsorshipRequestsPresenter';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
|
||||
interface SponsorshipSlot {
|
||||
tier: 'main' | 'secondary';
|
||||
@@ -29,6 +29,7 @@ export function LeagueSponsorshipsSection({
|
||||
readOnly = false
|
||||
}: LeagueSponsorshipsSectionProps) {
|
||||
const currentDriverId = useEffectiveDriverId();
|
||||
const { sponsorshipService } = useServices();
|
||||
const [slots, setSlots] = useState<SponsorshipSlot[]>([
|
||||
{ tier: 'main', price: 500, isOccupied: false },
|
||||
{ tier: 'secondary', price: 200, isOccupied: false },
|
||||
|
||||
@@ -4,8 +4,7 @@ import { User, Users2, Info, Check, HelpCircle, X } from 'lucide-react';
|
||||
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import Input from '@/components/ui/Input';
|
||||
import type { LeagueConfigFormModel } from '@core/racing/application';
|
||||
import { GameConstraints } from '@core/racing/domain/value-objects/GameConstraints';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
|
||||
// ============================================================================
|
||||
// INFO FLYOUT COMPONENT
|
||||
@@ -233,8 +232,16 @@ export function LeagueStructureSection({
|
||||
|
||||
// Get game-specific constraints
|
||||
const gameConstraints = useMemo(
|
||||
() => GameConstraints.forGame(form.basics.gameId),
|
||||
[form.basics.gameId]
|
||||
() => ({
|
||||
minDrivers: 1,
|
||||
maxDrivers: 100,
|
||||
defaultMaxDrivers: 24,
|
||||
minTeams: 1,
|
||||
maxTeams: 50,
|
||||
minDriversPerTeam: 1,
|
||||
maxDriversPerTeam: 10,
|
||||
}),
|
||||
[form.basics?.gameId]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -17,13 +17,9 @@ import {
|
||||
Globe,
|
||||
MapPin,
|
||||
Pencil,
|
||||
Link2,
|
||||
} from 'lucide-react';
|
||||
import type {
|
||||
LeagueConfigFormModel,
|
||||
LeagueSchedulePreviewDTO,
|
||||
} from '@core/racing/application';
|
||||
import type { Weekday } from '@core/racing/domain/types/Weekday';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import type { Weekday } from '@/lib/types/Weekday';
|
||||
import Input from '@/components/ui/Input';
|
||||
import RangeField from '@/components/ui/RangeField';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Trophy, Users, Check, HelpCircle, X } from 'lucide-react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { LeagueConfigFormModel } from '@core/racing/application';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
|
||||
// Minimum drivers for ranked leagues
|
||||
const MIN_RANKED_DRIVERS = 10;
|
||||
@@ -132,14 +132,14 @@ export function LeagueVisibilitySection({
|
||||
const unrankedInfoRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Normalize visibility to new terminology
|
||||
const isRanked = basics.visibility === 'ranked' || basics.visibility === 'public';
|
||||
const isRanked = basics.visibility === 'public';
|
||||
|
||||
// Auto-update minDrivers when switching to ranked
|
||||
const handleVisibilityChange = (visibility: 'ranked' | 'unranked') => {
|
||||
const handleVisibilityChange = (visibility: 'public' | 'private' | 'unlisted') => {
|
||||
if (!onChange) return;
|
||||
|
||||
// If switching to ranked and current maxDrivers is below minimum, update it
|
||||
if (visibility === 'ranked' && form.structure.maxDrivers < MIN_RANKED_DRIVERS) {
|
||||
// If switching to public and current maxDrivers is below minimum, update it
|
||||
if (visibility === 'public' && (form.structure?.maxDrivers ?? 0) < MIN_RANKED_DRIVERS) {
|
||||
onChange({
|
||||
...form,
|
||||
basics: { ...form.basics, visibility },
|
||||
@@ -172,7 +172,7 @@ export function LeagueVisibilitySection({
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => handleVisibilityChange('ranked')}
|
||||
onClick={() => handleVisibilityChange('public')}
|
||||
className={`w-full group relative flex flex-col gap-4 p-6 text-left rounded-xl border-2 transition-all duration-200 ${
|
||||
isRanked
|
||||
? 'border-primary-blue bg-gradient-to-br from-primary-blue/15 to-primary-blue/5 shadow-[0_0_30px_rgba(25,140,255,0.25)]'
|
||||
@@ -293,7 +293,7 @@ export function LeagueVisibilitySection({
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => handleVisibilityChange('unranked')}
|
||||
onClick={() => handleVisibilityChange('private')}
|
||||
className={`w-full group relative flex flex-col gap-4 p-6 text-left rounded-xl border-2 transition-all duration-200 ${
|
||||
!isRanked
|
||||
? 'border-neon-aqua bg-gradient-to-br from-neon-aqua/15 to-neon-aqua/5 shadow-[0_0_30px_rgba(67,201,230,0.2)]'
|
||||
|
||||
Reference in New Issue
Block a user