903 lines
35 KiB
TypeScript
903 lines
35 KiB
TypeScript
'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>
|
||
);
|
||
}
|