Files
gridpilot.gg/apps/website/components/leagues/LeagueStructureSection.tsx
2025-12-05 12:47:20 +01:00

346 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { User, Users2, Info } from 'lucide-react';
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">
{/* League structure selection */}
<div className="space-y-3">
<label className="flex items-center gap-2 text-sm font-medium text-gray-300">
<Users2 className="w-4 h-4 text-primary-blue" />
League structure
</label>
<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>
{/* Solo mode capacity */}
{structure.mode === 'solo' && (
<div className="rounded-lg border border-charcoal-outline bg-iron-gray/30 p-5 space-y-4">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-blue/10 shrink-0">
<User className="w-5 h-5 text-primary-blue" />
</div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-white mb-1">Driver capacity</h3>
<p className="text-xs text-gray-500">
Set the maximum number of drivers who can join your league
</p>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">
Max drivers
</label>
<Input
type="number"
value={structure.maxDrivers ?? 24}
onChange={(e) => handleMaxDriversChange(e.target.value)}
disabled={disabled}
min={1}
max={64}
className="w-32"
/>
<div className="space-y-2">
<p className="text-xs text-gray-500 flex items-start gap-1.5">
<Info className="w-3 h-3 mt-0.5 shrink-0" />
<span>Typical club leagues use 2030 drivers</span>
</p>
<div className="flex flex-wrap gap-2 text-xs">
<button
type="button"
onClick={() => handleMaxDriversChange('20')}
disabled={disabled}
className="px-2 py-1 rounded bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue hover:text-primary-blue transition-colors"
>
Small (20)
</button>
<button
type="button"
onClick={() => handleMaxDriversChange('24')}
disabled={disabled}
className="px-2 py-1 rounded bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue hover:text-primary-blue transition-colors"
>
Medium (24)
</button>
<button
type="button"
onClick={() => handleMaxDriversChange('30')}
disabled={disabled}
className="px-2 py-1 rounded bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue hover:text-primary-blue transition-colors"
>
Large (30)
</button>
</div>
</div>
</div>
</div>
</div>
)}
{/* Teams mode capacity */}
{structure.mode === 'fixedTeams' && (
<div className="rounded-lg border border-charcoal-outline bg-iron-gray/30 p-5 space-y-4">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-blue/10 shrink-0">
<Users2 className="w-5 h-5 text-primary-blue" />
</div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-white mb-1">Team structure</h3>
<p className="text-xs text-gray-500">
Configure the team composition and maximum grid size
</p>
</div>
</div>
<div className="space-y-4">
<div className="rounded-lg bg-primary-blue/5 border border-primary-blue/20 p-3">
<p className="text-xs text-gray-300">
<span className="font-medium text-primary-blue">Quick setup:</span> Choose a common configuration
</p>
<div className="flex flex-wrap gap-2 mt-2">
<button
type="button"
onClick={() => {
handleMaxTeamsChange('10');
handleDriversPerTeamChange('2');
}}
disabled={disabled}
className="px-3 py-1.5 rounded bg-iron-gray border border-charcoal-outline text-xs text-gray-300 hover:border-primary-blue hover:text-primary-blue transition-colors"
>
10 teams × 2 drivers (20 grid)
</button>
<button
type="button"
onClick={() => {
handleMaxTeamsChange('12');
handleDriversPerTeamChange('2');
}}
disabled={disabled}
className="px-3 py-1.5 rounded bg-iron-gray border border-charcoal-outline text-xs text-gray-300 hover:border-primary-blue hover:text-primary-blue transition-colors"
>
12 teams × 2 drivers (24 grid)
</button>
<button
type="button"
onClick={() => {
handleMaxTeamsChange('8');
handleDriversPerTeamChange('3');
}}
disabled={disabled}
className="px-3 py-1.5 rounded bg-iron-gray border border-charcoal-outline text-xs text-gray-300 hover:border-primary-blue hover:text-primary-blue transition-colors"
>
8 teams × 3 drivers (24 grid)
</button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">
Max teams
</label>
<Input
type="number"
value={structure.maxTeams ?? 12}
onChange={(e) => handleMaxTeamsChange(e.target.value)}
disabled={disabled}
min={1}
max={32}
className="w-32"
/>
<p className="text-xs text-gray-500 flex items-start gap-1.5">
<Info className="w-3 h-3 mt-0.5 shrink-0" />
<span>Total competing teams</span>
</p>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">
Drivers per team
</label>
<Input
type="number"
value={structure.driversPerTeam ?? 2}
onChange={(e) => handleDriversPerTeamChange(e.target.value)}
disabled={disabled}
min={1}
max={6}
className="w-32"
/>
<p className="text-xs text-gray-500 flex items-start gap-1.5">
<Info className="w-3 h-3 mt-0.5 shrink-0" />
<span>Common: 23 drivers</span>
</p>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">
Total grid size
</label>
<div className="flex items-center gap-2">
<Input
type="number"
value={structure.maxDrivers ?? 0}
disabled
className="w-32"
/>
<div className="text-xs text-gray-500">drivers</div>
</div>
<p className="text-xs text-gray-500 flex items-start gap-1.5">
<Info className="w-3 h-3 mt-0.5 shrink-0" />
<span>Auto-calculated from teams × drivers</span>
</p>
</div>
</div>
</div>
</div>
)}
</div>
);
}