'use client'; import { RangeField } from '@/components/shared/RangeField'; import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; import type { Weekday } from '@/lib/types/Weekday'; import { Heading } from '@/ui/Heading'; import { Icon } from '@/ui/Icon'; import { Input } from '@/ui/Input'; import { Stack } from '@/ui/Stack'; import { Text } from '@/ui/Text'; import { Calendar, CalendarDays, CalendarRange, ChevronDown, ChevronUp, Clock, Eye, Flag, Globe, Info, MapPin, Pencil, Play, Timer, Trophy, } from 'lucide-react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; // Common time zones for racing leagues const TIME_ZONES = [ { value: 'track', label: 'Track Local Time', icon: MapPin }, { value: 'UTC', label: 'UTC', icon: Globe }, { value: 'America/New_York', label: 'Eastern (US)', icon: Globe }, { value: 'America/Los_Angeles', label: 'Pacific (US)', icon: Globe }, { value: 'Europe/London', label: 'London (UK)', icon: Globe }, { value: 'Europe/Berlin', label: 'Central Europe', icon: Globe }, { value: 'Australia/Sydney', label: 'Sydney (AU)', icon: Globe }, ]; type RecurrenceStrategy = Exclude< NonNullable['recurrenceStrategy'], undefined >; interface LeagueTimingsSectionProps { form: LeagueConfigFormModel; onChange?: (form: LeagueConfigFormModel) => void; readOnly?: boolean; errors?: { qualifyingMinutes?: string; mainRaceMinutes?: string; roundsPlanned?: string; }; title?: string; } // ============================================================================ // PREVIEW COMPONENTS // ============================================================================ /** Race Day View - Shows what happens during a race day */ function RaceDayPreview({ template, practiceMin, qualifyingMin, sprintMin, mainRaceMin, raceTime, }: { template: string; practiceMin: number; qualifyingMin: number; sprintMin?: number; mainRaceMin: number; raceTime?: string; }) { const hasSprint = template === 'sprintFeature' && sprintMin; // Build all possible sessions with their active status const allSessions = useMemo(() => { return [ { name: 'Practice', duration: practiceMin, type: 'practice', active: practiceMin > 0 }, { name: 'Qualifying', duration: qualifyingMin, type: 'qualifying', active: true }, { name: 'Sprint Race', duration: sprintMin ?? 0, type: 'sprint', active: hasSprint }, { name: template === 'endurance' ? 'Endurance Race' : hasSprint ? 'Feature Race' : 'Main Race', duration: mainRaceMin, type: 'race', active: true, }, ]; }, [template, practiceMin, qualifyingMin, sprintMin, mainRaceMin, hasSprint]); const activeSessions = allSessions.filter(s => s.active); const totalDuration = activeSessions.reduce((sum, s) => sum + s.duration, 0); // Calculate start times - use default 20:00 if not set const effectiveRaceTime = raceTime || '20:00'; const getStartTime = (sessionIndex: number) => { const [hoursStr, minutesStr] = effectiveRaceTime.split(':'); const hours = Number(hoursStr ?? '0'); const minutes = Number(minutesStr ?? '0'); let totalMinutes = hours * 60 + minutes; const active = allSessions.filter((s) => s.active); for (let i = 0; i < sessionIndex; i++) { const session = active[i]; if (!session) continue; totalMinutes += session.duration + 10; // 10 min break between sessions } const h = Math.floor(totalMinutes / 60) % 24; const m = totalMinutes % 60; return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`; }; return ( Race Day Schedule Starts {effectiveRaceTime}{!raceTime && (default)} {/* Timeline visualization - show ALL sessions */} {allSessions.map((session, i) => { const isRace = session.type === 'race' || session.type === 'sprint'; const isActive = session.active; const activeIndex = isActive ? allSessions.filter((s, idx) => s.active && idx < i).length : -1; const startTime = isActive ? getStartTime(activeIndex) : null; return ( {/* Status badge */} {!isActive && ( Not included )} {/* Time marker */} {startTime ?? '—'} {/* Session indicator */} {/* Session info */} {session.name} {isActive ? `${session.duration} minutes` : 'Disabled'} {/* Duration bar */} {isActive && ( s.duration))) * 100}%` }} /> )} ); })} {/* Legend */} Active race Active session Not included {/* Summary */} {activeSessions.length} active sessions {activeSessions.filter(s => s.type === 'race' || s.type === 'sprint').length} race{activeSessions.filter(s => s.type === 'race' || s.type === 'sprint').length > 1 ? 's' : ''} {Math.floor(totalDuration / 60)}h {totalDuration % 60}m ); } /** Full Year Calendar View */ function YearCalendarPreview({ weekdays, frequency, rounds, startDate, endDate, }: { weekdays: Weekday[]; frequency: RecurrenceStrategy; rounds: number; startDate?: string; endDate?: string; }) { const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ] as const; const getMonthLabel = (index: number): string => months[index] ?? '—'; // Parse start and end dates const seasonStart = useMemo(() => { const d = startDate ? new Date(startDate) : new Date(); d.setHours(12, 0, 0, 0); return d; }, [startDate]); const seasonEnd = useMemo(() => { if (!endDate) return null; const d = new Date(endDate); d.setHours(12, 0, 0, 0); return d; }, [endDate]); // Calculate race dates based on settings const raceDates = useMemo(() => { // JavaScript getDay(): 0=Sunday, 1=Monday, 2=Tuesday, etc. const dayMap: Record = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, }; // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation const dates: Date[] = []; if (weekdays.length === 0 || rounds <= 0) return dates; // Convert weekday names to day numbers for faster lookup const selectedDayNumbers = new Set(weekdays.map(wd => dayMap[wd])); // If we have both start and end dates, evenly distribute races if (seasonEnd && seasonEnd > seasonStart) { // First, collect all possible race days between start and end // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation const allPossibleDays: Date[] = []; const currentDate = new Date(seasonStart); while (currentDate <= seasonEnd) { const dayOfWeek = currentDate.getDay(); if (selectedDayNumbers.has(dayOfWeek)) { // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation allPossibleDays.push(new Date(currentDate)); } currentDate.setDate(currentDate.getDate() + 1); } // Now evenly distribute the rounds across available days const totalPossible = allPossibleDays.length; if (totalPossible >= rounds) { // Space them evenly const spacing = totalPossible / rounds; for (let i = 0; i < rounds; i++) { const index = Math.min(Math.floor(i * spacing), totalPossible - 1); const chosen = allPossibleDays[index]!; // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation dates.push(chosen); } } else { // Not enough days - use all available // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation dates.push(...allPossibleDays); } return dates; } // Original algorithm: schedule based on frequency const currentDate = new Date(seasonStart); let roundsScheduled = 0; // Generate race dates for up to 2 years to ensure we can schedule all rounds const maxDays = 365 * 2; let daysChecked = 0; while (roundsScheduled < rounds && daysChecked < maxDays) { const dayOfWeek = currentDate.getDay(); const isSelectedDay = selectedDayNumbers.has(dayOfWeek); // Calculate which week this is from the start const daysSinceStart = Math.floor((currentDate.getTime() - seasonStart.getTime()) / (24 * 60 * 60 * 1000)); const currentWeek = Math.floor(daysSinceStart / 7); if (isSelectedDay) { let shouldRace = false; if (frequency === 'weekly') { // Weekly: race every week on selected days shouldRace = true; } else if (frequency === 'everyNWeeks') { // Every 2 weeks: race only on even weeks (0, 2, 4, ...) shouldRace = currentWeek % 2 === 0; } else { // Default to weekly if frequency not set shouldRace = true; } if (shouldRace) { // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation dates.push(new Date(currentDate)); roundsScheduled++; } } currentDate.setDate(currentDate.getDate() + 1); daysChecked++; } return dates; }, [weekdays, frequency, rounds, seasonStart, seasonEnd]); // Helper to check if a date is the season start/end const isSeasonStartDate = useCallback((date: Date) => { return date.getFullYear() === seasonStart.getFullYear() && date.getMonth() === seasonStart.getMonth() && date.getDate() === seasonStart.getDate(); }, [seasonStart]); const isSeasonEndDate = useCallback((date: Date) => { if (!seasonEnd) return false; return date.getFullYear() === seasonEnd.getFullYear() && date.getMonth() === seasonEnd.getMonth() && date.getDate() === seasonEnd.getDate(); }, [seasonEnd]); // Create year view - DYNAMIC: shows months from first race to last race + some buffer const yearView = useMemo(() => { const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ] as const; type DayInfo = { date: Date; isRace: boolean; dayOfMonth: number; isStart: boolean; isEnd: boolean; raceNumber?: number; }; if (raceDates.length === 0) { // No races scheduled - show next 12 months from today const today = new Date(); // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation const view: { month: string; monthIndex: number; year: number; days: DayInfo[] }[] = []; for (let i = 0; i < 12; i++) { const targetMonth = (today.getMonth() + i) % 12; const targetYear = today.getFullYear() + Math.floor((today.getMonth() + i) / 12); const lastDay = new Date(targetYear, targetMonth + 1, 0); // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation const days: DayInfo[] = []; for (let day = 1; day <= lastDay.getDate(); day++) { const date = new Date(targetYear, targetMonth, day); // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation days.push({ date, isRace: false, dayOfMonth: day, isStart: isSeasonStartDate(date), isEnd: isSeasonEndDate(date), }); } // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation view.push({ month: months[targetMonth] ?? '—', monthIndex: targetMonth, year: targetYear, days }); } return view; } // Get the range of months that contain races const firstRaceDate = raceDates[0]!; // Start from first race month, show 12 months total const startMonth = firstRaceDate.getMonth(); const startYear = firstRaceDate.getFullYear(); // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation const view: { month: string; monthIndex: number; year: number; days: DayInfo[] }[] = []; for (let i = 0; i < 12; i++) { const targetMonth = (startMonth + i) % 12; const targetYear = startYear + Math.floor((startMonth + i) / 12); const lastDay = new Date(targetYear, targetMonth + 1, 0); // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation const days: DayInfo[] = []; for (let day = 1; day <= lastDay.getDate(); day++) { const date = new Date(targetYear, targetMonth, day); const raceIndex = raceDates.findIndex(rd => rd.getFullYear() === date.getFullYear() && rd.getMonth() === date.getMonth() && rd.getDate() === date.getDate() ); const isRace = raceIndex >= 0; const raceNumber = isRace ? raceIndex + 1 : undefined; // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation days.push({ date, isRace, dayOfMonth: day, isStart: isSeasonStartDate(date), isEnd: isSeasonEndDate(date), ...(raceNumber !== undefined ? { raceNumber } : {}), }); } // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation view.push({ month: months[targetMonth] ?? '—', monthIndex: targetMonth, year: targetYear, days }); } return view; }, [raceDates, isSeasonStartDate, isSeasonEndDate]); // Calculate season stats const firstRace = raceDates[0]; const lastRace = raceDates[raceDates.length - 1]; const seasonDurationWeeks = firstRace && lastRace ? Math.ceil( (lastRace.getTime() - firstRace.getTime()) / (7 * 24 * 60 * 60 * 1000), ) : 0; return ( Season Calendar {raceDates.length} race{raceDates.length !== 1 ? 's' : ''} scheduled {/* Year grid - 3 columns x 4 rows */} {yearView.map(({ month, monthIndex, year, days }) => { const hasRaces = days.some(d => d.isRace); const raceCount = days.filter(d => d.isRace).length; const uniqueKey = `${year}-${monthIndex}`; return ( {month} {year !== new Date().getFullYear() && {year}} {raceCount > 0 && ( {raceCount} )} {/* Mini calendar grid */} {/* Fill empty days at start - getDay() returns 0 for Sunday, we want Monday first */} {Array.from({ length: (new Date(year, monthIndex, 1).getDay() + 6) % 7 }).map((_, i) => ( ))} {days.map(({ dayOfMonth, isRace, isStart, isEnd, raceNumber }) => ( ))} ); })} {/* Season summary */} {rounds} Rounds {seasonDurationWeeks || '—'} Weeks {firstRace && lastRace ? `${getMonthLabel(firstRace.getMonth())}–${getMonthLabel( lastRace.getMonth(), )}` : '—'} Duration {/* Legend */} Start Race {seasonEnd && ( End )} No race ); } /** Season Stats Overview */ function SeasonStatsPreview({ rounds, weekdays, frequency, weekendTemplate, practiceMin, qualifyingMin, sprintMin, mainRaceMin, }: { rounds: number; weekdays: Weekday[]; frequency: RecurrenceStrategy; weekendTemplate: string; practiceMin: number; qualifyingMin: number; sprintMin?: number; mainRaceMin: number; }) { const hasSprint = weekendTemplate === 'sprintFeature'; const sessionsPerRound = 2 + (hasSprint ? 2 : 1); // practice + quali + race(s) const totalSessions = rounds * sessionsPerRound; const raceMinutesPerRound = (hasSprint ? (sprintMin ?? 0) : 0) + mainRaceMin; const totalRaceMinutes = rounds * raceMinutesPerRound; const totalMinutesPerRound = practiceMin + qualifyingMin + raceMinutesPerRound; // Estimate season duration const racesPerWeek = frequency === 'weekly' ? weekdays.length : weekdays.length / 2; const weeksNeeded = Math.ceil(rounds / Math.max(racesPerWeek, 0.5)); return ( Season Statistics {/* Visual rounds */} {Array.from({ length: Math.min(rounds, 20) }).map((_, i) => ( {i + 1} ))} {rounds > 20 && ( +{rounds - 20} )} Season start Finale {/* Stats grid */} {totalSessions} Total sessions {Math.round(totalRaceMinutes / 60)}h Racing time ~{weeksNeeded} Weeks duration {totalMinutesPerRound} min/race day ); } // ============================================================================ // INLINE EDITABLE ROUNDS COMPONENT // ============================================================================ function InlineEditableRounds({ value, onChange, min, max, }: { value: number; onChange: (value: number) => void; min: number; max: number; }) { const [isEditing, setIsEditing] = useState(false); const [editValue, setEditValue] = useState(value.toString()); const inputRef = useRef(null); useEffect(() => { if (isEditing && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [isEditing]); const handleSubmit = () => { const num = parseInt(editValue, 10); if (!isNaN(num) && num >= min && num <= max) { onChange(num); } else { setEditValue(value.toString()); } setIsEditing(false); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleSubmit(); } else if (e.key === 'Escape') { setEditValue(value.toString()); setIsEditing(false); } }; return ( {isEditing ? ( ) => setEditValue(e.target.value)} onBlur={handleSubmit} onKeyDown={handleKeyDown} w="20" bg="transparent" borderBottom borderWidth="2px" borderColor="border-primary-blue" color="text-white" textAlign="center" weight="bold" // eslint-disable-next-line gridpilot-rules/component-classification className="text-4xl outline-none" /> ) : ( { setEditValue(value.toString()); setIsEditing(true); }} display="flex" alignItems="center" gap={2} color="text-white" hoverTextColor="text-primary-blue" transition group // eslint-disable-next-line gridpilot-rules/component-classification className="text-4xl font-bold" > {value} )} rounds Click to edit onChange(Math.min(value + 1, max))} disabled={value >= max} p={1.5} rounded="sm" bg="bg-iron-gray/60" border borderColor="border-charcoal-outline" hoverBorderColor="border-primary-blue" transition opacity={value >= max ? 0.3 : 1} cursor={value >= max ? 'not-allowed' : 'pointer'} > onChange(Math.max(value - 1, min))} disabled={value <= min} p={1.5} rounded="sm" bg="bg-iron-gray/60" border borderColor="border-charcoal-outline" hoverBorderColor="border-primary-blue" transition opacity={value <= min ? 0.3 : 1} cursor={value <= min ? 'not-allowed' : 'pointer'} > ); } // ============================================================================ // COLLAPSIBLE SECTION COMPONENT // ============================================================================ interface CollapsibleSectionProps { icon: React.ReactNode; title: string; description?: string; defaultOpen?: boolean; children: React.ReactNode; } function CollapsibleSection({ icon, title, description, defaultOpen = false, children, }: CollapsibleSectionProps) { const [isOpen, setIsOpen] = useState(defaultOpen); return ( setIsOpen(!isOpen)} w="full" display="flex" alignItems="center" justifyContent="between" p={4} transition hoverBg="bg-iron-gray/30" > {icon} {title} {description && ( {description} )} {children} ); } // ============================================================================ // MAIN COMPONENT // ============================================================================ export function LeagueTimingsSection({ form, onChange, readOnly, errors, }: LeagueTimingsSectionProps) { const disabled = readOnly || !onChange; const timings = form.timings; const [showAdvanced, setShowAdvanced] = useState(false); const [previewTab, setPreviewTab] = useState<'day' | 'year' | 'stats'>('day'); const updateTimings = ( patch: Partial>, ) => { if (!onChange) return; onChange({ ...form, timings: { ...timings, ...patch, }, }); }; const handleRoundsChange = (value: number) => { if (!onChange) return; updateTimings({ roundsPlanned: value }); }; // Show sprint race field if sprintRaceMinutes is set (will be derived from scoring pattern) const showSprint = typeof timings.sprintRaceMinutes === 'number' && timings.sprintRaceMinutes > 0; const recurrenceStrategy: RecurrenceStrategy = timings.recurrenceStrategy ?? 'weekly'; const weekdays: Weekday[] = (timings.weekdays ?? []) as Weekday[]; const handleWeekdayToggle = (day: Weekday) => { const current = new Set(weekdays); if (current.has(day)) { current.delete(day); } else { current.add(day); } updateTimings({ weekdays: Array.from(current) }); }; // Read-only view if (disabled) { return ( Schedule & timings Planned rounds:{' '} {timings.roundsPlanned ?? '—'} ); } const allWeekdays: { day: Weekday; label: string; short: string }[] = [ { day: 'Mon', label: 'Monday', short: 'Mon' }, { day: 'Tue', label: 'Tuesday', short: 'Tue' }, { day: 'Wed', label: 'Wednesday', short: 'Wed' }, { day: 'Thu', label: 'Thursday', short: 'Thu' }, { day: 'Fri', label: 'Friday', short: 'Fri' }, { day: 'Sat', label: 'Saturday', short: 'Sat' }, { day: 'Sun', label: 'Sunday', short: 'Sun' }, ]; return ( {/* LEFT COLUMN: Configuration */} {/* Session Durations - Collapsible */} } title="Session Durations" description="Configure practice, qualifying, and race lengths" defaultOpen={false} > updateTimings({ practiceMinutes: v })} unitLabel="min" compact /> updateTimings({ qualifyingMinutes: v })} unitLabel="min" compact error={errors?.qualifyingMinutes} /> {showSprint && ( updateTimings({ sprintRaceMinutes: v })} unitLabel="min" compact /> )} updateTimings({ mainRaceMinutes: v })} unitLabel="min" compact error={errors?.mainRaceMinutes} /> {/* Season Length - Collapsible */} } title="Season Length" description={`${timings.roundsPlanned ?? 8} rounds planned`} defaultOpen={false} > Each round is one complete race day. Click the number to edit directly. {/* Race Schedule - Collapsible */} } title="Race Schedule" description="Weekly/bi-weekly and race days" defaultOpen={false} > {/* Frequency */} How often? {[ { id: 'weekly', label: 'Weekly' }, { id: 'everyNWeeks', label: 'Every 2 weeks' }, ].map((opt) => { const isSelected = (opt.id === 'weekly' && recurrenceStrategy === 'weekly') || (opt.id === 'everyNWeeks' && recurrenceStrategy === 'everyNWeeks'); return ( updateTimings({ recurrenceStrategy: opt.id as RecurrenceStrategy, ...(opt.id === 'everyNWeeks' ? { intervalWeeks: 2 } : {}), }) } flexGrow={1} px={3} py={2} rounded="lg" border transition borderColor={isSelected ? 'border-primary-blue' : 'border-charcoal-outline'} bg={isSelected ? 'bg-primary-blue/10' : 'bg-iron-gray/30'} color={isSelected ? 'text-white' : 'text-gray-400'} hoverBorderColor={!isSelected ? 'border-gray-500' : undefined} // eslint-disable-next-line gridpilot-rules/component-classification style={{ fontSize: '12px', fontWeight: 500 }} > {opt.label} ); })} {/* Day selection */} Which days? {weekdays.length === 0 && ( Select at least one )} {allWeekdays.map(({ day, short }) => { const isSelected = weekdays.includes(day); return ( handleWeekdayToggle(day)} flexGrow={1} py={2} rounded="lg" border transition borderColor={isSelected ? 'border-primary-blue' : 'border-charcoal-outline'} bg={isSelected ? 'bg-primary-blue' : 'bg-iron-gray/30'} color={isSelected ? 'text-white' : 'text-gray-500'} shadow={isSelected ? '0_0_8px_rgba(25,140,255,0.4)' : undefined} hoverBorderColor={!isSelected ? 'border-gray-500' : undefined} // eslint-disable-next-line gridpilot-rules/component-classification style={{ fontSize: '10px', fontWeight: 500 }} > {short} ); })} {/* Season Duration - Collapsible */} } title="Season Duration" description="Start/end dates and time settings" defaultOpen={false} > Season Start ) => updateTimings({ seasonStartDate: e.target.value })} // eslint-disable-next-line gridpilot-rules/component-classification className="bg-iron-gray/30" /> Season End ) => updateTimings({ seasonEndDate: e.target.value })} // eslint-disable-next-line gridpilot-rules/component-classification className="bg-iron-gray/30" /> {timings.seasonStartDate && timings.seasonEndDate && ( Races will be evenly distributed between start and end dates on your selected race days. )} Race Start Time ) => updateTimings({ raceStartTime: e.target.value })} // eslint-disable-next-line gridpilot-rules/component-classification className="bg-iron-gray/30" /> {/* Time Zone Selector */} Time Zone {TIME_ZONES.slice(0, 2).map((tz) => { const isSelected = (timings.timezoneId ?? 'UTC') === tz.value; const TzIcon = tz.icon; return ( updateTimings({ timezoneId: tz.value })} display="flex" alignItems="center" gap={2} px={3} py={2.5} rounded="lg" border transition borderColor={isSelected ? (tz.value === 'track' ? 'border-performance-green' : 'border-primary-blue') : 'border-charcoal-outline'} bg={isSelected ? (tz.value === 'track' ? 'bg-performance-green/10' : 'bg-primary-blue/10') : 'bg-iron-gray/30'} shadow={isSelected ? '0_0_12px_rgba(25,140,255,0.2)' : undefined} hoverBorderColor={!isSelected ? 'border-gray-500' : undefined} textAlign="left" > {tz.label} {tz.value === 'track' && ( Adjusts per track )} ); })} {/* More time zones - expandable */} setShowAdvanced(!showAdvanced)} display="flex" alignItems="center" gap={1.5} transition color="text-gray-400" hoverTextColor="text-white" > {showAdvanced ? 'Hide' : 'Show'} more time zones {showAdvanced && ( {TIME_ZONES.slice(2).map((tz) => { const isSelected = (timings.timezoneId ?? 'UTC') === tz.value; const TzIcon = tz.icon; return ( updateTimings({ timezoneId: tz.value })} display="flex" alignItems="center" gap={2} px={3} py={2} rounded="lg" border transition borderColor={isSelected ? 'border-primary-blue' : 'border-charcoal-outline'} bg={isSelected ? 'bg-primary-blue/10' : 'bg-iron-gray/30'} hoverBorderColor={!isSelected ? 'border-gray-500' : undefined} textAlign="left" > {tz.label} ); })} )} {(timings.timezoneId ?? 'UTC') === 'track' ? 'Track Local Time means race times will be displayed in each track\'s local time zone. Great for immersive scheduling!' : 'All race times will be displayed in the selected time zone for consistency.' } {/* RIGHT COLUMN: Live Preview */} {/* Preview header with tabs */} Preview {[ { id: 'day', label: 'Race Day', icon: Play }, { id: 'year', label: 'Calendar', icon: CalendarRange }, { id: 'stats', label: 'Stats', icon: Trophy }, ].map((tab) => ( setPreviewTab(tab.id as typeof previewTab)} display="flex" alignItems="center" gap={1} px={2} py={1} rounded="sm" transition bg={previewTab === tab.id ? 'bg-primary-blue' : undefined} color={previewTab === tab.id ? 'text-white' : 'text-gray-400'} hoverTextColor={previewTab !== tab.id ? 'text-white' : undefined} // eslint-disable-next-line gridpilot-rules/component-classification style={{ fontSize: '10px', fontWeight: 500 }} > {tab.label} ))} {/* Preview content */} {previewTab === 'day' && (() => { const sprintMinutes = showSprint ? timings.sprintRaceMinutes ?? 20 : undefined; return ( ); })()} {previewTab === 'year' && ( )} {previewTab === 'stats' && (() => { const sprintMinutes = showSprint ? timings.sprintRaceMinutes ?? 20 : undefined; return ( ); })()} {/* Helper tip */} Preview updates live as you configure. Check Race Day for session timing, Calendar for the full year view, and Stats for season totals. ); }