'use client'; import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; import { Heading } from '@/ui/Heading'; import { Icon } from '@/ui/Icon'; import { Stack } from '@/ui/Stack'; import { Text } from '@/ui/Text'; import { Check, HelpCircle, Trophy, Users, X } from 'lucide-react'; import type * as React from 'react'; import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; // Minimum drivers for ranked leagues const MIN_RANKED_DRIVERS = 10; // ============================================================================ // 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(340, window.innerWidth - 40); const flyoutHeight = 350; 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 LeagueVisibilitySectionProps { form: LeagueConfigFormModel; onChange?: (form: LeagueConfigFormModel) => void; errors?: { visibility?: string; }; readOnly?: boolean; } export function LeagueVisibilitySection({ form, onChange, errors, readOnly, }: LeagueVisibilitySectionProps) { const basics = form.basics; const disabled = readOnly || !onChange; // Flyout state const [showRankedFlyout, setShowRankedFlyout] = useState(false); const [showUnrankedFlyout, setShowUnrankedFlyout] = useState(false); const rankedInfoRef = useRef(null!); const unrankedInfoRef = useRef(null!); // Normalize visibility to new terminology const isRanked = basics.visibility === 'public'; // Auto-update minDrivers when switching to ranked const handleVisibilityChange = (visibility: 'public' | 'private' | 'unlisted') => { if (!onChange) return; // If switching to public and current maxDrivers is below minimum, update it if (visibility === 'public' && (form.structure?.maxDrivers ?? 0) < MIN_RANKED_DRIVERS) { onChange({ ...form, basics: { ...form.basics, visibility }, structure: { ...form.structure, maxDrivers: MIN_RANKED_DRIVERS }, }); } else { onChange({ ...form, basics: { ...form.basics, visibility }, }); } }; return ( {/* Emotional header for the step */} Choose your league's destiny Will you compete for glory on the global leaderboards, or race with friends in a private series? {/* League Type Selection */} {/* Ranked (Public) Option */} handleVisibilityChange('public')} display="flex" flexDirection="col" gap={4} p={6} textAlign="left" rounded="xl" border borderColor={isRanked ? 'border-primary-blue' : 'border-charcoal-outline'} bg={isRanked ? 'bg-primary-blue/15' : 'bg-iron-gray/30'} w="full" position="relative" transition shadow={isRanked ? '0_0_30px_rgba(25,140,255,0.25)' : undefined} hoverBorderColor={!isRanked && !disabled ? 'border-gray-500' : undefined} hoverBg={!isRanked && !disabled ? 'bg-iron-gray/50' : undefined} opacity={disabled ? 0.6 : 1} cursor={disabled ? 'not-allowed' : 'pointer'} group > {/* Header */} Ranked Compete for glory {/* Radio indicator */} {isRanked && } {/* Emotional tagline */} Your results matter. Build your reputation in the global standings and climb the ranks. {/* Features */} Discoverable by all drivers Affects driver ratings & rankings Featured on leaderboards {/* Requirement badge */} Requires {MIN_RANKED_DRIVERS}+ drivers for competitive integrity {/* Info button */} setShowRankedFlyout(true)} position="absolute" top="3" right="3" display="flex" h="7" w="7" alignItems="center" justifyContent="center" rounded="full" transition color="text-gray-500" hoverTextColor="text-primary-blue" hoverBg="bg-primary-blue/10" > {/* Ranked Info Flyout */} setShowRankedFlyout(false)} title="Ranked Leagues" anchorRef={rankedInfoRef} > Ranked leagues are competitive series where results matter. Your performance affects your driver rating and contributes to global leaderboards. Requirements Minimum {MIN_RANKED_DRIVERS} drivers for competitive integrity Anyone can discover and join your league Benefits Results affect driver ratings and rankings Featured in league discovery {/* Unranked (Private) Option */} handleVisibilityChange('private')} display="flex" flexDirection="col" gap={4} p={6} textAlign="left" rounded="xl" border borderColor={!isRanked ? 'border-neon-aqua' : 'border-charcoal-outline'} bg={!isRanked ? 'bg-neon-aqua/15' : 'bg-iron-gray/30'} w="full" position="relative" transition shadow={!isRanked ? '0_0_30px_rgba(67,201,230,0.2)' : undefined} hoverBorderColor={isRanked && !disabled ? 'border-gray-500' : undefined} hoverBg={isRanked && !disabled ? 'bg-iron-gray/50' : undefined} opacity={disabled ? 0.6 : 1} cursor={disabled ? 'not-allowed' : 'pointer'} group > {/* Header */} Unranked Race with friends {/* Radio indicator */} {!isRanked && } {/* Emotional tagline */} Pure racing fun. No pressure, no rankings — just you and your crew hitting the track. {/* Features */} Private, invite-only access Zero impact on your rating Perfect for practice & fun {/* Flexibility badge */} Any size — even 2 friends {/* Info button */} setShowUnrankedFlyout(true)} position="absolute" top="3" right="3" display="flex" h="7" w="7" alignItems="center" justifyContent="center" rounded="full" transition color="text-gray-500" hoverTextColor="text-neon-aqua" hoverBg="bg-neon-aqua/10" > {/* Unranked Info Flyout */} setShowUnrankedFlyout(false)} title="Unranked Leagues" anchorRef={unrankedInfoRef} > Unranked leagues are casual, private series for racing with friends. Results don't affect driver ratings, so you can practice and have fun without pressure. Perfect For Private racing with friends Practice and training sessions Small groups (2+ drivers) Features Invite-only membership Full stats and standings (internal only) {errors?.visibility && ( {errors.visibility} )} {/* Contextual info based on selection */} {isRanked ? ( <> Ready to compete Your league will be visible to all GridPilot drivers. Results will affect driver ratings and contribute to the global leaderboards. Make sure you have at least {MIN_RANKED_DRIVERS} drivers to ensure competitive integrity. ) : ( <> Private racing awaits Your league will be invite-only. Perfect for racing with friends, practice sessions, or any time you want to have fun without affecting your official ratings. )} ); }