website refactor

This commit is contained in:
2026-01-15 17:12:24 +01:00
parent c3b308e960
commit f035cfe7ce
468 changed files with 24378 additions and 17324 deletions

View File

@@ -1,14 +1,14 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { Trophy, Award, Check, Zap, Settings, Globe, Medal, Plus, Minus, RotateCcw, HelpCircle, X, LucideIcon } from 'lucide-react';
import { Trophy, Award, Check, Zap, Settings, Globe, Medal, Plus, Minus, RotateCcw, HelpCircle, X } from 'lucide-react';
import { createPortal } from 'react-dom';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationViewModel';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Grid } from '@/ui/Grid';
import { Surface } from '@/ui/Surface';
@@ -102,16 +102,18 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
ref={flyoutRef}
position="fixed"
zIndex={50}
width="380px"
backgroundColor="iron-gray"
w="380px"
bg="bg-iron-gray"
border
borderColor="charcoal-outline"
borderColor="border-charcoal-outline"
rounded="xl"
// eslint-disable-next-line gridpilot-rules/component-classification
className="max-h-[80vh] overflow-y-auto shadow-2xl animate-fade-in"
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ top: position.top, left: position.left }}
>
{/* Header */}
<Box display="flex" align="center" justify="between" padding={4} border borderBottom borderColor="charcoal-outline" position="sticky" top={0} backgroundColor="iron-gray" zIndex={10}>
<Box display="flex" alignItems="center" justifyContent="between" p={4} borderBottom borderColor="border-charcoal-outline" position="sticky" top="0" bg="bg-iron-gray" zIndex={10}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
@@ -120,14 +122,14 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
variant="ghost"
size="sm"
onClick={onClose}
// eslint-disable-next-line gridpilot-rules/component-classification
className="h-6 w-6 p-0"
icon={<Icon icon={X} size={4} color="text-gray-400" />}
>
{null}
<Icon icon={X} size={4} color="text-gray-400" />
</Button>
</Box>
{/* Content */}
<Box padding={4}>
<Box p={4}>
{children}
</Box>
</Box>,
@@ -142,10 +144,10 @@ function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: Re
variant="ghost"
size="sm"
onClick={onClick}
// eslint-disable-next-line gridpilot-rules/component-classification
className="h-5 w-5 p-0 rounded-full text-gray-500 hover:text-primary-blue hover:bg-primary-blue/10"
icon={<Icon icon={HelpCircle} size={3.5} />}
>
{null}
<Icon icon={HelpCircle} size={3.5} />
</Button>
);
}
@@ -164,32 +166,64 @@ function PointsSystemMockup() {
];
return (
<Surface variant="dark" rounded="lg" padding={4}>
<Surface variant="dark" rounded="lg" p={4}>
<Stack gap={3}>
<Box display="flex" align="center" justify="between" className="text-[10px] text-gray-500 uppercase tracking-wide px-1">
<Text>Position</Text>
<Text>Points</Text>
<Box display="flex" alignItems="center" justifyContent="between" px={1}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
>
Position
</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
>
Points
</Text>
</Box>
{positions.map((p) => (
<Stack key={p.pos} direction="row" align="center" gap={3}>
<Box width={8} height={8} rounded="lg" className={p.color} display="flex" center>
<Text size="sm" weight="bold" className={p.pos <= 3 ? 'text-deep-graphite' : 'text-gray-400'}>P{p.pos}</Text>
<Box w="8" h="8" rounded="lg" bg={p.color} display="flex" alignItems="center" justifyContent="center">
<Text size="sm" weight="bold" color={p.pos <= 3 ? 'text-deep-graphite' : 'text-gray-400'}>P{p.pos}</Text>
</Box>
<Box flex={1} height={2} backgroundColor="charcoal-outline" rounded="full" className="overflow-hidden opacity-50">
<Box flexGrow={1} h="2" bg="bg-charcoal-outline" rounded="full" overflow="hidden" opacity={0.5}>
<Box
height="full"
className="bg-gradient-to-r from-primary-blue to-neon-aqua rounded-full"
h="full"
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
rounded="full"
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ width: `${(p.pts / 25) * 100}%` }}
/>
</Box>
<Box width={8} textAlign="right">
<Box w="8" textAlign="right">
<Text size="sm" font="mono" weight="semibold" color="text-white">{p.pts}</Text>
</Box>
</Stack>
))}
<Box display="flex" align="center" justify="center" gap={1} pt={2} className="text-[10px] text-gray-500">
<Text>...</Text>
<Text>down to P10 = 1 point</Text>
<Box display="flex" alignItems="center" justifyContent="center" gap={1} pt={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
>
...
</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
>
down to P10 = 1 point
</Text>
</Box>
</Stack>
</Surface>
@@ -204,15 +238,22 @@ function BonusPointsMockup() {
];
return (
<Surface variant="dark" rounded="lg" padding={4}>
<Surface variant="dark" rounded="lg" p={4}>
<Stack gap={2}>
{bonuses.map((b, i) => (
<Surface key={i} variant="muted" border rounded="lg" padding={2}>
<Surface key={i} variant="muted" border rounded="lg" p={2}>
<Stack direction="row" align="center" gap={3}>
<Text size="xl">{b.emoji}</Text>
<Box flex={1}>
<Box flexGrow={1}>
<Text size="xs" weight="medium" color="text-white" block>{b.label}</Text>
<Text className="text-[10px] text-gray-500" block>{b.desc}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
block
>
{b.desc}
</Text>
</Box>
<Text size="sm" font="mono" weight="semibold" color="text-performance-green">{b.pts}</Text>
</Stack>
@@ -231,80 +272,48 @@ function ChampionshipMockup() {
];
return (
<Surface variant="dark" rounded="lg" padding={4}>
<Box display="flex" align="center" gap={2} mb={3} pb={2} border borderBottom borderColor="charcoal-outline" className="opacity-50">
<Surface variant="dark" rounded="lg" p={4}>
<Box display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline" opacity={0.5}>
<Icon icon={Trophy} size={4} color="text-yellow-500" />
<Text size="xs" weight="semibold" color="text-white">Driver Championship</Text>
</Box>
<Stack gap={2}>
{standings.map((s) => (
<Stack key={s.pos} direction="row" align="center" gap={2}>
<Box width={6} height={6} rounded="full" display="flex" center className={s.pos === 1 ? 'bg-yellow-500 text-deep-graphite' : 'bg-charcoal-outline text-gray-400'}>
<Text className="text-[10px]" weight="bold">{s.pos}</Text>
<Box w="6" h="6" rounded="full" display="flex" alignItems="center" justifyContent="center" bg={s.pos === 1 ? 'bg-yellow-500' : 'bg-charcoal-outline'} color={s.pos === 1 ? 'text-deep-graphite' : 'text-gray-400'}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
weight="bold"
>
{s.pos}
</Text>
</Box>
<Box flex={1}>
<Text size="xs" color="text-white" className="truncate" block>{s.name}</Text>
<Box flexGrow={1}>
<Text size="xs" color="text-white" truncate block>{s.name}</Text>
</Box>
<Text size="xs" font="mono" weight="semibold" color="text-white">{s.pts}</Text>
{s.delta && (
<Text className="text-[10px] font-mono text-gray-500">{s.delta}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
font="mono"
color="text-gray-500"
>
{s.delta}
</Text>
)}
</Stack>
))}
</Stack>
<Box mt={3} pt={2} border borderTop borderColor="charcoal-outline" className="text-[10px] text-gray-500 opacity-50" textAlign="center">
<Text>Points accumulated across all races</Text>
</Box>
</Surface>
);
}
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 (
<Surface variant="dark" rounded="lg" padding={4}>
<Box mb={3} pb={2} border borderBottom borderColor="charcoal-outline" className="opacity-50">
<Text size="xs" weight="semibold" color="text-white">Best 4 of 6 Results</Text>
</Box>
<Stack direction="row" gap={1} mb={3}>
{results.map((r, i) => (
<Box
key={i}
flex={1}
padding={2}
rounded="lg"
textAlign="center"
border
borderColor={r.dropped ? 'charcoal-outline' : 'performance-green'}
backgroundColor={r.dropped ? 'transparent' : 'performance-green'}
opacity={r.dropped ? 0.5 : 0.1}
className="transition-all"
>
<Text className="text-[9px] text-gray-500" block>{r.round}</Text>
<Text size="xs" font="mono" weight="semibold" color={r.dropped ? 'text-gray-500' : 'text-white'} className={r.dropped ? 'line-through' : ''} block>
{r.pts}
</Text>
</Box>
))}
</Stack>
<Box display="flex" justify="between" align="center">
<Text size="xs" color="text-gray-500">Total counted:</Text>
<Text font="mono" weight="semibold" color="text-performance-green">{total} pts</Text>
</Box>
<Box display="flex" justify="between" align="center" mt={1} className="text-[10px] text-gray-500">
<Text>Without drops:</Text>
<Text font="mono">{wouldBe} pts</Text>
<Box mt={3} pt={2} borderTop borderColor="border-charcoal-outline" opacity={0.5} textAlign="center">
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
>
Points accumulated across all races
</Text>
</Box>
</Surface>
);
@@ -418,7 +427,10 @@ export function LeagueScoringSection({
}
return (
<Grid cols={2} gap={6} className="lg:grid-cols-[minmax(0,2fr)_minmax(0,1.4fr)]">
<Grid cols={2} gap={6}
// eslint-disable-next-line gridpilot-rules/component-classification
className="lg:grid-cols-[minmax(0,2fr)_minmax(0,1.4fr)]"
>
{patternPanel}
{championshipsPanel}
</Grid>
@@ -560,10 +572,10 @@ export function ScoringPatternSection({
<Stack gap={5}>
{/* Section header */}
<Stack direction="row" align="center" gap={3}>
<Box width={10} height={10} display="flex" center rounded="xl" backgroundColor="primary-blue" opacity={0.1}>
<Box w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
<Icon icon={Trophy} size={5} color="text-primary-blue" />
</Box>
<Box flex={1}>
<Box flexGrow={1}>
<Stack direction="row" align="center" gap={2}>
<Heading level={3}>Points System</Heading>
<InfoButton buttonRef={pointsInfoRef} onClick={() => setShowPointsFlyout(true)} />
@@ -586,16 +598,32 @@ export function ScoringPatternSection({
</Text>
<Box>
<Box mb={2} className="text-[10px] text-gray-500 uppercase tracking-wide">
<Text>Example: F1-Style Points</Text>
<Box mb={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
>
Example: F1-Style Points
</Text>
</Box>
<PointsSystemMockup />
</Box>
<Surface variant="muted" border rounded="lg" padding={3}>
<Surface variant="muted" border rounded="lg" p={3}>
<Stack direction="row" align="start" gap={2}>
<Icon icon={Zap} size={3.5} color="text-primary-blue" className="mt-0.5" />
<Text className="text-[11px] text-gray-400">
<Icon icon={Zap} size={3.5} color="text-primary-blue"
// eslint-disable-next-line gridpilot-rules/component-classification
className="mt-0.5"
/>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '11px' }}
color="text-gray-400"
>
<Text weight="medium" color="text-primary-blue">Pro tip:</Text> Sprint formats
award points in both races, typically with reduced points for the sprint.
</Text>
@@ -605,11 +633,14 @@ export function ScoringPatternSection({
</InfoFlyout>
{/* Two-column layout: Presets | Custom */}
<Grid cols={2} gap={4} className="lg:grid-cols-[1fr_auto]">
<Grid cols={2} gap={4}
// eslint-disable-next-line gridpilot-rules/component-classification
className="lg:grid-cols-[1fr_auto]"
>
{/* Preset options */}
<Stack gap={2}>
{presets.length === 0 ? (
<Box padding={4} border borderStyle="dashed" borderColor="charcoal-outline" rounded="lg">
<Box p={4} border borderStyle="dashed" borderColor="border-charcoal-outline" rounded="lg">
<Text size="sm" color="text-gray-400">Loading presets...</Text>
</Box>
) : (
@@ -622,6 +653,7 @@ export function ScoringPatternSection({
variant="ghost"
onClick={() => onChangePatternId?.(preset.id)}
disabled={disabled}
// eslint-disable-next-line gridpilot-rules/component-classification
className={`
w-full flex items-center gap-3 p-3 rounded-xl border-2 text-left transition-all duration-200 h-auto
${isSelected
@@ -632,15 +664,17 @@ export function ScoringPatternSection({
>
{/* Radio indicator */}
<Box
width={5}
height={5}
w="5"
h="5"
display="flex"
center
alignItems="center"
justifyContent="center"
rounded="full"
border
borderColor={isSelected ? 'primary-blue' : 'gray-500'}
backgroundColor={isSelected ? 'primary-blue' : 'transparent'}
className="shrink-0 transition-colors"
borderColor={isSelected ? 'border-primary-blue' : 'border-gray-500'}
bg={isSelected ? 'bg-primary-blue' : 'transparent'}
transition
flexShrink={0}
>
{isSelected && <Icon icon={Check} size={3} color="text-white" />}
</Box>
@@ -649,14 +683,20 @@ export function ScoringPatternSection({
<Text size="xl">{getPresetEmoji(preset)}</Text>
{/* Text */}
<Box flex={1} className="min-w-0">
<Box flexGrow={1}
// eslint-disable-next-line gridpilot-rules/component-classification
className="min-w-0"
>
<Text size="sm" weight="medium" color="text-white" block>{preset.name}</Text>
<Text size="xs" color="text-gray-500" block>{getPresetDescription(preset)}</Text>
</Box>
{/* Bonus badge */}
{preset.bonusSummary && (
<Box className="hidden sm:inline-flex items-center gap-1 px-2 py-1 rounded-full bg-charcoal-outline/30 text-[10px] text-gray-400">
<Box
// eslint-disable-next-line gridpilot-rules/component-classification
className="hidden sm:inline-flex items-center gap-1 px-2 py-1 rounded-full bg-charcoal-outline/30 text-[10px] text-gray-400"
>
<Icon icon={Zap} size={3} />
<Text>{preset.bonusSummary}</Text>
</Box>
@@ -664,7 +704,7 @@ export function ScoringPatternSection({
{/* Info button */}
<Box
ref={(el: any) => { presetInfoRefs.current[preset.id] = el; }}
ref={(el: HTMLElement | null) => { presetInfoRefs.current[preset.id] = el; }}
role="button"
tabIndex={0}
onClick={(e: React.MouseEvent) => {
@@ -678,7 +718,17 @@ export function ScoringPatternSection({
setActivePresetFlyout(activePresetFlyout === preset.id ? null : preset.id);
}
}}
className="flex h-6 w-6 items-center justify-center rounded-full text-gray-500 hover:text-primary-blue hover:bg-primary-blue/10 transition-colors shrink-0"
display="flex"
h="6"
w="6"
alignItems="center"
justifyContent="center"
rounded="full"
color="text-gray-500"
hoverTextColor="text-primary-blue"
hoverBg="bg-primary-blue/10"
transition
flexShrink={0}
>
<Icon icon={HelpCircle} size={3.5} />
</Box>
@@ -695,13 +745,28 @@ export function ScoringPatternSection({
<Text size="xs" color="text-gray-400">{presetInfo.description}</Text>
<Stack gap={2}>
<Box className="text-[10px] text-gray-500 uppercase tracking-wide">
<Text>Key Features</Text>
<Box>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
>
Key Features
</Text>
</Box>
<Box as="ul" className="space-y-1.5">
<Box as="ul"
// eslint-disable-next-line gridpilot-rules/component-classification
className="space-y-1.5"
>
{presetInfo.details.map((detail, idx) => (
<Box as="li" key={idx} display="flex" align="start" gap={2}>
<Icon icon={Check} size={3} color="text-performance-green" className="mt-0.5" />
<Box as="li" key={idx} display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3} color="text-performance-green"
// eslint-disable-next-line gridpilot-rules/component-classification
className="mt-0.5"
/>
<Text size="xs" color="text-gray-400">{detail}</Text>
</Box>
))}
@@ -709,10 +774,17 @@ export function ScoringPatternSection({
</Stack>
{preset.bonusSummary && (
<Surface variant="muted" border rounded="lg" padding={3}>
<Surface variant="muted" border rounded="lg" p={3}>
<Stack direction="row" align="start" gap={2}>
<Icon icon={Zap} size={3.5} color="text-primary-blue" className="mt-0.5" />
<Text className="text-[11px] text-gray-400">
<Icon icon={Zap} size={3.5} color="text-primary-blue"
// eslint-disable-next-line gridpilot-rules/component-classification
className="mt-0.5"
/>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '11px' }}
color="text-gray-400"
>
<Text weight="medium" color="text-primary-blue">Bonus points:</Text> {preset.bonusSummary}
</Text>
</Stack>
@@ -727,11 +799,15 @@ export function ScoringPatternSection({
</Stack>
{/* Custom scoring option */}
<Box width="full" className="lg:w-48">
<Box w="full"
// eslint-disable-next-line gridpilot-rules/component-classification
className="lg:w-48"
>
<Button
variant="ghost"
onClick={onToggleCustomScoring}
disabled={!onToggleCustomScoring || readOnly}
// eslint-disable-next-line gridpilot-rules/component-classification
className={`
w-full h-full min-h-[100px] flex flex-col items-center justify-center gap-2 p-4 rounded-xl border-2 transition-all duration-200
${isCustom
@@ -741,25 +817,40 @@ export function ScoringPatternSection({
`}
>
<Box
width={10}
height={10}
w="10"
h="10"
display="flex"
center
alignItems="center"
justifyContent="center"
rounded="xl"
backgroundColor={isCustom ? 'primary-blue' : 'charcoal-outline'}
bg={isCustom ? 'bg-primary-blue' : 'bg-charcoal-outline'}
opacity={isCustom ? 0.2 : 0.3}
className="transition-colors"
transition
>
<Icon icon={Settings} size={5} color={isCustom ? 'text-primary-blue' : 'text-gray-500'} />
</Box>
<Box textAlign="center">
<Text size="sm" weight="medium" color={isCustom ? 'text-white' : 'text-gray-400'} block>Custom</Text>
<Text className="text-[10px] text-gray-500" block>Define your own</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
block
>
Define your own
</Text>
</Box>
{isCustom && (
<Box display="flex" align="center" gap={1} px={2} py={0.5} rounded="full" backgroundColor="primary-blue" opacity={0.2}>
<Box display="flex" alignItems="center" gap={1} px={2} py={0.5} rounded="full" bg="bg-primary-blue" opacity={0.2}>
<Icon icon={Check} size={2.5} color="text-primary-blue" />
<Text className="text-[10px]" weight="medium" color="text-primary-blue">Active</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
weight="medium"
color="text-primary-blue"
>
Active
</Text>
</Box>
)}
</Button>
@@ -773,10 +864,10 @@ export function ScoringPatternSection({
{/* Custom scoring editor - inline, no placeholder */}
{isCustom && (
<Surface variant="muted" border rounded="xl" padding={4}>
<Surface variant="muted" border rounded="xl" p={4}>
<Stack gap={4}>
{/* Header with reset button */}
<Box display="flex" align="center" justify="between">
<Box display="flex" alignItems="center" justifyContent="between">
<Stack direction="row" align="center" gap={2}>
<Icon icon={Settings} size={4} color="text-primary-blue" />
<Text size="sm" weight="medium" color="text-white">Custom Points Table</Text>
@@ -797,16 +888,32 @@ export function ScoringPatternSection({
</Text>
<Box>
<Box mb={2} className="text-[10px] text-gray-500 uppercase tracking-wide">
<Text>Available Bonuses</Text>
<Box mb={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
>
Available Bonuses
</Text>
</Box>
<BonusPointsMockup />
</Box>
<Surface variant="muted" border rounded="lg" padding={3}>
<Surface variant="muted" border rounded="lg" p={3}>
<Stack direction="row" align="start" gap={2}>
<Icon icon={Zap} size={3.5} color="text-primary-blue" className="mt-0.5" />
<Text className="text-[11px] text-gray-400">
<Icon icon={Zap} size={3.5} color="text-primary-blue"
// eslint-disable-next-line gridpilot-rules/component-classification
className="mt-0.5"
/>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '11px' }}
color="text-gray-400"
>
<Text weight="medium" color="text-primary-blue">Example:</Text> A driver finishing
P1 with pole and fastest lap would earn 25 + 1 + 1 = 27 points.
</Text>
@@ -820,16 +927,19 @@ export function ScoringPatternSection({
size="sm"
onClick={resetToDefaults}
disabled={readOnly}
// eslint-disable-next-line gridpilot-rules/component-classification
className="h-auto py-1 px-2 text-[10px] text-gray-400 hover:text-primary-blue hover:bg-primary-blue/10"
icon={<Icon icon={RotateCcw} size={3} />}
>
Reset
<Stack direction="row" align="center" gap={1}>
<Icon icon={RotateCcw} size={3} />
<Text>Reset</Text>
</Stack>
</Button>
</Box>
{/* Race position points */}
<Stack gap={2}>
<Box display="flex" align="center" justify="between">
<Box display="flex" alignItems="center" justifyContent="between">
<Text size="xs" color="text-gray-400">Finish position points</Text>
<Stack direction="row" align="center" gap={1}>
<Button
@@ -837,54 +947,70 @@ export function ScoringPatternSection({
size="sm"
onClick={removePosition}
disabled={readOnly || customPoints.racePoints.length <= 3}
// eslint-disable-next-line gridpilot-rules/component-classification
className="h-5 w-5 p-0"
icon={<Icon icon={Minus} size={3} />}
>
{null}
<Icon icon={Minus} size={3} />
</Button>
<Button
variant="secondary"
size="sm"
onClick={addPosition}
disabled={readOnly || customPoints.racePoints.length >= 20}
// eslint-disable-next-line gridpilot-rules/component-classification
className="h-5 w-5 p-0"
icon={<Icon icon={Plus} size={3} />}
>
{null}
<Icon icon={Plus} size={3} />
</Button>
</Stack>
</Box>
<Stack direction="row" wrap gap={1}>
<Box display="flex" flexWrap="wrap" gap={1}>
{customPoints.racePoints.map((pts, idx) => (
<Stack key={idx} align="center">
<Text className="text-[9px] text-gray-500" mb={0.5}>P{idx + 1}</Text>
<Stack direction="row" align="center" gap={0.5}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '9px' }}
color="text-gray-500"
mb={0.5}
>
P{idx + 1}
</Text>
<Box display="flex" alignItems="center">
<Button
variant="secondary"
size="sm"
onClick={() => updateRacePoints(idx, -1)}
disabled={readOnly || pts <= 0}
// eslint-disable-next-line gridpilot-rules/component-classification
className="h-5 w-4 p-0 rounded-r-none text-[10px]"
>
</Button>
<Box width={6} height={5} display="flex" center backgroundColor="deep-graphite" border borderTop borderBottom borderColor="charcoal-outline">
<Text className="text-[10px]" weight="medium" color="text-white">{pts}</Text>
<Box w="6" h="5" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
weight="medium"
color="text-white"
>
{pts}
</Text>
</Box>
<Button
variant="secondary"
size="sm"
onClick={() => updateRacePoints(idx, 1)}
disabled={readOnly}
// eslint-disable-next-line gridpilot-rules/component-classification
className="h-5 w-4 p-0 rounded-l-none text-[10px]"
>
+
</Button>
</Stack>
</Box>
</Stack>
))}
</Stack>
</Box>
</Stack>
{/* Bonus points */}
@@ -895,18 +1021,25 @@ export function ScoringPatternSection({
{ key: 'leaderLapPoints' as const, label: 'Led lap', emoji: '🥇' },
].map((bonus) => (
<Stack key={bonus.key} align="center" gap={1}>
<Text className="text-[10px] text-gray-500">{bonus.emoji} {bonus.label}</Text>
<Stack direction="row" align="center" gap={0.5}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
>
{bonus.emoji} {bonus.label}
</Text>
<Box display="flex" alignItems="center">
<Button
variant="secondary"
size="sm"
onClick={() => updateBonus(bonus.key, -1)}
disabled={readOnly || customPoints[bonus.key] <= 0}
// eslint-disable-next-line gridpilot-rules/component-classification
className="h-6 w-5 p-0 rounded-r-none"
>
</Button>
<Box width={7} height={6} display="flex" center backgroundColor="deep-graphite" border borderTop borderBottom borderColor="charcoal-outline">
<Box w="7" h="6" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
<Text size="xs" weight="medium" color="text-white">{customPoints[bonus.key]}</Text>
</Box>
<Button
@@ -914,11 +1047,12 @@ export function ScoringPatternSection({
size="sm"
onClick={() => updateBonus(bonus.key, 1)}
disabled={readOnly}
// eslint-disable-next-line gridpilot-rules/component-classification
className="h-6 w-5 p-0 rounded-l-none"
>
+
</Button>
</Stack>
</Box>
</Stack>
))}
</Grid>
@@ -1040,10 +1174,10 @@ export function ChampionshipsSection({
<Stack gap={4}>
{/* Section header */}
<Stack direction="row" align="center" gap={3}>
<Box width={10} height={10} display="flex" center rounded="xl" backgroundColor="primary-blue" opacity={0.1}>
<Box w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
<Icon icon={Award} size={5} color="text-primary-blue" />
</Box>
<Box flex={1}>
<Box flexGrow={1}>
<Stack direction="row" align="center" gap={2}>
<Heading level={3}>Championships</Heading>
<InfoButton buttonRef={champInfoRef} onClick={() => setShowChampFlyout(true)} />
@@ -1066,15 +1200,33 @@ export function ChampionshipsSection({
</Text>
<Box>
<Box mb={2} className="text-[10px] text-gray-500 uppercase tracking-wide">
<Text>Live Standings Example</Text>
<Box mb={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
>
Live Standings Example
</Text>
</Box>
<ChampionshipMockup />
</Box>
<Stack gap={2}>
<Box className="text-[10px] text-gray-500 uppercase tracking-wide">
<Text>Championship Types</Text>
<Box>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
>
Championship Types
</Text>
</Box>
<Grid cols={2} gap={2}>
{[
@@ -1083,12 +1235,27 @@ export function ChampionshipsSection({
{ icon: Globe, label: 'Nations', desc: 'By country' },
{ icon: Medal, label: 'Trophy', desc: 'Special class' },
].map((t, i) => (
<Surface key={i} variant="dark" border rounded="lg" padding={2}>
<Surface key={i} variant="dark" border rounded="lg" p={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={t.icon} size={3.5} color="text-primary-blue" />
<Box>
<Text className="text-[10px]" weight="medium" color="text-white" block>{t.label}</Text>
<Text className="text-[9px] text-gray-500" block>{t.desc}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
weight="medium"
color="text-white"
block
>
{t.label}
</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '9px' }}
color="text-gray-500"
block
>
{t.desc}
</Text>
</Box>
</Stack>
</Surface>
@@ -1110,6 +1277,7 @@ export function ChampionshipsSection({
variant="ghost"
disabled={disabled || !champ.available}
onClick={() => champ.available && updateChampionship(champ.key, !champ.enabled)}
// eslint-disable-next-line gridpilot-rules/component-classification
className={`
w-full flex items-center gap-3 p-3 rounded-xl border-2 text-left transition-all duration-200 h-auto
${isEnabled
@@ -1122,34 +1290,54 @@ export function ChampionshipsSection({
>
{/* Toggle indicator */}
<Box
width={5}
height={5}
w="5"
h="5"
display="flex"
center
alignItems="center"
justifyContent="center"
rounded="md"
backgroundColor={isEnabled ? 'primary-blue' : 'charcoal-outline'}
bg={isEnabled ? 'bg-primary-blue' : 'bg-charcoal-outline'}
opacity={isEnabled ? 1 : 0.5}
className="shrink-0 transition-colors"
transition
flexShrink={0}
>
{isEnabled && <Icon icon={Check} size={3} color="text-white" />}
</Box>
{/* Icon */}
<Icon icon={champ.icon} size={4} color={isEnabled ? 'text-primary-blue' : 'text-gray-500'} className="shrink-0" />
<Icon icon={champ.icon} size={4} color={isEnabled ? 'text-primary-blue' : 'text-gray-500'}
// eslint-disable-next-line gridpilot-rules/component-classification
className="shrink-0"
/>
{/* Text */}
<Box flex={1} className="min-w-0">
<Text className={`text-xs font-medium truncate ${isEnabled ? 'text-white' : 'text-gray-400'}`} block>
<Box flexGrow={1}
// eslint-disable-next-line gridpilot-rules/component-classification
className="min-w-0"
>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
className={`text-xs font-medium truncate ${isEnabled ? 'text-white' : 'text-gray-400'}`}
block
>
{champ.label}
</Text>
{!champ.available && champ.unavailableHint && (
<Text className="text-[10px] text-warning-amber/70" block>{champ.unavailableHint}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-warning-amber"
opacity={0.7}
block
>
{champ.unavailableHint}
</Text>
)}
</Box>
{/* Info button */}
<Box
ref={(el: any) => { champItemRefs.current[champ.key] = el; }}
ref={(el: HTMLElement | null) => { champItemRefs.current[champ.key] = el; }}
role="button"
tabIndex={0}
onClick={(e: React.MouseEvent) => {
@@ -1163,7 +1351,17 @@ export function ChampionshipsSection({
setActiveChampFlyout(activeChampFlyout === champ.key ? null : champ.key);
}
}}
className="flex h-5 w-5 items-center justify-center rounded-full text-gray-500 hover:text-primary-blue hover:bg-primary-blue/10 transition-colors shrink-0"
display="flex"
h="5"
w="5"
alignItems="center"
justifyContent="center"
rounded="full"
color="text-gray-500"
hoverTextColor="text-primary-blue"
hoverBg="bg-primary-blue/10"
transition
flexShrink={0}
>
<Icon icon={HelpCircle} size={3} />
</Box>
@@ -1175,19 +1373,34 @@ export function ChampionshipsSection({
isOpen={activeChampFlyout === champ.key}
onClose={() => setActiveChampFlyout(null)}
title={champInfo.title}
anchorRef={{ current: champItemRefs.current[champ.key] ?? champInfoRef.current }}
anchorRef={{ current: (champItemRefs.current[champ.key] as HTMLElement | null) ?? champInfoRef.current }}
>
<Stack gap={4}>
<Text size="xs" color="text-gray-400">{champInfo.description}</Text>
<Stack gap={2}>
<Box className="text-[10px] text-gray-500 uppercase tracking-wide">
<Text>How It Works</Text>
<Box>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
>
How It Works
</Text>
</Box>
<Box as="ul" className="space-y-1.5">
<Box as="ul"
// eslint-disable-next-line gridpilot-rules/component-classification
className="space-y-1.5"
>
{champInfo.details.map((detail, idx) => (
<Box as="li" key={idx} display="flex" align="start" gap={2}>
<Icon icon={Check} size={3} color="text-performance-green" className="mt-0.5" />
<Box as="li" key={idx} display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3} color="text-performance-green"
// eslint-disable-next-line gridpilot-rules/component-classification
className="mt-0.5"
/>
<Text size="xs" color="text-gray-400">{detail}</Text>
</Box>
))}
@@ -1195,10 +1408,20 @@ export function ChampionshipsSection({
</Stack>
{!champ.available && (
<Surface variant="muted" border rounded="lg" padding={3} className="bg-warning-amber/5 border-warning-amber/20">
<Surface variant="muted" border rounded="lg" p={3}
// eslint-disable-next-line gridpilot-rules/component-classification
className="bg-warning-amber/5 border-warning-amber/20"
>
<Stack direction="row" align="start" gap={2}>
<Icon icon={Zap} size={3.5} color="text-warning-amber" className="mt-0.5" />
<Text className="text-[11px] text-gray-400">
<Icon icon={Zap} size={3.5} color="text-warning-amber"
// eslint-disable-next-line gridpilot-rules/component-classification
className="mt-0.5"
/>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '11px' }}
color="text-gray-400"
>
<Text weight="medium" color="text-warning-amber">Note:</Text> {champ.unavailableHint}. Switch to Teams mode to enable this championship.
</Text>
</Stack>