'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 '@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; } function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutProps) { const [position, setPosition] = useState({ top: 0, left: 0 }); const [mounted, setMounted] = useState(false); const flyoutRef = useRef(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(
{title}
{children}
, 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, ) => { 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(null); const teamsInfoRef = useRef(null); const isSolo = structure.mode === 'solo'; // Get game-specific constraints const gameConstraints = useMemo( () => GameConstraints.forGame(form.basics.gameId), [form.basics.gameId] ); return (
{/* Emotional header */}

How will your drivers compete?

Choose your championship format — individual glory or team triumph.

{/* Mode Selection Cards */}
{/* Solo Mode Card */}
{/* Info button */}
{/* Solo Info Flyout */} setShowSoloFlyout(false)} title="Solo Drivers Mode" anchorRef={soloInfoRef} >

In solo mode, each driver competes individually. Points are awarded based on finishing position, and standings track individual performance.

Best For
  • Traditional racing championships
  • Smaller grids (10-30 drivers)
  • Quick setup with minimal coordination
{/* Teams Mode Card */}
{/* Info button */}
{/* Teams Info Flyout */} setShowTeamsFlyout(false)} title="Team Racing Mode" anchorRef={teamsInfoRef} >

In team mode, drivers are grouped into fixed teams. Points contribute to both individual and team standings, creating deeper competition.

Best For
  • Endurance races with driver swaps
  • Pro-Am style competitions
  • Larger organized leagues
{/* Configuration Panel */}
{isSolo ? ( ) : ( )}

{isSolo ? 'Grid size' : 'Team configuration'}

{isSolo ? 'How many drivers can join your championship?' : 'Configure teams and roster sizes' }

{/* Solo mode capacity */} {isSolo && (
handleMaxDriversChange(e.target.value)} disabled={disabled} min={gameConstraints.minDrivers} max={gameConstraints.maxDrivers} className="w-40" />

{form.basics.gameId.toUpperCase()} supports up to {gameConstraints.maxDrivers} drivers

Quick select:

{gameConstraints.maxDrivers >= 30 && ( )} {gameConstraints.maxDrivers >= 40 && ( )} {gameConstraints.maxDrivers >= 64 && ( )}
)} {/* Teams mode capacity */} {!isSolo && (
{/* Quick presets */}

Popular configurations:

{/* Manual configuration */}
handleMaxTeamsChange(e.target.value)} disabled={disabled} min={1} max={32} className="w-full" />
handleDriversPerTeamChange(e.target.value)} disabled={disabled} min={1} max={6} className="w-full" />
{structure.maxDrivers ?? 0} drivers
)}
); }