website refactor
This commit is contained in:
@@ -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,
|
||||
"Best 6" or "Drop 2" 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>
|
||||
|
||||
Reference in New Issue
Block a user