Files
gridpilot.gg/apps/website/components/races/RaceResultsTable.tsx
2026-01-19 01:24:07 +01:00

244 lines
8.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { Badge } from '@/ui/Badge';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table';
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 { PositionBadge } from '@/ui/ResultRow';
import { AlertTriangle, ExternalLink } from 'lucide-react';
import React, { ReactNode } from 'react';
type PenaltyTypeDTO =
| 'time_penalty'
| 'grid_penalty'
| 'points_deduction'
| 'disqualification'
| 'warning'
| 'license_points'
| string;
interface ResultDTO {
id: string;
raceId: string;
driverId: string;
position: number;
fastestLap: number;
incidents: number;
startPosition: number;
getPositionChange(): number;
}
interface DriverDTO {
id: string;
name: string;
}
interface PenaltyData {
driverId: string;
type: PenaltyTypeDTO;
value?: number;
}
interface RaceResultsTableProps {
results: ResultDTO[];
drivers: DriverDTO[];
pointsSystem: Record<number, number>;
fastestLapTime?: number | undefined;
penalties?: PenaltyData[];
currentDriverId?: string | undefined;
isAdmin?: boolean;
onPenaltyClick?: (driver: DriverDTO) => void;
penaltyButtonRenderer?: (driver: DriverDTO) => ReactNode;
}
export function RaceResultsTable({
results,
drivers,
pointsSystem,
fastestLapTime,
penalties = [],
currentDriverId,
isAdmin = false,
penaltyButtonRenderer,
}: RaceResultsTableProps) {
const getDriver = (driverId: string): DriverDTO | undefined => {
return drivers.find((d) => d.id === driverId);
};
const getDriverName = (driverId: string): string => {
const driver = getDriver(driverId);
return driver?.name || 'Unknown Driver';
};
const getDriverPenalties = (driverId: string): PenaltyData[] => {
return penalties.filter((p) => p.driverId === driverId);
};
const getPenaltyDescription = (penalty: PenaltyData): string => {
const descriptions: Record<string, string> = {
time_penalty: `+${penalty.value}s time penalty`,
grid_penalty: `${penalty.value} place grid penalty`,
points_deduction: `-${penalty.value} points`,
disqualification: 'Disqualified',
warning: 'Warning',
license_points: `${penalty.value} license points`,
};
return descriptions[penalty.type] || penalty.type;
};
const formatLapTime = (seconds: number): string => {
const minutes = Math.floor(seconds / 60);
const secs = (seconds % 60).toFixed(3);
return `${minutes}:${secs.padStart(6, '0')}`;
};
const getPoints = (position: number): number => {
return pointsSystem[position] || 0;
};
const getPositionChangeVariant = (change: number): 'success' | 'warning' | 'low' => {
if (change > 0) return 'success';
if (change < 0) return 'warning';
return 'low';
};
const getPositionChangeText = (change: number): string => {
if (change > 0) return `+${change}`;
if (change < 0) return `${change}`;
return '0';
};
if (results.length === 0) {
return (
<Box textAlign="center" paddingY={8}>
<Text variant="low">No results available</Text>
</Box>
);
}
return (
<Box overflowX="auto">
<Table>
<TableHead>
<TableRow>
<TableHeader>Pos</TableHeader>
<TableHeader>Driver</TableHeader>
<TableHeader>Fastest Lap</TableHeader>
<TableHeader>Incidents</TableHeader>
<TableHeader>Points</TableHeader>
<TableHeader>+/-</TableHeader>
<TableHeader>Penalties</TableHeader>
{isAdmin && <TableHeader textAlign="right">Actions</TableHeader>}
</TableRow>
</TableHead>
<TableBody>
{results.map((result) => {
const positionChange = result.getPositionChange();
const isFastestLap =
typeof fastestLapTime === 'number' && result.fastestLap === fastestLapTime;
const driverPenalties = getDriverPenalties(result.driverId);
const driver = getDriver(result.driverId);
const isCurrentUser = currentDriverId === result.driverId;
return (
<TableRow
key={result.id}
variant={isCurrentUser ? 'highlight' : 'default'}
>
<TableCell>
<PositionBadge position={result.position} />
</TableCell>
<TableCell>
<Group gap={3}>
{driver ? (
<React.Fragment>
<Surface
variant={isCurrentUser ? 'gradient-blue' : 'muted'}
rounded="full"
width="2rem"
height="2rem"
display="flex"
alignItems="center"
justifyContent="center"
flexShrink={0}
border={isCurrentUser ? '2px solid var(--ui-color-intent-primary)' : true}
>
<Text
size="sm"
weight="bold"
variant={isCurrentUser ? 'primary' : 'low'}
>
{driver.name.charAt(0)}
</Text>
</Surface>
<Link
href={`/drivers/${driver.id}`}
variant={isCurrentUser ? 'primary' : 'inherit'}
>
<Group gap={1.5}>
<Text weight={isCurrentUser ? 'semibold' : 'normal'}>{driver.name}</Text>
{isCurrentUser && (
<Badge variant="primary" size="sm">You</Badge>
)}
<Icon icon={ExternalLink} size={3} intent="low" />
</Group>
</Link>
</React.Fragment>
) : (
<Text variant="high">{getDriverName(result.driverId)}</Text>
)}
</Group>
</TableCell>
<TableCell>
<Text variant={isFastestLap ? 'success' : 'high'} weight={isFastestLap ? 'medium' : 'normal'}>
{formatLapTime(result.fastestLap)}
</Text>
</TableCell>
<TableCell>
<Text variant={result.incidents > 0 ? 'warning' : 'high'}>
{result.incidents}×
</Text>
</TableCell>
<TableCell>
<Text variant="high" weight="medium">
{getPoints(result.position)}
</Text>
</TableCell>
<TableCell>
<Text weight="medium" variant={getPositionChangeVariant(positionChange)}>
{getPositionChangeText(positionChange)}
</Text>
</TableCell>
<TableCell>
{driverPenalties.length > 0 ? (
<Stack gap={1}>
{driverPenalties.map((penalty, idx) => (
<Group key={idx} gap={1.5}>
<Icon icon={AlertTriangle} size={3} intent="critical" />
<Text size="xs" variant="critical">{getPenaltyDescription(penalty)}</Text>
</Group>
))}
</Stack>
) : (
<Text variant="low"></Text>
)}
</TableCell>
{isAdmin && (
<TableCell textAlign="right">
{driver && penaltyButtonRenderer && penaltyButtonRenderer(driver)}
</TableCell>
)}
</TableRow>
);
})}
</TableBody>
</Table>
</Box>
);
}