Files
gridpilot.gg/apps/website/components/races/RaceResultsTable.tsx
2026-01-18 13:26:35 +01:00

265 lines
9.0 KiB
TypeScript
Raw 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.
import { AlertTriangle, ExternalLink } from 'lucide-react';
import { ReactNode } from 'react';
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table';
import { Text } from '@/ui/Text';
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 getPositionChangeColor = (change: number): string => {
if (change > 0) return 'text-performance-green';
if (change < 0) return 'text-warning-amber';
return 'text-gray-500';
};
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" py={8}>
<Text color="text-gray-400">No results available</Text>
</Box>
);
}
return (
<Box overflow="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 className="text-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>
<Box
display="inline-flex"
center
width="8"
height="8"
rounded="lg"
weight="bold"
size="sm"
bg={
result.position === 1
? 'bg-yellow-500/20'
: result.position === 2
? 'bg-gray-400/20'
: result.position === 3
? 'bg-amber-600/20'
: undefined
}
color={
result.position === 1
? 'text-yellow-400'
: result.position === 2
? 'text-gray-300'
: result.position === 3
? 'text-amber-500'
: 'text-white'
}
>
{result.position}
</Box>
</TableCell>
<TableCell>
<Stack direction="row" align="center" gap={3}>
{driver ? (
<>
<Box
width="8"
height="8"
rounded="full"
display="flex"
center
size="sm"
weight="bold"
flexShrink={0}
bg={isCurrentUser ? 'bg-primary-blue/30' : 'bg-iron-gray'}
color={isCurrentUser ? 'text-primary-blue' : 'text-gray-400'}
className={isCurrentUser ? 'ring-2 ring-primary-blue/50' : ''}
>
{driver.name.charAt(0)}
</Box>
<Link
href={`/drivers/${driver.id}`}
variant="ghost"
className={`group ${isCurrentUser ? 'text-primary-blue font-semibold' : 'text-white'}`}
>
<Text className="group-hover:underline">{driver.name}</Text>
{isCurrentUser && (
<Box as="span" px={1.5} py={0.5} ml={1.5} bg="bg-primary-blue" color="text-white" rounded="full" uppercase style={{ fontSize: '10px', fontWeight: 'bold' }}>
You
</Box>
)}
<Icon icon={ExternalLink} size={3} className="ml-1.5 opacity-0 group-hover:opacity-100 transition-opacity" />
</Link>
</>
) : (
<Text color="text-white">{getDriverName(result.driverId)}</Text>
)}
</Stack>
</TableCell>
<TableCell>
<Text color={isFastestLap ? 'text-performance-green' : 'text-white'} weight={isFastestLap ? 'medium' : 'normal'}>
{formatLapTime(result.fastestLap)}
</Text>
</TableCell>
<TableCell>
<Text color={result.incidents > 0 ? 'text-warning-amber' : 'text-white'}>
{result.incidents}×
</Text>
</TableCell>
<TableCell>
<Text color="text-white" weight="medium">
{getPoints(result.position)}
</Text>
</TableCell>
<TableCell>
<Text weight="medium" className={getPositionChangeColor(positionChange)}>
{getPositionChangeText(positionChange)}
</Text>
</TableCell>
<TableCell>
{driverPenalties.length > 0 ? (
<Stack gap={1}>
{driverPenalties.map((penalty, idx) => (
<Stack key={idx} direction="row" align="center" gap={1.5} color="text-red-400">
<Icon icon={AlertTriangle} size={3} />
<Text size="xs">{getPenaltyDescription(penalty)}</Text>
</Stack>
))}
</Stack>
) : (
<Text color="text-gray-500"></Text>
)}
</TableCell>
{isAdmin && (
<TableCell className="text-right">
{driver && penaltyButtonRenderer && penaltyButtonRenderer(driver)}
</TableCell>
)}
</TableRow>
);
})}
</TableBody>
</Table>
</Box>
);
}