wip
This commit is contained in:
@@ -1,9 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import { User, Users2, Info } from 'lucide-react';
|
||||
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 SegmentedControl from '@/components/ui/SegmentedControl';
|
||||
import type { LeagueConfigFormModel } from '@gridpilot/racing/application';
|
||||
import { GameConstraints } from '@gridpilot/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;
|
||||
@@ -127,123 +228,368 @@ export function LeagueStructureSection({
|
||||
});
|
||||
};
|
||||
|
||||
// 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-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 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>
|
||||
|
||||
{/* 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" />
|
||||
{/* 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>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 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">
|
||||
Max drivers
|
||||
Maximum drivers
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={structure.maxDrivers ?? 24}
|
||||
value={structure.maxDrivers ?? gameConstraints.defaultMaxDrivers}
|
||||
onChange={(e) => handleMaxDriversChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
min={1}
|
||||
max={64}
|
||||
className="w-32"
|
||||
min={gameConstraints.minDrivers}
|
||||
max={gameConstraints.maxDrivers}
|
||||
className="w-40"
|
||||
/>
|
||||
<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 20–30 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>
|
||||
<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-2 py-1 rounded bg-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue hover:text-primary-blue transition-colors"
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
Large (30)
|
||||
Full Grid (30)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{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>
|
||||
</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">
|
||||
{/* 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={() => {
|
||||
@@ -251,9 +597,13 @@ export function LeagueStructureSection({
|
||||
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"
|
||||
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 teams × 2 drivers (20 grid)
|
||||
10 × 2 (20 grid)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -262,9 +612,13 @@ export function LeagueStructureSection({
|
||||
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"
|
||||
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 teams × 2 drivers (24 grid)
|
||||
12 × 2 (24 grid)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -273,17 +627,37 @@ export function LeagueStructureSection({
|
||||
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"
|
||||
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 teams × 3 drivers (24 grid)
|
||||
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>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{/* 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">
|
||||
Max teams
|
||||
Teams
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -292,17 +666,13 @@ export function LeagueStructureSection({
|
||||
disabled={disabled}
|
||||
min={1}
|
||||
max={32}
|
||||
className="w-32"
|
||||
className="w-full"
|
||||
/>
|
||||
<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
|
||||
Drivers / team
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -311,36 +681,27 @@ export function LeagueStructureSection({
|
||||
disabled={disabled}
|
||||
min={1}
|
||||
max={6}
|
||||
className="w-32"
|
||||
className="w-full"
|
||||
/>
|
||||
<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: 2–3 drivers</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-300">
|
||||
Total grid size
|
||||
Total grid
|
||||
</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 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>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user