Files
gridpilot.gg/apps/website/components/leagues/LeagueStructureSection.tsx
2026-01-18 23:24:30 +01:00

903 lines
35 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 type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Check, HelpCircle, User, Users2, X } from 'lucide-react';
import type * as React from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
// ============================================================================
// INFO FLYOUT COMPONENT
// ============================================================================
interface InfoFlyoutProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
anchorRef: React.RefObject<HTMLElement>;
}
function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutProps) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const [mounted, setMounted] = useState(false);
const flyoutRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (isOpen && anchorRef.current && mounted) {
const rect = anchorRef.current.getBoundingClientRect();
const flyoutWidth = Math.min(320, window.innerWidth - 40);
const flyoutHeight = 300;
const padding = 16;
let left = rect.right + 12;
let top = rect.top;
if (left + flyoutWidth > window.innerWidth - padding) {
left = rect.left - flyoutWidth - 12;
}
if (left < padding) {
left = Math.max(padding, (window.innerWidth - flyoutWidth) / 2);
}
top = rect.top - flyoutHeight / 3;
if (top + flyoutHeight > window.innerHeight - padding) {
top = window.innerHeight - flyoutHeight - padding;
}
if (top < padding) top = padding;
left = Math.max(padding, Math.min(left, window.innerWidth - flyoutWidth - padding));
setPosition({ top, left });
}
}, [isOpen, anchorRef, mounted]);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
const handleClickOutside = (e: MouseEvent) => {
if (flyoutRef.current && !flyoutRef.current.contains(e.target as Node)) {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, onClose]);
if (!isOpen || !mounted) return null;
return createPortal(
<Stack
ref={flyoutRef}
position="fixed"
zIndex={50}
w="320px"
bg="bg-iron-gray"
border
borderColor="border-charcoal-outline"
rounded="xl"
shadow="2xl"
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ top: position.top, left: position.left, maxHeight: '80vh', overflowY: 'auto' }}
>
<Stack
display="flex"
alignItems="center"
justifyContent="between"
p={4}
borderBottom
borderColor="border-charcoal-outline/50"
position="sticky"
top="0"
bg="bg-iron-gray"
zIndex={10}
>
<Stack direction="row" align="center" gap={2}>
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
</Stack>
<Stack
as="button"
type="button"
onClick={onClose}
display="flex"
h="6"
w="6"
alignItems="center"
justifyContent="center"
rounded="md"
transition
hoverBg="bg-charcoal-outline"
>
<Icon icon={X} size={4} color="text-gray-400" />
</Stack>
</Stack>
<Stack p={4}>
{children}
</Stack>
</Stack>,
document.body
);
}
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') {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { maxTeams, driversPerTeam, ...restStructure } = nextStructure;
nextForm = {
...nextForm,
structure: restStructure,
};
}
onChange(nextForm);
};
const handleModeChange = (mode: 'solo' | 'fixedTeams') => {
if (mode === structure.mode) return;
if (mode === 'solo') {
updateStructure({
mode: 'solo',
maxDrivers: structure.maxDrivers || 24,
});
} 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,
});
};
// Flyout state
const [showSoloFlyout, setShowSoloFlyout] = useState(false);
const [showTeamsFlyout, setShowTeamsFlyout] = useState(false);
const soloInfoRef = useRef<HTMLButtonElement>(null!);
const teamsInfoRef = useRef<HTMLButtonElement>(null!);
const isSolo = structure.mode === 'solo';
// Get game-specific constraints
const gameConstraints = useMemo(
() => ({
minDrivers: 1,
maxDrivers: 100,
defaultMaxDrivers: 24,
minTeams: 1,
maxTeams: 50,
minDriversPerTeam: 1,
maxDriversPerTeam: 10,
}),
[]
);
return (
<Stack gap={8}>
{/* Emotional header */}
<Stack textAlign="center" pb={2}>
<Heading level={3} mb={2}>
How will your drivers compete?
</Heading>
<Text size="sm" color="text-gray-400" maxWidth="lg" mx="auto" block>
Choose your championship format individual glory or team triumph.
</Text>
</Stack>
{/* Mode Selection Cards */}
<Stack display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={4}>
{/* Solo Mode Card */}
<Stack position="relative">
<Stack
as="button"
type="button"
disabled={disabled}
onClick={() => handleModeChange('solo')}
display="flex"
flexDirection="col"
gap={4}
p={6}
textAlign="left"
rounded="xl"
border
borderColor={isSolo ? 'border-primary-blue' : 'border-charcoal-outline'}
bg={isSolo ? 'bg-primary-blue/15' : 'bg-iron-gray/30'}
w="full"
position="relative"
transition
shadow={isSolo ? '0_0_30px_rgba(25,140,255,0.25)' : undefined}
hoverBorderColor={!isSolo && !disabled ? 'border-gray-500' : undefined}
hoverBg={!isSolo && !disabled ? 'bg-iron-gray/50' : undefined}
opacity={disabled ? 0.6 : 1}
cursor={disabled ? 'not-allowed' : 'pointer'}
group
>
{/* Header */}
<Stack display="flex" alignItems="start" justifyContent="between">
<Stack direction="row" align="center" gap={3}>
<Stack
display="flex"
h="12"
w="12"
alignItems="center"
justifyContent="center"
rounded="xl"
bg={isSolo ? 'bg-primary-blue/30' : 'bg-charcoal-outline/50'}
>
<Icon icon={User} size={6} color={isSolo ? 'text-primary-blue' : 'text-gray-400'} />
</Stack>
<Stack>
<Text weight="bold" size="lg" color={isSolo ? 'text-white' : 'text-gray-300'} block>
Solo Drivers
</Text>
<Text size="xs" color={isSolo ? 'text-primary-blue' : 'text-gray-500'} block>
Individual competition
</Text>
</Stack>
</Stack>
{/* Radio indicator */}
<Stack
display="flex"
h="6"
w="6"
alignItems="center"
justifyContent="center"
rounded="full"
border
borderColor={isSolo ? 'border-primary-blue' : 'border-gray-500'}
bg={isSolo ? 'bg-primary-blue' : ''}
flexShrink={0}
transition
>
{isSolo && <Icon icon={Check} size={3.5} color="text-white" />}
</Stack>
</Stack>
{/* Emotional tagline */}
<Text size="sm" color={isSolo ? 'text-gray-300' : 'text-gray-500'} block>
Every driver for themselves. Pure skill, pure competition, pure glory.
</Text>
{/* Features */}
<Stack gap={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Check} size={3.5} color={isSolo ? 'text-performance-green' : 'text-gray-500'} />
<Text size="xs" color="text-gray-400">Individual driver standings</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Check} size={3.5} color={isSolo ? 'text-performance-green' : 'text-gray-500'} />
<Text size="xs" color="text-gray-400">Simple, classic format</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Check} size={3.5} color={isSolo ? 'text-performance-green' : 'text-gray-500'} />
<Text size="xs" color="text-gray-400">Perfect for any grid size</Text>
</Stack>
</Stack>
</Stack>
{/* Info button */}
<Stack
as="button"
ref={soloInfoRef}
type="button"
onClick={() => setShowSoloFlyout(true)}
position="absolute"
top="2"
right="2"
display="flex"
h="6"
w="6"
alignItems="center"
justifyContent="center"
rounded="full"
transition
color="text-gray-500"
hoverTextColor="text-primary-blue"
hoverBg="bg-primary-blue/10"
>
<Icon icon={HelpCircle} size={4} />
</Stack>
</Stack>
{/* Solo Info Flyout */}
<InfoFlyout
isOpen={showSoloFlyout}
onClose={() => setShowSoloFlyout(false)}
title="Solo Drivers Mode"
anchorRef={soloInfoRef}
>
<Stack gap={4}>
<Text size="xs" color="text-gray-400" block>
In solo mode, each driver competes individually. Points are awarded
based on finishing position, and standings track individual performance.
</Text>
<Stack gap={2}>
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
block
>
Best For
</Text>
<Stack as="ul" gap={1.5}>
<Stack as="li" direction="row" align="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-performance-green" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Traditional racing championships</Text>
</Stack>
<Stack as="li" direction="row" align="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-performance-green" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Smaller grids (10-30 drivers)</Text>
</Stack>
<Stack as="li" direction="row" align="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-performance-green" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Quick setup with minimal coordination</Text>
</Stack>
</Stack>
</Stack>
</Stack>
</InfoFlyout>
{/* Teams Mode Card */}
<Stack position="relative">
<Stack
as="button"
type="button"
disabled={disabled}
onClick={() => handleModeChange('fixedTeams')}
display="flex"
flexDirection="col"
gap={4}
p={6}
textAlign="left"
rounded="xl"
border
borderColor={!isSolo ? 'border-neon-aqua' : 'border-charcoal-outline'}
bg={!isSolo ? 'bg-neon-aqua/15' : 'bg-iron-gray/30'}
w="full"
position="relative"
transition
shadow={!isSolo ? '0_0_30px_rgba(67,201,230,0.2)' : undefined}
hoverBorderColor={isSolo && !disabled ? 'border-gray-500' : undefined}
hoverBg={isSolo && !disabled ? 'bg-iron-gray/50' : undefined}
opacity={disabled ? 0.6 : 1}
cursor={disabled ? 'not-allowed' : 'pointer'}
group
>
{/* Header */}
<Stack display="flex" alignItems="start" justifyContent="between">
<Stack direction="row" align="center" gap={3}>
<Stack
display="flex"
h="12"
w="12"
alignItems="center"
justifyContent="center"
rounded="xl"
bg={!isSolo ? 'bg-neon-aqua/30' : 'bg-charcoal-outline/50'}
>
<Icon icon={Users2} size={6} color={!isSolo ? 'text-neon-aqua' : 'text-gray-400'} />
</Stack>
<Stack>
<Text weight="bold" size="lg" color={!isSolo ? 'text-white' : 'text-gray-300'} block>
Team Racing
</Text>
<Text size="xs" color={!isSolo ? 'text-neon-aqua' : 'text-gray-500'} block>
Shared destiny
</Text>
</Stack>
</Stack>
{/* Radio indicator */}
<Stack
display="flex"
h="6"
w="6"
alignItems="center"
justifyContent="center"
rounded="full"
border
borderColor={!isSolo ? 'border-neon-aqua' : 'border-gray-500'}
bg={!isSolo ? 'bg-neon-aqua' : ''}
flexShrink={0}
transition
>
{!isSolo && <Icon icon={Check} size={3.5} color="text-deep-graphite" />}
</Stack>
</Stack>
{/* Emotional tagline */}
<Text size="sm" color={!isSolo ? 'text-gray-300' : 'text-gray-500'} block>
Victory is sweeter together. Build a team, share the podium.
</Text>
{/* Features */}
<Stack gap={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Check} size={3.5} color={!isSolo ? 'text-neon-aqua' : 'text-gray-500'} />
<Text size="xs" color="text-gray-400">Team & driver standings</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Check} size={3.5} color={!isSolo ? 'text-neon-aqua' : 'text-gray-500'} />
<Text size="xs" color="text-gray-400">Fixed roster per team</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Check} size={3.5} color={!isSolo ? 'text-neon-aqua' : 'text-gray-500'} />
<Text size="xs" color="text-gray-400">Great for endurance & pro-am</Text>
</Stack>
</Stack>
</Stack>
{/* Info button */}
<Stack
as="button"
ref={teamsInfoRef}
type="button"
onClick={() => setShowTeamsFlyout(true)}
position="absolute"
top="2"
right="2"
display="flex"
h="6"
w="6"
alignItems="center"
justifyContent="center"
rounded="full"
transition
color="text-gray-500"
hoverTextColor="text-neon-aqua"
hoverBg="bg-neon-aqua/10"
>
<Icon icon={HelpCircle} size={4} />
</Stack>
</Stack>
{/* Teams Info Flyout */}
<InfoFlyout
isOpen={showTeamsFlyout}
onClose={() => setShowTeamsFlyout(false)}
title="Team Racing Mode"
anchorRef={teamsInfoRef}
>
<Stack gap={4}>
<Text size="xs" color="text-gray-400" block>
In team mode, drivers are grouped into fixed teams. Points contribute to
both individual and team standings, creating deeper competition.
</Text>
<Stack gap={2}>
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
block
>
Best For
</Text>
<Stack as="ul" gap={1.5}>
<Stack as="li" direction="row" align="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Endurance races with driver swaps</Text>
</Stack>
<Stack as="li" direction="row" align="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Pro-Am style competitions</Text>
</Stack>
<Stack as="li" direction="row" align="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Larger organized leagues</Text>
</Stack>
</Stack>
</Stack>
</Stack>
</InfoFlyout>
</Stack>
{/* Configuration Panel */}
<Stack rounded="xl" border borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={6}>
<Stack gap={5}>
<Stack direction="row" align="center" gap={3}>
<Stack
display="flex"
h="10"
w="10"
alignItems="center"
justifyContent="center"
rounded="lg"
bg={isSolo ? 'bg-primary-blue/20' : 'bg-neon-aqua/20'}
>
{isSolo ? (
<Icon icon={User} size={5} color="text-primary-blue" />
) : (
<Icon icon={Users2} size={5} color="text-neon-aqua" />
)}
</Stack>
<Stack>
<Text size="sm" weight="semibold" color="text-white" block>
{isSolo ? 'Grid size' : 'Team configuration'}
</Text>
<Text size="xs" color="text-gray-500" block>
{isSolo
? 'How many drivers can join your championship?'
: 'Configure teams and roster sizes'
}
</Text>
</Stack>
</Stack>
{/* Solo mode capacity */}
{isSolo && (
<Stack gap={4}>
<Stack gap={3}>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
Maximum drivers
</Text>
<Input
type="number"
value={structure.maxDrivers ?? gameConstraints.defaultMaxDrivers}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleMaxDriversChange(e.target.value)}
disabled={disabled}
min={gameConstraints.minDrivers}
max={gameConstraints.maxDrivers}
// eslint-disable-next-line gridpilot-rules/component-classification
className="w-40"
/>
<Text size="xs" color="text-gray-500" block>
{form.basics.gameId.toUpperCase()} supports up to {gameConstraints.maxDrivers} drivers
</Text>
</Stack>
<Stack gap={2}>
<Text size="xs" color="text-gray-500" block>Quick select:</Text>
<Stack display="flex" flexWrap="wrap" gap={2}>
<Stack
as="button"
type="button"
onClick={() => handleMaxDriversChange('16')}
disabled={disabled || 16 > gameConstraints.maxDrivers}
p={2}
px={3}
rounded="lg"
bg={structure.maxDrivers === 16 ? 'bg-primary-blue' : 'bg-iron-gray'}
border
borderColor={structure.maxDrivers === 16 ? 'border-primary-blue' : 'border-charcoal-outline'}
color={structure.maxDrivers === 16 ? 'text-white' : 'text-gray-300'}
hoverTextColor={structure.maxDrivers !== 16 ? 'text-primary-blue' : undefined}
hoverBorderColor={structure.maxDrivers !== 16 ? 'border-primary-blue' : undefined}
transition
>
Compact (16)
</Stack>
<Stack
as="button"
type="button"
onClick={() => handleMaxDriversChange('24')}
disabled={disabled || 24 > gameConstraints.maxDrivers}
p={2}
px={3}
rounded="lg"
bg={structure.maxDrivers === 24 ? 'bg-primary-blue' : 'bg-iron-gray'}
border
borderColor={structure.maxDrivers === 24 ? 'border-primary-blue' : 'border-charcoal-outline'}
color={structure.maxDrivers === 24 ? 'text-white' : 'text-gray-300'}
hoverTextColor={structure.maxDrivers !== 24 ? 'text-primary-blue' : undefined}
hoverBorderColor={structure.maxDrivers !== 24 ? 'border-primary-blue' : undefined}
transition
>
Standard (24)
</Stack>
<Stack
as="button"
type="button"
onClick={() => handleMaxDriversChange('30')}
disabled={disabled || 30 > gameConstraints.maxDrivers}
p={2}
px={3}
rounded="lg"
bg={structure.maxDrivers === 30 ? 'bg-primary-blue' : 'bg-iron-gray'}
border
borderColor={structure.maxDrivers === 30 ? 'border-primary-blue' : 'border-charcoal-outline'}
color={structure.maxDrivers === 30 ? 'text-white' : 'text-gray-300'}
hoverTextColor={structure.maxDrivers !== 30 ? 'text-primary-blue' : undefined}
hoverBorderColor={structure.maxDrivers !== 30 ? 'border-primary-blue' : undefined}
transition
>
Full Grid (30)
</Stack>
<Stack
as="button"
type="button"
onClick={() => handleMaxDriversChange('40')}
disabled={disabled || 40 > gameConstraints.maxDrivers}
p={2}
px={3}
rounded="lg"
bg={structure.maxDrivers === 40 ? 'bg-primary-blue' : 'bg-iron-gray'}
border
borderColor={structure.maxDrivers === 40 ? 'border-primary-blue' : 'border-charcoal-outline'}
color={structure.maxDrivers === 40 ? 'text-white' : 'text-gray-300'}
hoverTextColor={structure.maxDrivers !== 40 ? 'text-primary-blue' : undefined}
hoverBorderColor={structure.maxDrivers !== 40 ? 'border-primary-blue' : undefined}
transition
>
Large (40)
</Stack>
<Stack
as="button"
type="button"
onClick={() => handleMaxDriversChange(String(gameConstraints.maxDrivers))}
disabled={disabled}
p={2}
px={3}
rounded="lg"
bg={structure.maxDrivers === gameConstraints.maxDrivers ? 'bg-primary-blue' : 'bg-iron-gray'}
border
borderColor={structure.maxDrivers === gameConstraints.maxDrivers ? 'border-primary-blue' : 'border-charcoal-outline'}
color={structure.maxDrivers === gameConstraints.maxDrivers ? 'text-white' : 'text-gray-300'}
hoverTextColor={structure.maxDrivers !== gameConstraints.maxDrivers ? 'text-primary-blue' : undefined}
hoverBorderColor={structure.maxDrivers !== gameConstraints.maxDrivers ? 'border-primary-blue' : undefined}
transition
>
Max ({gameConstraints.maxDrivers})
</Stack>
</Stack>
</Stack>
</Stack>
)}
{/* Teams mode capacity */}
{!isSolo && (
<Stack gap={5}>
{/* Quick presets */}
<Stack gap={3}>
<Text size="xs" color="text-gray-500" block>Popular configurations:</Text>
<Stack display="flex" flexWrap="wrap" gap={2}>
<Stack
as="button"
type="button"
onClick={() => {
handleMaxTeamsChange('10');
handleDriversPerTeamChange('2');
}}
disabled={disabled}
p={2}
px={3}
rounded="lg"
bg={structure.maxTeams === 10 && structure.driversPerTeam === 2 ? 'bg-neon-aqua' : 'bg-iron-gray'}
border
borderColor={structure.maxTeams === 10 && structure.driversPerTeam === 2 ? 'border-neon-aqua' : 'border-charcoal-outline'}
color={structure.maxTeams === 10 && structure.driversPerTeam === 2 ? 'text-deep-graphite' : 'text-gray-300'}
hoverTextColor={!(structure.maxTeams === 10 && structure.driversPerTeam === 2) ? 'text-neon-aqua' : undefined}
hoverBorderColor={!(structure.maxTeams === 10 && structure.driversPerTeam === 2) ? 'border-neon-aqua' : undefined}
transition
>
10 × 2 (20 grid)
</Stack>
<Stack
as="button"
type="button"
onClick={() => {
handleMaxTeamsChange('12');
handleDriversPerTeamChange('2');
}}
disabled={disabled}
p={2}
px={3}
rounded="lg"
bg={structure.maxTeams === 12 && structure.driversPerTeam === 2 ? 'bg-neon-aqua' : 'bg-iron-gray'}
border
borderColor={structure.maxTeams === 12 && structure.driversPerTeam === 2 ? 'border-neon-aqua' : 'border-charcoal-outline'}
color={structure.maxTeams === 12 && structure.driversPerTeam === 2 ? 'text-deep-graphite' : 'text-gray-300'}
hoverTextColor={!(structure.maxTeams === 12 && structure.driversPerTeam === 2) ? 'text-neon-aqua' : undefined}
hoverBorderColor={!(structure.maxTeams === 12 && structure.driversPerTeam === 2) ? 'border-neon-aqua' : undefined}
transition
>
12 × 2 (24 grid)
</Stack>
<Stack
as="button"
type="button"
onClick={() => {
handleMaxTeamsChange('8');
handleDriversPerTeamChange('3');
}}
disabled={disabled}
p={2}
px={3}
rounded="lg"
bg={structure.maxTeams === 8 && structure.driversPerTeam === 3 ? 'bg-neon-aqua' : 'bg-iron-gray'}
border
borderColor={structure.maxTeams === 8 && structure.driversPerTeam === 3 ? 'border-neon-aqua' : 'border-charcoal-outline'}
color={structure.maxTeams === 8 && structure.driversPerTeam === 3 ? 'text-deep-graphite' : 'text-gray-300'}
hoverTextColor={!(structure.maxTeams === 8 && structure.driversPerTeam === 3) ? 'text-neon-aqua' : undefined}
hoverBorderColor={!(structure.maxTeams === 8 && structure.driversPerTeam === 3) ? 'border-neon-aqua' : undefined}
transition
>
8 × 3 (24 grid)
</Stack>
<Stack
as="button"
type="button"
onClick={() => {
handleMaxTeamsChange('15');
handleDriversPerTeamChange('2');
}}
disabled={disabled}
p={2}
px={3}
rounded="lg"
bg={structure.maxTeams === 15 && structure.driversPerTeam === 2 ? 'bg-neon-aqua' : 'bg-iron-gray'}
border
borderColor={structure.maxTeams === 15 && structure.driversPerTeam === 2 ? 'border-neon-aqua' : 'border-charcoal-outline'}
color={structure.maxTeams === 15 && structure.driversPerTeam === 2 ? 'text-deep-graphite' : 'text-gray-300'}
hoverTextColor={!(structure.maxTeams === 15 && structure.driversPerTeam === 2) ? 'text-neon-aqua' : undefined}
hoverBorderColor={!(structure.maxTeams === 15 && structure.driversPerTeam === 2) ? 'border-neon-aqua' : undefined}
transition
>
15 × 2 (30 grid)
</Stack>
</Stack>
</Stack>
{/* Manual configuration */}
<Stack display="grid" responsiveGridCols={{ base: 1, sm: 3 }} gap={4} pt={2} borderTop borderColor="border-charcoal-outline/50">
<Stack gap={2}>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
Teams
</Text>
<Input
type="number"
value={structure.maxTeams ?? 12}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleMaxTeamsChange(e.target.value)}
disabled={disabled}
min={1}
max={32}
/>
</Stack>
<Stack gap={2}>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
Drivers / team
</Text>
<Input
type="number"
value={structure.driversPerTeam ?? 2}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleDriversPerTeamChange(e.target.value)}
disabled={disabled}
min={1}
max={6}
/>
</Stack>
<Stack gap={2}>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
Total grid
</Text>
<Stack
display="flex"
alignItems="center"
justifyContent="center"
h="10"
rounded="lg"
border
bg={!isSolo ? 'bg-neon-aqua/10' : 'bg-iron-gray'}
borderColor={!isSolo ? 'border-neon-aqua/30' : 'border-charcoal-outline'}
>
<Text size="lg" weight="bold" color={!isSolo ? 'text-neon-aqua' : 'text-gray-400'}>
{structure.maxDrivers ?? 0}
</Text>
<Text size="xs" color="text-gray-500" ml={1}>drivers</Text>
</Stack>
</Stack>
</Stack>
</Stack>
)}
</Stack>
</Stack>
</Stack>
);
}