'use client'; import React, { useState, useRef, useEffect } from 'react'; import { Trophy, Award, Check, Zap, Settings, Globe, Medal, Plus, Minus, RotateCcw, HelpCircle, X } from 'lucide-react'; import { createPortal } from 'react-dom'; import type { LeagueScoringPresetDTO } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider'; import type { LeagueConfigFormModel } from '@gridpilot/racing/application'; // ============================================================================ // 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(380, window.innerWidth - 40); const flyoutHeight = 450; const padding = 16; // Try to position to the right first let left = rect.right + 12; let top = rect.top; // If goes off right edge, try left side if (left + flyoutWidth > window.innerWidth - padding) { left = rect.left - flyoutWidth - 12; } // If still off screen (left side), center horizontally if (left < padding) { left = Math.max(padding, (window.innerWidth - flyoutWidth) / 2); } // Vertical positioning - try to center on button, but stay in viewport top = rect.top - flyoutHeight / 3; // Adjust if goes off bottom edge if (top + flyoutHeight > window.innerHeight - padding) { top = window.innerHeight - flyoutHeight - padding; } // Ensure not above viewport if (top < padding) top = padding; // Final bounds check 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) return null; return createPortal(
{/* Header */}
{title}
{/* Content */}
{children}
, document.body ); } function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.RefObject }) { return ( ); } // ============================================================================ // MOCKUP COMPONENTS FOR FLYOUTS // ============================================================================ function PointsSystemMockup() { const positions = [ { pos: 1, pts: 25, color: 'bg-yellow-500' }, { pos: 2, pts: 18, color: 'bg-gray-300' }, { pos: 3, pts: 15, color: 'bg-amber-600' }, { pos: 4, pts: 12, color: 'bg-charcoal-outline' }, { pos: 5, pts: 10, color: 'bg-charcoal-outline' }, ]; return (
Position Points
{positions.map((p) => (
P{p.pos}
{p.pts}
))}
... down to P10 = 1 point
); } function BonusPointsMockup() { const bonuses = [ { emoji: '🏎️', label: 'Pole Position', pts: '+1', desc: 'Fastest in qualifying' }, { emoji: '⚡', label: 'Fastest Lap', pts: '+1', desc: 'Best race lap time' }, { emoji: '🥇', label: 'Led a Lap', pts: '+1', desc: 'Led at least one lap' }, ]; return (
{bonuses.map((b, i) => (
{b.emoji}
{b.label}
{b.desc}
{b.pts}
))}
); } function ChampionshipMockup() { const standings = [ { pos: 1, name: 'M. Verstappen', pts: 575, delta: null }, { pos: 2, name: 'L. Norris', pts: 412, delta: -163 }, { pos: 3, name: 'C. Leclerc', pts: 389, delta: -186 }, ]; return (
Driver Championship
{standings.map((s) => (
{s.pos}
{s.name}
{s.pts}
{s.delta && (
{s.delta}
)}
))}
Points accumulated across all races
); } function DropRulesMockup() { const results = [ { round: 'R1', pts: 25, dropped: false }, { round: 'R2', pts: 18, dropped: false }, { round: 'R3', pts: 4, dropped: true }, { round: 'R4', pts: 15, dropped: false }, { round: 'R5', pts: 12, dropped: false }, { round: 'R6', pts: 0, dropped: true }, ]; const total = results.filter(r => !r.dropped).reduce((sum, r) => sum + r.pts, 0); const wouldBe = results.reduce((sum, r) => sum + r.pts, 0); return (
Best 4 of 6 Results
{results.map((r, i) => (
{r.round}
{r.pts}
))}
Total counted: {total} pts
Without drops: {wouldBe} pts
); } interface LeagueScoringSectionProps { form: LeagueConfigFormModel; presets: LeagueScoringPresetDTO[]; onChange?: (form: LeagueConfigFormModel) => void; readOnly?: boolean; /** * When true, only render the scoring pattern panel. */ patternOnly?: boolean; /** * When true, only render the championships panel. */ championshipsOnly?: boolean; } interface ScoringPatternSectionProps { scoring: LeagueConfigFormModel['scoring']; presets: LeagueScoringPresetDTO[]; readOnly?: boolean; patternError?: string; onChangePatternId?: (patternId: string) => void; onToggleCustomScoring?: () => void; onUpdateCustomPoints?: (points: CustomPointsConfig) => void; } // Custom points configuration for inline editor export interface CustomPointsConfig { racePoints: number[]; poleBonusPoints: number; fastestLapPoints: number; leaderLapPoints: number; } const DEFAULT_CUSTOM_POINTS: CustomPointsConfig = { racePoints: [25, 18, 15, 12, 10, 8, 6, 4, 2, 1], poleBonusPoints: 1, fastestLapPoints: 1, leaderLapPoints: 0, }; interface ChampionshipsSectionProps { form: LeagueConfigFormModel; onChange?: (form: LeagueConfigFormModel) => void; readOnly?: boolean; } export function LeagueScoringSection({ form, presets, onChange, readOnly, patternOnly, championshipsOnly, }: LeagueScoringSectionProps) { const disabled = readOnly || !onChange; const handleSelectPreset = (presetId: string) => { if (disabled || !onChange) return; onChange({ ...form, scoring: { ...form.scoring, patternId: presetId, customScoringEnabled: false, }, }); }; const handleToggleCustomScoring = () => { if (disabled || !onChange) return; onChange({ ...form, scoring: { ...form.scoring, customScoringEnabled: !form.scoring.customScoringEnabled, }, }); }; const patternProps: ScoringPatternSectionProps = { scoring: form.scoring, presets, readOnly: !!readOnly, }; if (!readOnly && onChange) { patternProps.onChangePatternId = handleSelectPreset; } if (!disabled) { patternProps.onToggleCustomScoring = handleToggleCustomScoring; } const patternPanel = ; const championshipsProps: ChampionshipsSectionProps = { form, readOnly: !!readOnly, }; if (onChange) { championshipsProps.onChange = onChange; } const championshipsPanel = ; if (patternOnly) { return
{patternPanel}
; } if (championshipsOnly) { return
{championshipsPanel}
; } return (
{patternPanel} {championshipsPanel}
); } /** * Step 4 – Super simple scoring preset picker. * Flat layout, no collapsibles, easy to understand. */ // Preset info content for individual presets function getPresetInfoContent(presetName: string): { title: string; description: string; details: string[] } { const name = presetName.toLowerCase(); if (name.includes('sprint')) { return { title: 'Sprint + Feature Format', description: 'A two-race weekend format with a shorter sprint race and a longer feature race.', details: [ 'Sprint race typically awards reduced points (e.g., 8-6-4-3-2-1)', 'Feature race awards full points (e.g., 25-18-15-12-10-8-6-4-2-1)', 'Grid for feature often based on sprint results', 'Great for competitive leagues with time for multiple races', ], }; } if (name.includes('endurance') || name.includes('long')) { return { title: 'Endurance Format', description: 'Long-form racing focused on consistency and strategy over raw pace.', details: [ 'Single race per weekend, longer duration (60-90+ minutes)', 'Higher points for finishing (rewards reliability)', 'Often includes mandatory pit stops', 'Best for serious leagues with dedicated racers', ], }; } if (name.includes('club') || name.includes('casual')) { return { title: 'Club/Casual Format', description: 'Relaxed format perfect for community leagues and casual racing.', details: [ 'Simple points structure, easy to understand', 'Typically single race per weekend', 'Lower stakes, focus on participation', 'Great for beginners or mixed-skill leagues', ], }; } return { title: 'Standard Race Format', description: 'Traditional single-race weekend with standard F1-style points.', details: [ 'Points: 25-18-15-12-10-8-6-4-2-1 for top 10', 'Bonus points for pole position and fastest lap', 'One race per weekend', 'The most common format used in sim racing', ], }; } export function ScoringPatternSection({ scoring, presets, readOnly, patternError, onChangePatternId, onToggleCustomScoring, onUpdateCustomPoints, }: ScoringPatternSectionProps) { const disabled = readOnly || !onChangePatternId; const currentPreset = presets.find((p) => p.id === scoring.patternId) ?? null; const isCustom = scoring.customScoringEnabled; // Local state for custom points editor const [customPoints, setCustomPoints] = useState(DEFAULT_CUSTOM_POINTS); const updateRacePoints = (index: number, delta: number) => { setCustomPoints((prev) => { const newPoints = [...prev.racePoints]; newPoints[index] = Math.max(0, (newPoints[index] ?? 0) + delta); const updated = { ...prev, racePoints: newPoints }; onUpdateCustomPoints?.(updated); return updated; }); }; const addPosition = () => { setCustomPoints((prev) => { const lastPoints = prev.racePoints[prev.racePoints.length - 1] ?? 0; const updated = { ...prev, racePoints: [...prev.racePoints, Math.max(0, lastPoints - 1)] }; onUpdateCustomPoints?.(updated); return updated; }); }; const removePosition = () => { if (customPoints.racePoints.length <= 3) return; setCustomPoints((prev) => { const updated = { ...prev, racePoints: prev.racePoints.slice(0, -1) }; onUpdateCustomPoints?.(updated); return updated; }); }; const updateBonus = (key: keyof Omit, delta: number) => { setCustomPoints((prev) => { const updated = { ...prev, [key]: Math.max(0, prev[key] + delta) }; onUpdateCustomPoints?.(updated); return updated; }); }; const resetToDefaults = () => { setCustomPoints(DEFAULT_CUSTOM_POINTS); onUpdateCustomPoints?.(DEFAULT_CUSTOM_POINTS); }; const getPresetEmoji = (preset: LeagueScoringPresetDTO) => { const name = preset.name.toLowerCase(); if (name.includes('sprint') || name.includes('double')) return '⚡'; if (name.includes('endurance') || name.includes('long')) return '🏆'; if (name.includes('club') || name.includes('casual')) return '🏅'; return '🏁'; }; const getPresetDescription = (preset: LeagueScoringPresetDTO) => { const name = preset.name.toLowerCase(); if (name.includes('sprint')) return 'Sprint + Feature race'; if (name.includes('endurance')) return 'Long-form endurance'; if (name.includes('club')) return 'Casual league format'; return preset.sessionSummary; }; // Flyout state const [showPointsFlyout, setShowPointsFlyout] = useState(false); const [showBonusFlyout, setShowBonusFlyout] = useState(false); const [activePresetFlyout, setActivePresetFlyout] = useState(null); const pointsInfoRef = useRef(null); const bonusInfoRef = useRef(null); const presetInfoRefs = useRef>({}); return (
{/* Section header */}

