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

@@ -1,10 +1,9 @@
'use client';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Check, HelpCircle, TrendingDown, X, Zap } from 'lucide-react';
import React, { useEffect, useRef, useState } from 'react';
@@ -83,7 +82,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
if (!isOpen) return null;
return createPortal(
<Box
<Stack
ref={flyoutRef}
position="fixed"
zIndex={50}
@@ -96,7 +95,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ top: position.top, left: position.left, maxHeight: '80vh', overflowY: 'auto' }}
>
<Box
<Stack
display="flex"
alignItems="center"
justifyContent="between"
@@ -112,7 +111,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
</Stack>
<Box
<Stack
as="button"
type="button"
onClick={onClose}
@@ -126,19 +125,19 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
hoverBg="bg-charcoal-outline"
>
<Icon icon={X} size={4} color="text-gray-400" />
</Box>
</Box>
<Box p={4}>
</Stack>
</Stack>
<Stack p={4}>
{children}
</Box>
</Box>,
</Stack>
</Stack>,
document.body
);
}
function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.Ref<HTMLButtonElement> }) {
return (
<Box
<Stack
as="button"
ref={buttonRef}
type="button"
@@ -155,7 +154,7 @@ function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: Re
hoverBg="bg-primary-blue/10"
>
<Icon icon={HelpCircle} size={3.5} />
</Box>
</Stack>
);
}
@@ -174,13 +173,13 @@ function DropRulesMockup() {
const wouldBe = results.reduce((sum, r) => sum + r.pts, 0);
return (
<Box bg="bg-deep-graphite" rounded="lg" p={4}>
<Box display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline/50" opacity={0.5}>
<Stack bg="bg-deep-graphite" rounded="lg" p={4}>
<Stack display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline/50" opacity={0.5}>
<Text size="xs" weight="semibold" color="text-white">Best 4 of 6 Results</Text>
</Box>
<Box display="flex" gap={1} mb={3}>
</Stack>
<Stack display="flex" gap={1} mb={3}>
{results.map((r, i) => (
<Box
<Stack
key={i}
flexGrow={1}
p={2}
@@ -209,14 +208,14 @@ function DropRulesMockup() {
>
{r.pts}
</Text>
</Box>
</Stack>
))}
</Box>
<Box display="flex" justifyContent="between" alignItems="center">
</Stack>
<Stack display="flex" justifyContent="between" alignItems="center">
<Text size="xs" color="text-gray-500">Total counted:</Text>
<Text font="mono" weight="semibold" color="text-performance-green" size="xs">{total} pts</Text>
</Box>
<Box display="flex" justifyContent="between" alignItems="center" mt={1}>
</Stack>
<Stack display="flex" justifyContent="between" alignItems="center" mt={1}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -231,8 +230,8 @@ function DropRulesMockup() {
>
{wouldBe} pts
</Text>
</Box>
</Box>
</Stack>
</Stack>
);
}
@@ -363,18 +362,18 @@ export function LeagueDropSection({
return (
<Stack gap={4}>
{/* Section header */}
<Box display="flex" alignItems="center" gap={3}>
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10">
<Stack display="flex" alignItems="center" gap={3}>
<Stack display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10">
<Icon icon={TrendingDown} size={5} color="text-primary-blue" />
</Box>
<Box flexGrow={1}>
<Box display="flex" alignItems="center" gap={2}>
</Stack>
<Stack flexGrow={1}>
<Stack display="flex" alignItems="center" gap={2}>
<Heading level={3}>Drop Rules</Heading>
<InfoButton buttonRef={dropInfoRef} onClick={() => setShowDropFlyout(true)} />
</Box>
</Stack>
<Text size="xs" color="text-gray-500">Protect from bad races</Text>
</Box>
</Box>
</Stack>
</Stack>
{/* Drop Rules Flyout */}
<InfoFlyout
@@ -389,7 +388,7 @@ export function LeagueDropSection({
This protects against mechanical failures, bad luck, or occasional poor performances.
</Text>
<Box>
<Stack>
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
@@ -401,7 +400,7 @@ export function LeagueDropSection({
Visual Example
</Text>
<DropRulesMockup />
</Box>
</Stack>
<Stack gap={2}>
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
@@ -414,9 +413,9 @@ export function LeagueDropSection({
Drop Strategies
</Text>
<Stack gap={2}>
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Text size="base"></Text>
<Box>
<Stack>
<Text size="xs" weight="medium" color="text-white" block
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -429,11 +428,11 @@ export function LeagueDropSection({
>
Every race affects standings. Best for short seasons.
</Text>
</Box>
</Box>
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
</Stack>
</Stack>
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Text size="base">🏆</Text>
<Box>
<Stack>
<Text size="xs" weight="medium" color="text-white" block
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -446,11 +445,11 @@ export function LeagueDropSection({
>
Only your top N races count. Extra races are optional.
</Text>
</Box>
</Box>
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
</Stack>
</Stack>
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Text size="base">🗑</Text>
<Box>
<Stack>
<Text size="xs" weight="medium" color="text-white" block
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -463,15 +462,15 @@ export function LeagueDropSection({
>
Exclude your N worst results. Forgives bad days.
</Text>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
<Box rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20" p={3}>
<Box display="flex" alignItems="start" gap={2}>
<Stack rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20" p={3}>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Zap} size={3.5} color="text-primary-blue" flexShrink={0} mt={0.5} />
<Box>
<Stack>
<Text size="xs" color="text-gray-400"
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '11px' }}
@@ -479,20 +478,20 @@ export function LeagueDropSection({
<Text weight="medium" color="text-primary-blue">Pro tip:</Text> For an 8-round season,
&quot;Best 6&quot; or &quot;Drop 2&quot; are popular choices.
</Text>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
</InfoFlyout>
{/* Strategy buttons + N stepper inline */}
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
<Stack display="flex" flexWrap="wrap" alignItems="center" gap={2}>
{DROP_OPTIONS.map((option) => {
const isSelected = dropPolicy.strategy === option.value;
const ruleInfo = DROP_RULE_INFO[option.value];
return (
<Box key={option.value} display="flex" alignItems="center" position="relative">
<Box
<Stack key={option.value} display="flex" alignItems="center" position="relative">
<Stack
as="button"
type="button"
disabled={disabled}
@@ -516,7 +515,7 @@ export function LeagueDropSection({
style={{ borderRightWidth: 0, borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
>
{/* Radio indicator */}
<Box
<Stack
display="flex"
h="4"
w="4"
@@ -530,16 +529,16 @@ export function LeagueDropSection({
transition
>
{isSelected && <Icon icon={Check} size={2.5} color="text-white" />}
</Box>
</Stack>
<Text size="sm">{option.emoji}</Text>
<Text size="sm" weight="medium" color={isSelected ? 'text-white' : 'text-gray-400'}>
{option.label}
</Text>
</Box>
</Stack>
{/* Info button - separate from main button */}
<Box
<Stack
as="button"
ref={(el: HTMLButtonElement | null) => { dropRuleRefs.current[option.value] = el; }}
type="button"
@@ -569,7 +568,7 @@ export function LeagueDropSection({
// eslint-disable-next-line gridpilot-rules/component-classification
className="hover:text-primary-blue transition-colors"
/>
</Box>
</Stack>
{/* Drop Rule Info Flyout */}
<InfoFlyout
@@ -594,18 +593,18 @@ export function LeagueDropSection({
</Text>
<Stack gap={1.5}>
{ruleInfo.details.map((detail, idx) => (
<Box key={idx} display="flex" alignItems="start" gap={2}>
<Stack key={idx} display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3} color="text-performance-green" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">{detail}</Text>
</Box>
</Stack>
))}
</Stack>
</Stack>
<Box rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30" p={3}>
<Box display="flex" alignItems="center" gap={2}>
<Stack rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30" p={3}>
<Stack display="flex" alignItems="center" gap={2}>
<Text size="base">{option.emoji}</Text>
<Box>
<Stack>
<Text size="xs" color="text-gray-400" block
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -613,20 +612,20 @@ export function LeagueDropSection({
Example
</Text>
<Text size="xs" weight="medium" color="text-white" block>{ruleInfo.example}</Text>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
</InfoFlyout>
</Box>
</Stack>
);
})}
{/* N Stepper - only show when needed */}
{needsN && (
<Box display="flex" alignItems="center" gap={1} ml={2}>
<Stack display="flex" alignItems="center" gap={1} ml={2}>
<Text size="xs" color="text-gray-500" mr={1}>N =</Text>
<Box
<Stack
as="button"
type="button"
disabled={disabled || (dropPolicy.n ?? 1) <= 1}
@@ -647,11 +646,11 @@ export function LeagueDropSection({
opacity={disabled || (dropPolicy.n ?? 1) <= 1 ? 0.4 : 1}
>
</Box>
<Box display="flex" h="7" w="10" alignItems="center" justifyContent="center" rounded="md" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/50">
</Stack>
<Stack display="flex" h="7" w="10" alignItems="center" justifyContent="center" rounded="md" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/50">
<Text size="sm" weight="semibold" color="text-white">{dropPolicy.n ?? 1}</Text>
</Box>
<Box
</Stack>
<Stack
as="button"
type="button"
disabled={disabled}
@@ -672,10 +671,10 @@ export function LeagueDropSection({
opacity={disabled ? 0.4 : 1}
>
+
</Box>
</Box>
</Stack>
</Stack>
)}
</Box>
</Stack>
{/* Explanation text */}
<Text size="xs" color="text-gray-500" block>