'use client'; import React, { useState, useRef, useEffect } from 'react'; import { TrendingDown, Check, HelpCircle, X, Zap } from 'lucide-react'; import { createPortal } from 'react-dom'; import type { LeagueConfigFormModel } from '@core/racing/application'; // ============================================================================ // INFO FLYOUT (duplicated for self-contained 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; 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) return null; return createPortal(
{title}
{children}
, document.body ); } function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.RefObject }) { return ( ); } // Drop Rules Mockup 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 LeagueDropSectionProps { form: LeagueConfigFormModel; onChange?: (form: LeagueConfigFormModel) => void; readOnly?: boolean; } type DropStrategy = 'none' | 'bestNResults' | 'dropWorstN'; // Drop rule info content const DROP_RULE_INFO: Record = { none: { title: 'All Results Count', description: 'Every race result affects the championship standings with no exceptions.', details: [ 'All race results count toward final standings', 'No safety net for bad races or DNFs', 'Rewards consistency across entire season', 'Best for shorter seasons (4-6 races)', ], example: '8 races × your points = your total', }, bestNResults: { title: 'Best N Results', description: 'Only your top N race results count toward the championship.', details: [ 'Choose how many of your best races count', 'Extra races become "bonus" opportunities', 'Protects against occasional bad days', 'Encourages trying even when behind', ], example: 'Best 6 of 8 races count', }, dropWorstN: { title: 'Drop Worst N Results', description: 'Your N worst race results are excluded from championship calculations.', details: [ 'Automatically removes your worst performances', 'Great for handling DNFs or incidents', 'All other races count normally', 'Common in real-world championships', ], example: 'Drop 2 worst → 6 of 8 count', }, }; const DROP_OPTIONS: Array<{ value: DropStrategy; label: string; emoji: string; description: string; defaultN?: number; }> = [ { value: 'none', label: 'All count', emoji: '✓', description: 'Every race counts', }, { value: 'bestNResults', label: 'Best N', emoji: '🏆', description: 'Only best results', defaultN: 6, }, { value: 'dropWorstN', label: 'Drop worst', emoji: '🗑️', description: 'Exclude worst races', defaultN: 2, }, ]; export function LeagueDropSection({ form, onChange, readOnly, }: LeagueDropSectionProps) { const disabled = readOnly || !onChange; const dropPolicy = form.dropPolicy; const [showDropFlyout, setShowDropFlyout] = useState(false); const [activeDropRuleFlyout, setActiveDropRuleFlyout] = useState(null); const dropInfoRef = useRef(null); const dropRuleRefs = useRef>({ none: null, bestNResults: null, dropWorstN: null, }); const handleStrategyChange = (strategy: DropStrategy) => { if (disabled || !onChange) return; const option = DROP_OPTIONS.find((o) => o.value === strategy); const next: LeagueConfigFormModel = { ...form, dropPolicy: strategy === 'none' ? { strategy, } : { strategy, n: dropPolicy.n ?? option?.defaultN ?? 1, }, }; onChange(next); }; const handleNChange = (delta: number) => { if (disabled || !onChange || dropPolicy.strategy === 'none') return; const current = dropPolicy.n ?? 1; const newValue = Math.max(1, current + delta); onChange({ ...form, dropPolicy: { ...dropPolicy, n: newValue, }, }); }; const needsN = dropPolicy.strategy !== 'none'; return (
{/* Section header */}

Drop Rules

setShowDropFlyout(true)} />

Protect from bad races

{/* Drop Rules Flyout */} setShowDropFlyout(false)} title="Drop Rules Explained" anchorRef={dropInfoRef} >

Drop rules allow drivers to exclude their worst results from championship calculations. This protects against mechanical failures, bad luck, or occasional poor performances.

Visual Example
Drop Strategies
All Count
Every race affects standings. Best for short seasons.
🏆
Best N Results
Only your top N races count. Extra races are optional.
🗑️
Drop Worst N
Exclude your N worst results. Forgives bad days.
Pro tip: For an 8-round season, "Best 6" or "Drop 2" are popular choices.
{/* Strategy buttons + N stepper inline */}
{DROP_OPTIONS.map((option) => { const isSelected = dropPolicy.strategy === option.value; const ruleInfo = DROP_RULE_INFO[option.value]; return (
{/* Info button - separate from main button */} {/* Drop Rule Info Flyout */} setActiveDropRuleFlyout(null)} title={ruleInfo.title} anchorRef={{ current: dropRuleRefs.current[option.value] }} >

{ruleInfo.description}

How It Works
    {ruleInfo.details.map((detail, idx) => (
  • {detail}
  • ))}
{option.emoji}
Example
{ruleInfo.example}
); })} {/* N Stepper - only show when needed */} {needsN && (
N =
{dropPolicy.n ?? 1}
)}
{/* Explanation text */}

{dropPolicy.strategy === 'none' && 'Every race result affects the championship standings.'} {dropPolicy.strategy === 'bestNResults' && `Only your best ${dropPolicy.n ?? 1} results will count.`} {dropPolicy.strategy === 'dropWorstN' && `Your worst ${dropPolicy.n ?? 1} results will be excluded.`}

); }