website refactor

This commit is contained in:
2026-01-18 16:18:18 +01:00
parent 0b301feb61
commit 13567d51af
329 changed files with 4701 additions and 4750 deletions

View File

@@ -6,9 +6,8 @@ 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 { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Grid } from '@/ui/Grid';
import { Surface } from '@/ui/Surface';
@@ -98,7 +97,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
if (!isOpen) return null;
return createPortal(
<Box
<Stack
ref={flyoutRef}
position="fixed"
zIndex={50}
@@ -113,7 +112,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
style={{ top: position.top, left: position.left }}
>
{/* Header */}
<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 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>
@@ -127,12 +126,12 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
>
<Icon icon={X} size={4} color="text-gray-400" />
</Button>
</Box>
</Stack>
{/* Content */}
<Box p={4}>
<Stack p={4}>
{children}
</Box>
</Box>,
</Stack>
</Stack>,
document.body
);
}
@@ -168,7 +167,7 @@ function PointsSystemMockup() {
return (
<Surface variant="dark" rounded="lg" p={4}>
<Stack gap={3}>
<Box display="flex" alignItems="center" justifyContent="between" px={1}>
<Stack display="flex" alignItems="center" justifyContent="between" px={1}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -189,27 +188,27 @@ function PointsSystemMockup() {
>
Points
</Text>
</Box>
</Stack>
{positions.map((p) => (
<Stack key={p.pos} direction="row" align="center" gap={3}>
<Box w="8" h="8" rounded="lg" bg={p.color} display="flex" alignItems="center" justifyContent="center">
<Stack 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 flexGrow={1} h="2" bg="bg-charcoal-outline" rounded="full" overflow="hidden" opacity={0.5}>
<Box
</Stack>
<Stack flexGrow={1} h="2" bg="bg-charcoal-outline" rounded="full" overflow="hidden" opacity={0.5}>
<Stack
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 w="8" textAlign="right">
</Stack>
<Stack w="8" textAlign="right">
<Text size="sm" font="mono" weight="semibold" color="text-white">{p.pts}</Text>
</Box>
</Stack>
</Stack>
))}
<Box display="flex" alignItems="center" justifyContent="center" gap={1} pt={2}>
<Stack display="flex" alignItems="center" justifyContent="center" gap={1} pt={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -224,7 +223,7 @@ function PointsSystemMockup() {
>
down to P10 = 1 point
</Text>
</Box>
</Stack>
</Stack>
</Surface>
);
@@ -244,7 +243,7 @@ function BonusPointsMockup() {
<Surface key={i} variant="muted" border rounded="lg" p={2}>
<Stack direction="row" align="center" gap={3}>
<Text size="xl">{b.emoji}</Text>
<Box flexGrow={1}>
<Stack flexGrow={1}>
<Text size="xs" weight="medium" color="text-white" block>{b.label}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -254,7 +253,7 @@ function BonusPointsMockup() {
>
{b.desc}
</Text>
</Box>
</Stack>
<Text size="sm" font="mono" weight="semibold" color="text-performance-green">{b.pts}</Text>
</Stack>
</Surface>
@@ -273,14 +272,14 @@ function ChampionshipMockup() {
return (
<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}>
<Stack 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>
<Stack gap={2}>
{standings.map((s) => (
<Stack key={s.pos} direction="row" align="center" gap={2}>
<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'}>
<Stack 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' }}
@@ -288,10 +287,10 @@ function ChampionshipMockup() {
>
{s.pos}
</Text>
</Box>
<Box flexGrow={1}>
</Stack>
<Stack flexGrow={1}>
<Text size="xs" color="text-white" truncate block>{s.name}</Text>
</Box>
</Stack>
<Text size="xs" font="mono" weight="semibold" color="text-white">{s.pts}</Text>
{s.delta && (
<Text
@@ -306,7 +305,7 @@ function ChampionshipMockup() {
</Stack>
))}
</Stack>
<Box mt={3} pt={2} borderTop borderColor="border-charcoal-outline" opacity={0.5} textAlign="center">
<Stack 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' }}
@@ -314,7 +313,7 @@ function ChampionshipMockup() {
>
Points accumulated across all races
</Text>
</Box>
</Stack>
</Surface>
);
}
@@ -419,11 +418,11 @@ export function LeagueScoringSection({
const championshipsPanel = <ChampionshipsSection {...championshipsProps} />;
if (patternOnly) {
return <Box>{patternPanel}</Box>;
return <Stack>{patternPanel}</Stack>;
}
if (championshipsOnly) {
return <Box>{championshipsPanel}</Box>;
return <Stack>{championshipsPanel}</Stack>;
}
return (
@@ -572,16 +571,16 @@ export function ScoringPatternSection({
<Stack gap={5}>
{/* Section header */}
<Stack direction="row" align="center" gap={3}>
<Box w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
<Stack 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 flexGrow={1}>
</Stack>
<Stack flexGrow={1}>
<Stack direction="row" align="center" gap={2}>
<Heading level={3}>Points System</Heading>
<InfoButton buttonRef={pointsInfoRef} onClick={() => setShowPointsFlyout(true)} />
</Stack>
<Text size="xs" color="text-gray-500">Choose how points are awarded</Text>
</Box>
</Stack>
</Stack>
{/* Points System Flyout */}
@@ -597,8 +596,8 @@ export function ScoringPatternSection({
which accumulate across the season to determine championship standings.
</Text>
<Box>
<Box mb={2}>
<Stack>
<Stack mb={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -609,9 +608,9 @@ export function ScoringPatternSection({
>
Example: F1-Style Points
</Text>
</Box>
</Stack>
<PointsSystemMockup />
</Box>
</Stack>
<Surface variant="muted" border rounded="lg" p={3}>
<Stack direction="row" align="start" gap={2}>
@@ -640,15 +639,15 @@ export function ScoringPatternSection({
{/* Preset options */}
<Stack gap={2}>
{presets.length === 0 ? (
<Box p={4} border borderStyle="dashed" borderColor="border-charcoal-outline" rounded="lg">
<Stack p={4} border borderStyle="dashed" borderColor="border-charcoal-outline" rounded="lg">
<Text size="sm" color="text-gray-400">Loading presets...</Text>
</Box>
</Stack>
) : (
presets.map((preset) => {
const isSelected = !isCustom && scoring.patternId === preset.id;
const presetInfo = getPresetInfoContent(preset.name);
return (
<Box key={preset.id} position="relative">
<Stack key={preset.id} position="relative">
<Button
variant="ghost"
onClick={() => onChangePatternId?.(preset.id)}
@@ -663,7 +662,7 @@ export function ScoringPatternSection({
`}
>
{/* Radio indicator */}
<Box
<Stack
w="5"
h="5"
display="flex"
@@ -677,33 +676,33 @@ export function ScoringPatternSection({
flexShrink={0}
>
{isSelected && <Icon icon={Check} size={3} color="text-white" />}
</Box>
</Stack>
{/* Emoji */}
<Text size="xl">{getPresetEmoji(preset)}</Text>
{/* Text */}
<Box flexGrow={1}
<Stack 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>
</Stack>
{/* Bonus badge */}
{preset.bonusSummary && (
<Box
<Stack
// 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>
</Stack>
)}
{/* Info button */}
<Box
<Stack
ref={(el: HTMLElement | null) => { presetInfoRefs.current[preset.id] = el; }}
role="button"
tabIndex={0}
@@ -731,7 +730,7 @@ export function ScoringPatternSection({
flexShrink={0}
>
<Icon icon={HelpCircle} size={3.5} />
</Box>
</Stack>
</Button>
{/* Preset Info Flyout */}
@@ -745,7 +744,7 @@ export function ScoringPatternSection({
<Text size="xs" color="text-gray-400">{presetInfo.description}</Text>
<Stack gap={2}>
<Box>
<Stack>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -756,21 +755,21 @@ export function ScoringPatternSection({
>
Key Features
</Text>
</Box>
<Box as="ul"
</Stack>
<Stack 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" alignItems="start" gap={2}>
<Stack 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>
</Stack>
))}
</Box>
</Stack>
</Stack>
{preset.bonusSummary && (
@@ -792,14 +791,14 @@ export function ScoringPatternSection({
)}
</Stack>
</InfoFlyout>
</Box>
</Stack>
);
})
)}
</Stack>
{/* Custom scoring option */}
<Box w="full"
<Stack w="full"
// eslint-disable-next-line gridpilot-rules/component-classification
className="lg:w-48"
>
@@ -816,7 +815,7 @@ export function ScoringPatternSection({
}
`}
>
<Box
<Stack
w="10"
h="10"
display="flex"
@@ -828,8 +827,8 @@ export function ScoringPatternSection({
transition
>
<Icon icon={Settings} size={5} color={isCustom ? 'text-primary-blue' : 'text-gray-500'} />
</Box>
<Box textAlign="center">
</Stack>
<Stack textAlign="center">
<Text size="sm" weight="medium" color={isCustom ? 'text-white' : 'text-gray-400'} block>Custom</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -839,9 +838,9 @@ export function ScoringPatternSection({
>
Define your own
</Text>
</Box>
</Stack>
{isCustom && (
<Box display="flex" alignItems="center" gap={1} px={2} py={0.5} rounded="full" bg="bg-primary-blue" opacity={0.2}>
<Stack 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
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -851,10 +850,10 @@ export function ScoringPatternSection({
>
Active
</Text>
</Box>
</Stack>
)}
</Button>
</Box>
</Stack>
</Grid>
{/* Error message */}
@@ -867,7 +866,7 @@ export function ScoringPatternSection({
<Surface variant="muted" border rounded="xl" p={4}>
<Stack gap={4}>
{/* Header with reset button */}
<Box display="flex" alignItems="center" justifyContent="between">
<Stack 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>
@@ -887,8 +886,8 @@ export function ScoringPatternSection({
They add strategic depth and excitement to your championship.
</Text>
<Box>
<Box mb={2}>
<Stack>
<Stack mb={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -899,9 +898,9 @@ export function ScoringPatternSection({
>
Available Bonuses
</Text>
</Box>
</Stack>
<BonusPointsMockup />
</Box>
</Stack>
<Surface variant="muted" border rounded="lg" p={3}>
<Stack direction="row" align="start" gap={2}>
@@ -935,11 +934,11 @@ export function ScoringPatternSection({
<Text>Reset</Text>
</Stack>
</Button>
</Box>
</Stack>
{/* Race position points */}
<Stack gap={2}>
<Box display="flex" alignItems="center" justifyContent="between">
<Stack 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
@@ -963,9 +962,9 @@ export function ScoringPatternSection({
<Icon icon={Plus} size={3} />
</Button>
</Stack>
</Box>
</Stack>
<Box display="flex" flexWrap="wrap" gap={1}>
<Stack display="flex" flexWrap="wrap" gap={1}>
{customPoints.racePoints.map((pts, idx) => (
<Stack key={idx} align="center">
<Text
@@ -976,7 +975,7 @@ export function ScoringPatternSection({
>
P{idx + 1}
</Text>
<Box display="flex" alignItems="center">
<Stack display="flex" alignItems="center">
<Button
variant="secondary"
size="sm"
@@ -987,7 +986,7 @@ export function ScoringPatternSection({
>
</Button>
<Box w="6" h="5" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
<Stack 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' }}
@@ -996,7 +995,7 @@ export function ScoringPatternSection({
>
{pts}
</Text>
</Box>
</Stack>
<Button
variant="secondary"
size="sm"
@@ -1007,10 +1006,10 @@ export function ScoringPatternSection({
>
+
</Button>
</Box>
</Stack>
</Stack>
))}
</Box>
</Stack>
</Stack>
{/* Bonus points */}
@@ -1028,7 +1027,7 @@ export function ScoringPatternSection({
>
{bonus.emoji} {bonus.label}
</Text>
<Box display="flex" alignItems="center">
<Stack display="flex" alignItems="center">
<Button
variant="secondary"
size="sm"
@@ -1039,9 +1038,9 @@ export function ScoringPatternSection({
>
</Button>
<Box w="7" h="6" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
<Stack 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>
</Stack>
<Button
variant="secondary"
size="sm"
@@ -1052,7 +1051,7 @@ export function ScoringPatternSection({
>
+
</Button>
</Box>
</Stack>
</Stack>
))}
</Grid>
@@ -1174,16 +1173,16 @@ export function ChampionshipsSection({
<Stack gap={4}>
{/* Section header */}
<Stack direction="row" align="center" gap={3}>
<Box w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
<Stack 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 flexGrow={1}>
</Stack>
<Stack flexGrow={1}>
<Stack direction="row" align="center" gap={2}>
<Heading level={3}>Championships</Heading>
<InfoButton buttonRef={champInfoRef} onClick={() => setShowChampFlyout(true)} />
</Stack>
<Text size="xs" color="text-gray-500">What standings to track</Text>
</Box>
</Stack>
</Stack>
{/* Championships Flyout */}
@@ -1199,8 +1198,8 @@ export function ChampionshipsSection({
championship types to run different competitions simultaneously.
</Text>
<Box>
<Box mb={2}>
<Stack>
<Stack mb={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -1211,12 +1210,12 @@ export function ChampionshipsSection({
>
Live Standings Example
</Text>
</Box>
</Stack>
<ChampionshipMockup />
</Box>
</Stack>
<Stack gap={2}>
<Box>
<Stack>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -1227,7 +1226,7 @@ export function ChampionshipsSection({
>
Championship Types
</Text>
</Box>
</Stack>
<Grid cols={2} gap={2}>
{[
{ icon: Trophy, label: 'Driver', desc: 'Individual points' },
@@ -1238,7 +1237,7 @@ export function ChampionshipsSection({
<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>
<Stack>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -1256,7 +1255,7 @@ export function ChampionshipsSection({
>
{t.desc}
</Text>
</Box>
</Stack>
</Stack>
</Surface>
))}
@@ -1272,7 +1271,7 @@ export function ChampionshipsSection({
const champInfo = CHAMPIONSHIP_INFO[champ.key];
return (
<Box key={champ.key} position="relative">
<Stack key={champ.key} position="relative">
<Button
variant="ghost"
disabled={disabled || !champ.available}
@@ -1289,7 +1288,7 @@ export function ChampionshipsSection({
`}
>
{/* Toggle indicator */}
<Box
<Stack
w="5"
h="5"
display="flex"
@@ -1302,7 +1301,7 @@ export function ChampionshipsSection({
flexShrink={0}
>
{isEnabled && <Icon icon={Check} size={3} color="text-white" />}
</Box>
</Stack>
{/* Icon */}
<Icon icon={champ.icon} size={4} color={isEnabled ? 'text-primary-blue' : 'text-gray-500'}
@@ -1311,7 +1310,7 @@ export function ChampionshipsSection({
/>
{/* Text */}
<Box flexGrow={1}
<Stack flexGrow={1}
// eslint-disable-next-line gridpilot-rules/component-classification
className="min-w-0"
>
@@ -1333,10 +1332,10 @@ export function ChampionshipsSection({
{champ.unavailableHint}
</Text>
)}
</Box>
</Stack>
{/* Info button */}
<Box
<Stack
ref={(el: HTMLElement | null) => { champItemRefs.current[champ.key] = el; }}
role="button"
tabIndex={0}
@@ -1364,7 +1363,7 @@ export function ChampionshipsSection({
flexShrink={0}
>
<Icon icon={HelpCircle} size={3} />
</Box>
</Stack>
</Button>
{/* Championship Item Info Flyout */}
@@ -1379,7 +1378,7 @@ export function ChampionshipsSection({
<Text size="xs" color="text-gray-400">{champInfo.description}</Text>
<Stack gap={2}>
<Box>
<Stack>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -1390,21 +1389,21 @@ export function ChampionshipsSection({
>
How It Works
</Text>
</Box>
<Box as="ul"
</Stack>
<Stack 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" alignItems="start" gap={2}>
<Stack 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>
</Stack>
))}
</Box>
</Stack>
</Stack>
{!champ.available && (
@@ -1430,7 +1429,7 @@ export function ChampionshipsSection({
</Stack>
</InfoFlyout>
)}
</Box>
</Stack>
);
})}
</Grid>