182 lines
5.1 KiB
TypeScript
182 lines
5.1 KiB
TypeScript
'use client';
|
|
|
|
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { Text } from '@/ui/Text';
|
|
import { Box } from '@/ui/Box';
|
|
import { Group } from '@/ui/Group';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Surface } from '@/ui/Surface';
|
|
import { InfoFlyout } from '@/ui/InfoFlyout';
|
|
import { Stepper } from '@/ui/Stepper';
|
|
import { Button } from '@/ui/Button';
|
|
import { IconButton } from '@/ui/IconButton';
|
|
import { Check, HelpCircle, TrendingDown, Zap } from 'lucide-react';
|
|
import React, { useRef, useState } from 'react';
|
|
|
|
interface LeagueDropSectionProps {
|
|
form: LeagueConfigFormModel;
|
|
onChange?: (form: LeagueConfigFormModel) => void;
|
|
readOnly?: boolean;
|
|
}
|
|
|
|
type DropStrategy = 'none' | 'bestNResults' | 'dropWorstN';
|
|
|
|
const DROP_OPTIONS: Array<{
|
|
value: DropStrategy;
|
|
label: string;
|
|
emoji: string;
|
|
description: string;
|
|
defaultN?: number;
|
|
}> = [
|
|
{
|
|
value: 'none',
|
|
label: 'All count',
|
|
emoji: '✓',
|
|
description: 'Every race counts',
|
|
},
|
|
{
|
|
value: 'bestNResults',
|
|
label: 'Best N',
|
|
emoji: '🏆',
|
|
description: 'Only best results',
|
|
defaultN: 6,
|
|
},
|
|
{
|
|
value: 'dropWorstN',
|
|
label: 'Drop worst',
|
|
emoji: '🗑️',
|
|
description: 'Exclude worst races',
|
|
defaultN: 2,
|
|
},
|
|
];
|
|
|
|
export function LeagueDropSection({
|
|
form,
|
|
onChange,
|
|
readOnly,
|
|
}: LeagueDropSectionProps) {
|
|
const disabled = readOnly || !onChange;
|
|
const dropPolicy = form.dropPolicy || { strategy: 'none' as const };
|
|
const [showDropFlyout, setShowDropFlyout] = useState(false);
|
|
const dropInfoRef = useRef<HTMLButtonElement>(null!);
|
|
|
|
const handleStrategyChange = (strategy: DropStrategy) => {
|
|
if (disabled || !onChange) return;
|
|
|
|
const option = DROP_OPTIONS.find((o) => o.value === strategy);
|
|
const next: LeagueConfigFormModel = {
|
|
...form,
|
|
dropPolicy:
|
|
strategy === 'none'
|
|
? {
|
|
strategy,
|
|
}
|
|
: {
|
|
strategy,
|
|
n: dropPolicy.n ?? option?.defaultN ?? 1,
|
|
},
|
|
};
|
|
onChange(next);
|
|
};
|
|
|
|
const handleNChange = (newValue: number) => {
|
|
if (disabled || !onChange || dropPolicy.strategy === 'none') return;
|
|
onChange({
|
|
...form,
|
|
dropPolicy: {
|
|
...dropPolicy,
|
|
n: newValue,
|
|
},
|
|
});
|
|
};
|
|
|
|
const needsN = dropPolicy.strategy !== 'none';
|
|
|
|
return (
|
|
<Stack gap={4}>
|
|
{/* Section header */}
|
|
<Group gap={3}>
|
|
<Surface
|
|
display="flex"
|
|
width="2.5rem"
|
|
height="2.5rem"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
rounded="md"
|
|
bg="rgba(25, 140, 255, 0.1)"
|
|
>
|
|
<Icon icon={TrendingDown} size={5} intent="primary" />
|
|
</Surface>
|
|
<Stack flex={1} gap={0}>
|
|
<Group gap={2}>
|
|
<Heading level={3}>Drop Rules</Heading>
|
|
<IconButton
|
|
ref={dropInfoRef}
|
|
icon={HelpCircle}
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => setShowDropFlyout(true)}
|
|
title="Help"
|
|
/>
|
|
</Group>
|
|
<Text size="xs" variant="low">Protect from bad races</Text>
|
|
</Stack>
|
|
</Group>
|
|
|
|
<InfoFlyout
|
|
isOpen={showDropFlyout}
|
|
onClose={() => setShowDropFlyout(false)}
|
|
title="Drop Rules Explained"
|
|
anchorRef={dropInfoRef}
|
|
>
|
|
<Text size="xs" variant="low" block>
|
|
Drop rules allow drivers to exclude their worst results from championship calculations.
|
|
This protects against mechanical failures, bad luck, or occasional poor performances.
|
|
</Text>
|
|
</InfoFlyout>
|
|
|
|
{/* Strategy buttons + N stepper inline */}
|
|
<Group gap={2} wrap>
|
|
{DROP_OPTIONS.map((option) => {
|
|
const isSelected = dropPolicy.strategy === option.value;
|
|
return (
|
|
<Button
|
|
key={option.value}
|
|
variant={isSelected ? 'primary' : 'secondary'}
|
|
size="sm"
|
|
onClick={() => handleStrategyChange(option.value)}
|
|
disabled={disabled}
|
|
>
|
|
<Group gap={2}>
|
|
{isSelected && <Icon icon={Check} size={3} />}
|
|
<Text as="span">{option.emoji}</Text>
|
|
<Text as="span">{option.label}</Text>
|
|
</Group>
|
|
</Button>
|
|
);
|
|
})}
|
|
|
|
{needsN && (
|
|
<Box marginLeft={2}>
|
|
<Stepper
|
|
value={dropPolicy.n ?? 1}
|
|
onChange={handleNChange}
|
|
label="N ="
|
|
disabled={disabled}
|
|
/>
|
|
</Box>
|
|
)}
|
|
</Group>
|
|
|
|
{/* Explanation text */}
|
|
<Text size="xs" variant="low" block>
|
|
{dropPolicy.strategy === 'none' && 'Every race result affects the championship standings.'}
|
|
{dropPolicy.strategy === 'bestNResults' && `Only your best ${dropPolicy.n ?? 1} results will count.`}
|
|
{dropPolicy.strategy === 'dropWorstN' && `Your worst ${dropPolicy.n ?? 1} results will be excluded.`}
|
|
</Text>
|
|
</Stack>
|
|
);
|
|
}
|