Points System

setShowPointsFlyout(true)} />

Choose how points are awarded

{/* Points System Flyout */} setShowPointsFlyout(false)} title="How Points Work" anchorRef={pointsInfoRef} >

Points are awarded based on finishing position. Higher positions earn more points, which accumulate across the season to determine championship standings.

Example: F1-Style Points
Pro tip: Sprint formats award points in both races, typically with reduced points for the sprint.
{/* Two-column layout: Presets | Custom */}
{/* Preset options */}
{presets.length === 0 ? (

Loading presets...

) : ( presets.map((preset) => { const isSelected = !isCustom && scoring.patternId === preset.id; const presetInfo = getPresetInfoContent(preset.name); return (
{/* Preset Info Flyout */} setActivePresetFlyout(null)} title={presetInfo.title} anchorRef={{ current: presetInfoRefs.current[preset.id] ?? null }} >

{presetInfo.description}

Key Features
    {presetInfo.details.map((detail, idx) => (
  • {detail}
  • ))}
{preset.bonusSummary && (
Bonus points: {preset.bonusSummary}
)}
); }) )}
{/* Custom scoring option */}
{/* Error message */} {patternError && (

{patternError}

)} {/* Custom scoring editor - inline, no placeholder */} {isCustom && (
{/* Header with reset button */}
Custom Points Table setShowBonusFlyout(true)} />
{/* Bonus Points Flyout */} setShowBonusFlyout(false)} title="Bonus Points Explained" anchorRef={bonusInfoRef} >

