Files
gridpilot.gg/apps/website/components/leagues/LeagueDropSection.tsx
2025-12-05 12:47:20 +01:00

200 lines
7.1 KiB
TypeScript

'use client';
import { TrendingDown, Info } from 'lucide-react';
import type { LeagueConfigFormModel } from '@gridpilot/racing/application';
import Input from '@/components/ui/Input';
import SegmentedControl from '@/components/ui/SegmentedControl';
interface LeagueDropSectionProps {
form: LeagueConfigFormModel;
onChange?: (form: LeagueConfigFormModel) => void;
readOnly?: boolean;
}
export function LeagueDropSection({
form,
onChange,
readOnly,
}: LeagueDropSectionProps) {
const disabled = readOnly || !onChange;
const dropPolicy = form.dropPolicy;
const updateDropPolicy = (
patch: Partial<LeagueConfigFormModel['dropPolicy']>,
) => {
if (!onChange) return;
onChange({
...form,
dropPolicy: {
...dropPolicy,
...patch,
},
});
};
const handleStrategyChange = (
strategy: LeagueConfigFormModel['dropPolicy']['strategy'],
) => {
if (strategy === 'none') {
updateDropPolicy({ strategy: 'none', n: undefined });
} else if (strategy === 'bestNResults') {
const n = dropPolicy.n ?? 6;
updateDropPolicy({ strategy: 'bestNResults', n });
} else if (strategy === 'dropWorstN') {
const n = dropPolicy.n ?? 2;
updateDropPolicy({ strategy: 'dropWorstN', n });
}
};
const handleNChange = (value: string) => {
const parsed = parseInt(value, 10);
updateDropPolicy({
n: Number.isNaN(parsed) || parsed <= 0 ? undefined : parsed,
});
};
const getSuggestedN = () => {
const rounds = form.timings.roundsPlanned;
if (!rounds || rounds <= 0) return null;
if (dropPolicy.strategy === 'bestNResults') {
// Suggest keeping 70-80% of rounds
const suggestion = Math.max(1, Math.floor(rounds * 0.75));
return { value: suggestion, explanation: `Keep best ${suggestion} of ${rounds} rounds (75%)` };
} else if (dropPolicy.strategy === 'dropWorstN') {
// Suggest dropping 1-2 rounds for every 8-10 rounds
const suggestion = Math.max(1, Math.floor(rounds / 8));
return { value: suggestion, explanation: `Drop worst ${suggestion} of ${rounds} rounds` };
}
return null;
};
const computeSummary = () => {
if (dropPolicy.strategy === 'none') {
return 'All results will count towards the championship.';
}
if (dropPolicy.strategy === 'bestNResults') {
const n = dropPolicy.n;
if (typeof n === 'number' && n > 0) {
return `Best ${n} results will count; others are ignored.`;
}
return 'Best N results will count; others are ignored.';
}
if (dropPolicy.strategy === 'dropWorstN') {
const n = dropPolicy.n;
if (typeof n === 'number' && n > 0) {
return `Worst ${n} results will be dropped from the standings.`;
}
return 'Worst N results will be dropped from the standings.';
}
return 'All results will count towards the championship.';
};
const currentStrategyValue =
dropPolicy.strategy === 'none'
? 'all'
: dropPolicy.strategy === 'bestNResults'
? 'bestN'
: 'dropWorstN';
const suggestedN = getSuggestedN();
return (
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center gap-2">
<TrendingDown className="w-4 h-4 text-primary-blue" />
<h3 className="text-sm font-semibold text-white">Drop rule</h3>
</div>
<p className="text-xs text-gray-400">
Protect drivers from bad races by dropping worst results or counting only the best ones
</p>
</div>
<div className="space-y-3">
<SegmentedControl
options={[
{ value: 'all', label: 'All count', description: 'Every race matters' },
{ value: 'bestN', label: 'Best N', description: 'Keep best results' },
{ value: 'dropWorstN', label: 'Drop worst N', description: 'Ignore worst results' },
]}
value={currentStrategyValue}
onChange={(value) => {
if (disabled) return;
if (value === 'all') {
handleStrategyChange('none');
} else if (value === 'bestN') {
handleStrategyChange('bestNResults');
} else if (value === 'dropWorstN') {
handleStrategyChange('dropWorstN');
}
}}
/>
{dropPolicy.strategy === 'none' && (
<div className="rounded-lg bg-primary-blue/5 border border-primary-blue/20 p-3">
<p className="text-xs text-gray-300">
<span className="font-medium text-primary-blue">All count:</span> Every race result affects the championship. Best for shorter seasons or when consistency is key.
</p>
</div>
)}
</div>
{(dropPolicy.strategy === 'bestNResults' ||
dropPolicy.strategy === 'dropWorstN') && (
<div className="space-y-3">
{suggestedN && (
<div className="rounded-lg bg-primary-blue/5 border border-primary-blue/20 p-3">
<p className="text-xs text-gray-300 mb-2">
<span className="font-medium text-primary-blue">Suggested:</span> {suggestedN.explanation}
</p>
<button
type="button"
onClick={() => handleNChange(String(suggestedN.value))}
disabled={disabled}
className="px-3 py-1.5 rounded bg-iron-gray border border-charcoal-outline text-xs text-gray-300 hover:border-primary-blue hover:text-primary-blue transition-colors"
>
Use suggested value ({suggestedN.value})
</button>
</div>
)}
<div className="rounded-lg border border-charcoal-outline bg-iron-gray/30 p-4 space-y-3">
<div className="flex items-start gap-2">
<Info className="w-4 h-4 text-primary-blue mt-0.5 shrink-0" />
<div className="flex-1">
<label className="block text-sm font-medium text-gray-300 mb-2">
Number of rounds (N)
</label>
<Input
type="number"
value={
typeof dropPolicy.n === 'number' && dropPolicy.n > 0
? String(dropPolicy.n)
: ''
}
onChange={(e) => handleNChange(e.target.value)}
disabled={disabled}
min={1}
className="max-w-[140px]"
/>
<p className="mt-2 text-xs text-gray-500">
{dropPolicy.strategy === 'bestNResults'
? 'Only your best N results will count towards the championship. Great for long seasons.'
: 'Your worst N results will be excluded from the championship. Helps forgive bad days.'}
</p>
</div>
</div>
</div>
</div>
)}
<div className="rounded-lg border border-charcoal-outline/50 bg-iron-gray/30 p-4">
<div className="flex items-start gap-2">
<Info className="w-4 h-4 text-primary-blue mt-0.5 shrink-0" />
<div className="text-xs text-gray-300">{computeSummary()}</div>
</div>
</div>
</div>
);
}