This commit is contained in:
2025-12-05 12:24:38 +01:00
parent fb509607c1
commit 5a9cd28d5b
47 changed files with 5456 additions and 228 deletions

View File

@@ -0,0 +1,246 @@
'use client';
import Input from '@/components/ui/Input';
import SegmentedControl from '@/components/ui/SegmentedControl';
import type { LeagueConfigFormModel } from '@gridpilot/racing/application';
interface LeagueStructureSectionProps {
form: LeagueConfigFormModel;
onChange?: (form: LeagueConfigFormModel) => void;
readOnly?: boolean;
}
export function LeagueStructureSection({
form,
onChange,
readOnly,
}: LeagueStructureSectionProps) {
const disabled = readOnly || !onChange;
const structure = form.structure;
const updateStructure = (
patch: Partial<LeagueConfigFormModel['structure']>,
) => {
if (!onChange) return;
const nextStructure = {
...structure,
...patch,
};
let nextForm: LeagueConfigFormModel = {
...form,
structure: nextStructure,
};
if (nextStructure.mode === 'fixedTeams') {
const maxTeams =
typeof nextStructure.maxTeams === 'number' &&
nextStructure.maxTeams > 0
? nextStructure.maxTeams
: 1;
const driversPerTeam =
typeof nextStructure.driversPerTeam === 'number' &&
nextStructure.driversPerTeam > 0
? nextStructure.driversPerTeam
: 1;
const maxDrivers = maxTeams * driversPerTeam;
nextForm = {
...nextForm,
structure: {
...nextStructure,
maxTeams,
driversPerTeam,
maxDrivers,
},
};
}
if (nextStructure.mode === 'solo') {
nextForm = {
...nextForm,
structure: {
...nextStructure,
maxTeams: undefined,
driversPerTeam: undefined,
},
};
}
onChange(nextForm);
};
const handleModeChange = (mode: 'solo' | 'fixedTeams') => {
if (mode === structure.mode) return;
if (mode === 'solo') {
updateStructure({
mode: 'solo',
maxDrivers: structure.maxDrivers || 24,
maxTeams: undefined,
driversPerTeam: undefined,
});
} else {
const maxTeams = structure.maxTeams ?? 12;
const driversPerTeam = structure.driversPerTeam ?? 2;
updateStructure({
mode: 'fixedTeams',
maxTeams,
driversPerTeam,
maxDrivers: maxTeams * driversPerTeam,
});
}
};
const handleMaxDriversChange = (value: string) => {
const parsed = parseInt(value, 10);
updateStructure({
maxDrivers: Number.isNaN(parsed) ? 0 : parsed,
});
};
const handleMaxTeamsChange = (value: string) => {
const parsed = parseInt(value, 10);
const maxTeams = Number.isNaN(parsed) ? 0 : parsed;
const driversPerTeam = structure.driversPerTeam ?? 2;
updateStructure({
maxTeams,
driversPerTeam,
maxDrivers:
maxTeams > 0 && driversPerTeam > 0
? maxTeams * driversPerTeam
: structure.maxDrivers,
});
};
const handleDriversPerTeamChange = (value: string) => {
const parsed = parseInt(value, 10);
const driversPerTeam = Number.isNaN(parsed) ? 0 : parsed;
const maxTeams = structure.maxTeams ?? 12;
updateStructure({
driversPerTeam,
maxTeams,
maxDrivers:
maxTeams > 0 && driversPerTeam > 0
? maxTeams * driversPerTeam
: structure.maxDrivers,
});
};
return (
<div className="space-y-6">
<h2 className="text-lg font-semibold text-white">
Step 2 Structure &amp; capacity
</h2>
<div className="space-y-5">
<div>
<span className="block text-sm font-medium text-gray-300 mb-2">
League structure
</span>
<SegmentedControl
options={[
{
value: 'solo',
label: 'Drivers only (Solo)',
description: 'Individual drivers score points.',
},
{
value: 'fixedTeams',
label: 'Teams',
description: 'Teams with fixed drivers per team.',
},
]}
value={structure.mode}
onChange={
disabled
? undefined
: (mode) => handleModeChange(mode as 'solo' | 'fixedTeams')
}
/>
</div>
{structure.mode === 'solo' && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300">
Max drivers
</label>
<p className="text-xs text-gray-500">
Typical club leagues use 2030
</p>
<div className="mt-2">
<Input
type="number"
value={structure.maxDrivers ?? 24}
onChange={(e) => handleMaxDriversChange(e.target.value)}
disabled={disabled}
min={1}
max={64}
className="w-28"
/>
</div>
</div>
</div>
)}
{structure.mode === 'fixedTeams' && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300">
Max teams
</label>
<p className="text-xs text-gray-500">
Roughly how many teams you expect.
</p>
<div className="mt-2">
<Input
type="number"
value={structure.maxTeams ?? 12}
onChange={(e) => handleMaxTeamsChange(e.target.value)}
disabled={disabled}
min={1}
max={32}
className="w-28"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300">
Drivers per team
</label>
<p className="text-xs text-gray-500">
Common values are 23 drivers.
</p>
<div className="mt-2">
<Input
type="number"
value={structure.driversPerTeam ?? 2}
onChange={(e) => handleDriversPerTeamChange(e.target.value)}
disabled={disabled}
min={1}
max={6}
className="w-28"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300">
Max drivers (derived)
</label>
<p className="text-xs text-gray-500">
Calculated as teams × drivers per team.
</p>
<div className="mt-2 max-w-[7rem]">
<Input
type="number"
value={structure.maxDrivers ?? 0}
disabled
/>
</div>
</div>
</div>
)}
</div>
</div>
);
}