Bonus points reward exceptional performances beyond just finishing position. They add strategic depth and excitement to your championship.

Available Bonuses
Example: A driver finishing P1 with pole and fastest lap would earn 25 + 1 + 1 = 27 points.
{/* Race position points */}
Finish position points
{customPoints.racePoints.map((pts, idx) => (
P{idx + 1}
{pts}
))}
{/* Bonus points */}
{[ { key: 'poleBonusPoints' as const, label: 'Pole', emoji: '🏎️' }, { key: 'fastestLapPoints' as const, label: 'Fast lap', emoji: '⚡' }, { key: 'leaderLapPoints' as const, label: 'Led lap', emoji: '🥇' }, ].map((bonus) => (
{bonus.emoji} {bonus.label}
{customPoints[bonus.key]}
))}
)}
); } /** * Championships section - simple inline toggle list. * No collapsibles, flat and easy to scan. */ // Championship info content const CHAMPIONSHIP_INFO: Record = { enableDriverChampionship: { title: 'Driver Championship', description: 'Track individual driver performance across all races in the season.', details: [ 'Each driver accumulates points based on race finishes', 'The driver with most points at season end wins', 'Standard in all racing leagues', 'Shows overall driver skill and consistency', ], }, enableTeamChampionship: { title: 'Team Championship', description: 'Combine points from all drivers within a team for team standings.', details: [ 'All drivers\' points count toward team total', 'Rewards having consistent performers across the roster', 'Creates team strategy opportunities', 'Only available in Teams mode leagues', ], }, enableNationsChampionship: { title: 'Nations Cup', description: 'Group drivers by nationality for international competition.', details: [ 'Drivers represent their country automatically', 'Points pooled by nationality', 'Adds international rivalry element', 'Great for diverse, international leagues', ], }, enableTrophyChampionship: { title: 'Trophy Championship', description: 'A special category championship for specific classes or groups.', details: [ 'Custom category you define (e.g., Am drivers, rookies)', 'Separate standings from main championship', 'Encourages participation from all skill levels', 'Can be used for gentleman drivers, newcomers, etc.', ], }, }; export function ChampionshipsSection({ form, onChange, readOnly, }: ChampionshipsSectionProps) { const disabled = readOnly || !onChange; const isTeamsMode = form.structure.mode === 'fixedTeams'; const [showChampFlyout, setShowChampFlyout] = useState(false); const [activeChampFlyout, setActiveChampFlyout] = useState(null); const champInfoRef = useRef(null); const champItemRefs = useRef>({}); const updateChampionship = (key: keyof LeagueConfigFormModel['championships'], value: boolean) => { if (!onChange) return; onChange({ ...form, championships: { ...form.championships, [key]: value, }, }); }; const championships = [ { key: 'enableDriverChampionship' as const, icon: Trophy, label: 'Driver Standings', description: 'Track individual driver points', enabled: form.championships.enableDriverChampionship, available: true, }, { key: 'enableTeamChampionship' as const, icon: Award, label: 'Team Standings', description: 'Combined team points', enabled: form.championships.enableTeamChampionship, available: isTeamsMode, unavailableHint: 'Teams mode only', }, { key: 'enableNationsChampionship' as const, icon: Globe, label: 'Nations Cup', description: 'By nationality', enabled: form.championships.enableNationsChampionship, available: true, }, { key: 'enableTrophyChampionship' as const, icon: Medal, label: 'Trophy Cup', description: 'Special category', enabled: form.championships.enableTrophyChampionship, available: true, }, ]; return (
{/* Section header */}

