Files
gridpilot.gg/apps/website/components/leagues/LeagueDropSection.tsx
2026-01-19 18:01:30 +01:00

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 '@/components/shared/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>
);
}