702 lines
28 KiB
TypeScript
702 lines
28 KiB
TypeScript
'use client';
|
||
|
||
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';
|
||
|
||
// ============================================================================
|
||
// INFO FLYOUT COMPONENT
|
||
// ============================================================================
|
||
|
||
interface InfoFlyoutProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
title: string;
|
||
children: React.ReactNode;
|
||
anchorRef: React.RefObject<HTMLElement | null>;
|
||
}
|
||
|
||
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(
|
||
<div
|
||
ref={flyoutRef}
|
||
className="fixed z-50 w-[320px] max-h-[80vh] overflow-y-auto bg-iron-gray border border-charcoal-outline rounded-xl shadow-2xl animate-fade-in"
|
||
style={{ top: position.top, left: position.left }}
|
||
>
|
||
<div className="flex items-center justify-between p-4 border-b border-charcoal-outline/50 sticky top-0 bg-iron-gray z-10">
|
||
<div className="flex items-center gap-2">
|
||
<HelpCircle className="w-4 h-4 text-primary-blue" />
|
||
<span className="text-sm font-semibold text-white">{title}</span>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="flex h-6 w-6 items-center justify-center rounded-md hover:bg-charcoal-outline transition-colors"
|
||
>
|
||
<X className="w-4 h-4 text-gray-400" />
|
||
</button>
|
||
</div>
|
||
<div className="p-4">
|
||
{children}
|
||
</div>
|
||
</div>,
|
||
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') {
|
||
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(
|
||
() => GameConstraints.forGame(form.basics.gameId),
|
||
[form.basics.gameId]
|
||
);
|
||
|
||
return (
|
||
<div className="space-y-8">
|
||
{/* Emotional header */}
|
||
<div className="text-center pb-2">
|
||
<h3 className="text-lg font-semibold text-white mb-2">
|
||
How will your drivers compete?
|
||
</h3>
|
||
<p className="text-sm text-gray-400 max-w-lg mx-auto">
|
||
Choose your championship format — individual glory or team triumph.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Mode Selection Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{/* Solo Mode Card */}
|
||
<div className="relative">
|
||
<button
|
||
type="button"
|
||
disabled={disabled}
|
||
onClick={() => handleModeChange('solo')}
|
||
className={`w-full group relative flex flex-col gap-4 p-6 text-left rounded-xl border-2 transition-all duration-200 ${
|
||
isSolo
|
||
? '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)]'
|
||
: 'border-charcoal-outline bg-iron-gray/30 hover:border-gray-500 hover:bg-iron-gray/50'
|
||
} ${disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'}`}
|
||
>
|
||
{/* Header */}
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className={`flex h-12 w-12 items-center justify-center rounded-xl ${
|
||
isSolo ? 'bg-primary-blue/30' : 'bg-charcoal-outline/50'
|
||
}`}>
|
||
<User className={`w-6 h-6 ${isSolo ? 'text-primary-blue' : 'text-gray-400'}`} />
|
||
</div>
|
||
<div>
|
||
<div className={`text-lg font-bold ${isSolo ? 'text-white' : 'text-gray-300'}`}>
|
||
Solo Drivers
|
||
</div>
|
||
<div className={`text-xs ${isSolo ? 'text-primary-blue' : 'text-gray-500'}`}>
|
||
Individual competition
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* Radio indicator */}
|
||
<div className={`flex h-6 w-6 items-center justify-center rounded-full border-2 shrink-0 transition-colors ${
|
||
isSolo ? 'border-primary-blue bg-primary-blue' : 'border-gray-500'
|
||
}`}>
|
||
{isSolo && <Check className="w-3.5 h-3.5 text-white" />}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Emotional tagline */}
|
||
<p className={`text-sm ${isSolo ? 'text-gray-300' : 'text-gray-500'}`}>
|
||
Every driver for themselves. Pure skill, pure competition, pure glory.
|
||
</p>
|
||
|
||
{/* Features */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||
<Check className={`w-3.5 h-3.5 ${isSolo ? 'text-performance-green' : 'text-gray-500'}`} />
|
||
<span>Individual driver standings</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||
<Check className={`w-3.5 h-3.5 ${isSolo ? 'text-performance-green' : 'text-gray-500'}`} />
|
||
<span>Simple, classic format</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||
<Check className={`w-3.5 h-3.5 ${isSolo ? 'text-performance-green' : 'text-gray-500'}`} />
|
||
<span>Perfect for any grid size</span>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
|
||
{/* Info button */}
|
||
<button
|
||
ref={soloInfoRef}
|
||
type="button"
|
||
onClick={() => setShowSoloFlyout(true)}
|
||
className="absolute top-2 right-2 flex h-6 w-6 items-center justify-center rounded-full text-gray-500 hover:text-primary-blue hover:bg-primary-blue/10 transition-colors"
|
||
>
|
||
<HelpCircle className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Solo Info Flyout */}
|
||
<InfoFlyout
|
||
isOpen={showSoloFlyout}
|
||
onClose={() => setShowSoloFlyout(false)}
|
||
title="Solo Drivers Mode"
|
||
anchorRef={soloInfoRef}
|
||
>
|
||
<div className="space-y-4">
|
||
<p className="text-xs text-gray-400">
|
||
In solo mode, each driver competes individually. Points are awarded
|
||
based on finishing position, and standings track individual performance.
|
||
</p>
|
||
|
||
<div className="space-y-2">
|
||
<div className="text-[10px] text-gray-500 uppercase tracking-wide">Best For</div>
|
||
<ul className="space-y-1.5">
|
||
<li className="flex items-start gap-2 text-xs text-gray-400">
|
||
<Check className="w-3.5 h-3.5 text-performance-green shrink-0 mt-0.5" />
|
||
<span>Traditional racing championships</span>
|
||
</li>
|
||
<li className="flex items-start gap-2 text-xs text-gray-400">
|
||
<Check className="w-3.5 h-3.5 text-performance-green shrink-0 mt-0.5" />
|
||
<span>Smaller grids (10-30 drivers)</span>
|
||
</li>
|
||
<li className="flex items-start gap-2 text-xs text-gray-400">
|
||
<Check className="w-3.5 h-3.5 text-performance-green shrink-0 mt-0.5" />
|
||
<span>Quick setup with minimal coordination</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</InfoFlyout>
|
||
|
||
{/* Teams Mode Card */}
|
||
<div className="relative">
|
||
<button
|
||
type="button"
|
||
disabled={disabled}
|
||
onClick={() => handleModeChange('fixedTeams')}
|
||
className={`w-full group relative flex flex-col gap-4 p-6 text-left rounded-xl border-2 transition-all duration-200 ${
|
||
!isSolo
|
||
? '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)]'
|
||
: 'border-charcoal-outline bg-iron-gray/30 hover:border-gray-500 hover:bg-iron-gray/50'
|
||
} ${disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'}`}
|
||
>
|
||
{/* Header */}
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className={`flex h-12 w-12 items-center justify-center rounded-xl ${
|
||
!isSolo ? 'bg-neon-aqua/30' : 'bg-charcoal-outline/50'
|
||
}`}>
|
||
<Users2 className={`w-6 h-6 ${!isSolo ? 'text-neon-aqua' : 'text-gray-400'}`} />
|
||
</div>
|
||
<div>
|
||
<div className={`text-lg font-bold ${!isSolo ? 'text-white' : 'text-gray-300'}`}>
|
||
Team Racing
|
||
</div>
|
||
<div className={`text-xs ${!isSolo ? 'text-neon-aqua' : 'text-gray-500'}`}>
|
||
Shared destiny
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* Radio indicator */}
|
||
<div className={`flex h-6 w-6 items-center justify-center rounded-full border-2 shrink-0 transition-colors ${
|
||
!isSolo ? 'border-neon-aqua bg-neon-aqua' : 'border-gray-500'
|
||
}`}>
|
||
{!isSolo && <Check className="w-3.5 h-3.5 text-deep-graphite" />}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Emotional tagline */}
|
||
<p className={`text-sm ${!isSolo ? 'text-gray-300' : 'text-gray-500'}`}>
|
||
Victory is sweeter together. Build a team, share the podium.
|
||
</p>
|
||
|
||
{/* Features */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||
<Check className={`w-3.5 h-3.5 ${!isSolo ? 'text-neon-aqua' : 'text-gray-500'}`} />
|
||
<span>Team & driver standings</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||
<Check className={`w-3.5 h-3.5 ${!isSolo ? 'text-neon-aqua' : 'text-gray-500'}`} />
|
||
<span>Fixed roster per team</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||
<Check className={`w-3.5 h-3.5 ${!isSolo ? 'text-neon-aqua' : 'text-gray-500'}`} />
|
||
<span>Great for endurance & pro-am</span>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
|
||
{/* Info button */}
|
||
<button
|
||
ref={teamsInfoRef}
|
||
type="button"
|
||
onClick={() => setShowTeamsFlyout(true)}
|
||
className="absolute top-2 right-2 flex h-6 w-6 items-center justify-center rounded-full text-gray-500 hover:text-neon-aqua hover:bg-neon-aqua/10 transition-colors"
|
||
>
|
||
<HelpCircle className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Teams Info Flyout */}
|
||
<InfoFlyout
|
||
isOpen={showTeamsFlyout}
|
||
onClose={() => setShowTeamsFlyout(false)}
|
||
title="Team Racing Mode"
|
||
anchorRef={teamsInfoRef}
|
||
>
|
||
<div className="space-y-4">
|
||
<p className="text-xs text-gray-400">
|
||
In team mode, drivers are grouped into fixed teams. Points contribute to
|
||
both individual and team standings, creating deeper competition.
|
||
</p>
|
||
|
||
<div className="space-y-2">
|
||
<div className="text-[10px] text-gray-500 uppercase tracking-wide">Best For</div>
|
||
<ul className="space-y-1.5">
|
||
<li className="flex items-start gap-2 text-xs text-gray-400">
|
||
<Check className="w-3.5 h-3.5 text-neon-aqua shrink-0 mt-0.5" />
|
||
<span>Endurance races with driver swaps</span>
|
||
</li>
|
||
<li className="flex items-start gap-2 text-xs text-gray-400">
|
||
<Check className="w-3.5 h-3.5 text-neon-aqua shrink-0 mt-0.5" />
|
||
<span>Pro-Am style competitions</span>
|
||
</li>
|
||
<li className="flex items-start gap-2 text-xs text-gray-400">
|
||
<Check className="w-3.5 h-3.5 text-neon-aqua shrink-0 mt-0.5" />
|
||
<span>Larger organized leagues</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</InfoFlyout>
|
||
</div>
|
||
|
||
{/* Configuration Panel */}
|
||
<div className="rounded-xl border border-charcoal-outline bg-gradient-to-b from-iron-gray/50 to-iron-gray/30 p-6 space-y-5">
|
||
<div className="flex items-center gap-3">
|
||
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${
|
||
isSolo ? 'bg-primary-blue/20' : 'bg-neon-aqua/20'
|
||
}`}>
|
||
{isSolo ? (
|
||
<User className="w-5 h-5 text-primary-blue" />
|
||
) : (
|
||
<Users2 className="w-5 h-5 text-neon-aqua" />
|
||
)}
|
||
</div>
|
||
<div>
|
||
<h3 className="text-sm font-semibold text-white">
|
||
{isSolo ? 'Grid size' : 'Team configuration'}
|
||
</h3>
|
||
<p className="text-xs text-gray-500">
|
||
{isSolo
|
||
? 'How many drivers can join your championship?'
|
||
: 'Configure teams and roster sizes'
|
||
}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Solo mode capacity */}
|
||
{isSolo && (
|
||
<div className="space-y-4">
|
||
<div className="space-y-3">
|
||
<label className="block text-sm font-medium text-gray-300">
|
||
Maximum drivers
|
||
</label>
|
||
<Input
|
||
type="number"
|
||
value={structure.maxDrivers ?? gameConstraints.defaultMaxDrivers}
|
||
onChange={(e) => handleMaxDriversChange(e.target.value)}
|
||
disabled={disabled}
|
||
min={gameConstraints.minDrivers}
|
||
max={gameConstraints.maxDrivers}
|
||
className="w-40"
|
||
/>
|
||
<p className="text-xs text-gray-500">
|
||
{form.basics.gameId.toUpperCase()} supports up to {gameConstraints.maxDrivers} drivers
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<p className="text-xs text-gray-500">Quick select:</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => handleMaxDriversChange('16')}
|
||
disabled={disabled || 16 > gameConstraints.maxDrivers}
|
||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
|
||
structure.maxDrivers === 16
|
||
? 'bg-primary-blue text-white'
|
||
: 'bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue hover:text-primary-blue disabled:opacity-40 disabled:cursor-not-allowed'
|
||
}`}
|
||
>
|
||
Compact (16)
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleMaxDriversChange('24')}
|
||
disabled={disabled || 24 > gameConstraints.maxDrivers}
|
||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
|
||
structure.maxDrivers === 24
|
||
? 'bg-primary-blue text-white'
|
||
: 'bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue hover:text-primary-blue disabled:opacity-40 disabled:cursor-not-allowed'
|
||
}`}
|
||
>
|
||
Standard (24)
|
||
</button>
|
||
{gameConstraints.maxDrivers >= 30 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleMaxDriversChange('30')}
|
||
disabled={disabled}
|
||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
|
||
structure.maxDrivers === 30
|
||
? 'bg-primary-blue text-white'
|
||
: 'bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue hover:text-primary-blue'
|
||
}`}
|
||
>
|
||
Full Grid (30)
|
||
</button>
|
||
)}
|
||
{gameConstraints.maxDrivers >= 40 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleMaxDriversChange('40')}
|
||
disabled={disabled}
|
||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
|
||
structure.maxDrivers === 40
|
||
? 'bg-primary-blue text-white'
|
||
: 'bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue hover:text-primary-blue'
|
||
}`}
|
||
>
|
||
Large (40)
|
||
</button>
|
||
)}
|
||
{gameConstraints.maxDrivers >= 64 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleMaxDriversChange(String(gameConstraints.maxDrivers))}
|
||
disabled={disabled}
|
||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
|
||
structure.maxDrivers === gameConstraints.maxDrivers
|
||
? 'bg-primary-blue text-white'
|
||
: 'bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue hover:text-primary-blue'
|
||
}`}
|
||
>
|
||
Max ({gameConstraints.maxDrivers})
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Teams mode capacity */}
|
||
{!isSolo && (
|
||
<div className="space-y-5">
|
||
{/* Quick presets */}
|
||
<div className="space-y-3">
|
||
<p className="text-xs text-gray-500">Popular configurations:</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
handleMaxTeamsChange('10');
|
||
handleDriversPerTeamChange('2');
|
||
}}
|
||
disabled={disabled}
|
||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
|
||
structure.maxTeams === 10 && structure.driversPerTeam === 2
|
||
? 'bg-neon-aqua text-deep-graphite'
|
||
: 'bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-neon-aqua hover:text-neon-aqua'
|
||
}`}
|
||
>
|
||
10 × 2 (20 grid)
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
handleMaxTeamsChange('12');
|
||
handleDriversPerTeamChange('2');
|
||
}}
|
||
disabled={disabled}
|
||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
|
||
structure.maxTeams === 12 && structure.driversPerTeam === 2
|
||
? 'bg-neon-aqua text-deep-graphite'
|
||
: 'bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-neon-aqua hover:text-neon-aqua'
|
||
}`}
|
||
>
|
||
12 × 2 (24 grid)
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
handleMaxTeamsChange('8');
|
||
handleDriversPerTeamChange('3');
|
||
}}
|
||
disabled={disabled}
|
||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
|
||
structure.maxTeams === 8 && structure.driversPerTeam === 3
|
||
? 'bg-neon-aqua text-deep-graphite'
|
||
: 'bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-neon-aqua hover:text-neon-aqua'
|
||
}`}
|
||
>
|
||
8 × 3 (24 grid)
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
handleMaxTeamsChange('15');
|
||
handleDriversPerTeamChange('2');
|
||
}}
|
||
disabled={disabled}
|
||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
|
||
structure.maxTeams === 15 && structure.driversPerTeam === 2
|
||
? 'bg-neon-aqua text-deep-graphite'
|
||
: 'bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-neon-aqua hover:text-neon-aqua'
|
||
}`}
|
||
>
|
||
15 × 2 (30 grid)
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Manual configuration */}
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 pt-2 border-t border-charcoal-outline/50">
|
||
<div className="space-y-2">
|
||
<label className="block text-sm font-medium text-gray-300">
|
||
Teams
|
||
</label>
|
||
<Input
|
||
type="number"
|
||
value={structure.maxTeams ?? 12}
|
||
onChange={(e) => handleMaxTeamsChange(e.target.value)}
|
||
disabled={disabled}
|
||
min={1}
|
||
max={32}
|
||
className="w-full"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="block text-sm font-medium text-gray-300">
|
||
Drivers / team
|
||
</label>
|
||
<Input
|
||
type="number"
|
||
value={structure.driversPerTeam ?? 2}
|
||
onChange={(e) => handleDriversPerTeamChange(e.target.value)}
|
||
disabled={disabled}
|
||
min={1}
|
||
max={6}
|
||
className="w-full"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="block text-sm font-medium text-gray-300">
|
||
Total grid
|
||
</label>
|
||
<div className={`flex items-center justify-center h-10 rounded-lg border ${
|
||
!isSolo ? 'bg-neon-aqua/10 border-neon-aqua/30' : 'bg-iron-gray border-charcoal-outline'
|
||
}`}>
|
||
<span className={`text-lg font-bold ${!isSolo ? 'text-neon-aqua' : 'text-gray-400'}`}>
|
||
{structure.maxDrivers ?? 0}
|
||
</span>
|
||
<span className="text-xs text-gray-500 ml-1">drivers</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
} |