Championships

setShowChampFlyout(true)} />

What standings to track

{/* Championships Flyout */} setShowChampFlyout(false)} title="Championship Standings" anchorRef={champInfoRef} >

Championships track cumulative points across all races. You can enable multiple championship types to run different competitions simultaneously.

Live Standings Example
Championship Types
{[ { icon: Trophy, label: 'Driver', desc: 'Individual points' }, { icon: Award, label: 'Team', desc: 'Combined drivers' }, { icon: Globe, label: 'Nations', desc: 'By country' }, { icon: Medal, label: 'Trophy', desc: 'Special class' }, ].map((t, i) => (
{t.label}
{t.desc}
))}
{/* Inline toggle grid */}
{championships.map((champ) => { const Icon = champ.icon; const isEnabled = champ.enabled && champ.available; const champInfo = CHAMPIONSHIP_INFO[champ.key]; return (
{/* Championship Item Info Flyout */} {champInfo && ( setActiveChampFlyout(null)} title={champInfo.title} anchorRef={{ current: champItemRefs.current[champ.key] ?? null }} >

{champInfo.description}

How It Works
    {champInfo.details.map((detail, idx) => (
  • {detail}
  • ))}
{!champ.available && (
Note: {champ.unavailableHint}. Switch to Teams mode to enable this championship.
)}
)}
); })}
); }