website refactor
This commit is contained in:
@@ -1,38 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import CountryFlag from '@/ui/CountryFlag';
|
||||
import PlaceholderImage from '@/ui/PlaceholderImage';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { CountryFlag } from '@/ui/CountryFlag';
|
||||
import { PlaceholderImage } from '@/ui/PlaceholderImage';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { User, Edit } from 'lucide-react';
|
||||
|
||||
// League role display data
|
||||
const leagueRoleDisplay = {
|
||||
owner: {
|
||||
text: 'Owner',
|
||||
badgeClasses: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30',
|
||||
bg: 'bg-yellow-500/10',
|
||||
color: 'text-yellow-500',
|
||||
borderColor: 'border-yellow-500/30',
|
||||
},
|
||||
admin: {
|
||||
text: 'Admin',
|
||||
badgeClasses: 'bg-purple-500/10 text-purple-400 border-purple-500/30',
|
||||
bg: 'bg-purple-500/10',
|
||||
color: 'text-purple-400',
|
||||
borderColor: 'border-purple-500/30',
|
||||
},
|
||||
steward: {
|
||||
text: 'Steward',
|
||||
badgeClasses: 'bg-blue-500/10 text-blue-400 border-blue-500/30',
|
||||
bg: 'bg-blue-500/10',
|
||||
color: 'text-blue-400',
|
||||
borderColor: 'border-blue-500/30',
|
||||
},
|
||||
member: {
|
||||
text: 'Member',
|
||||
badgeClasses: 'bg-primary-blue/10 text-primary-blue border-primary-blue/30',
|
||||
bg: 'bg-primary-blue/10',
|
||||
color: 'text-primary-blue',
|
||||
borderColor: 'border-primary-blue/30',
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Position background colors
|
||||
const getPositionBgColor = (position: number): string => {
|
||||
const getPositionBgColor = (position: number) => {
|
||||
switch (position) {
|
||||
case 1: return 'bg-yellow-500/10 border-l-4 border-l-yellow-500';
|
||||
case 2: return 'bg-gray-300/10 border-l-4 border-l-gray-400';
|
||||
case 3: return 'bg-amber-600/10 border-l-4 border-l-amber-600';
|
||||
default: return 'border-l-4 border-l-transparent';
|
||||
case 1: return { bg: 'bg-yellow-500/10', borderLeft: true, borderColor: 'border-l-yellow-500' };
|
||||
case 2: return { bg: 'bg-gray-300/10', borderLeft: true, borderColor: 'border-l-gray-400' };
|
||||
case 3: return { bg: 'bg-amber-600/10', borderLeft: true, borderColor: 'border-l-amber-600' };
|
||||
default: return { borderLeft: true, borderColor: 'border-l-transparent' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,151 +172,293 @@ export function StandingsTable({
|
||||
const hasMembership = !!membership;
|
||||
|
||||
return (
|
||||
<div
|
||||
<Box
|
||||
ref={menuRef}
|
||||
className="absolute right-0 top-full mt-1 z-50 bg-deep-graphite border border-charcoal-outline rounded-lg shadow-xl p-2 min-w-[200px]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
position="absolute"
|
||||
right="0"
|
||||
top="full"
|
||||
mt={1}
|
||||
zIndex={50}
|
||||
bg="bg-deep-graphite"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
rounded="lg"
|
||||
shadow="xl"
|
||||
p={2}
|
||||
minWidth="200px"
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
>
|
||||
<div className="text-xs text-gray-400 px-2 py-1 mb-1">
|
||||
<Text size="xs" color="text-gray-400" px={2} py={1} mb={1} block>
|
||||
Member Management
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
</Text>
|
||||
<Stack gap={1}>
|
||||
{hasMembership ? (
|
||||
<>
|
||||
{/* Role Management for existing members */}
|
||||
{membership!.role !== 'admin' && membership!.role !== 'owner' && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleRoleChange(driverId, 'admin'); }}
|
||||
className="w-full text-left px-3 py-2 text-sm text-white hover:bg-purple-500/10 rounded flex items-center gap-2 transition-colors"
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'admin'); }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
w="full"
|
||||
textAlign="left"
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
fontSize="14px"
|
||||
color="text-white"
|
||||
hoverBg="bg-purple-500/10"
|
||||
transition
|
||||
>
|
||||
<span>🛡️</span>
|
||||
<span>Promote to Admin</span>
|
||||
</button>
|
||||
<Text>🛡️</Text>
|
||||
<Text>Promote to Admin</Text>
|
||||
</Box>
|
||||
)}
|
||||
{membership!.role === 'admin' && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleRoleChange(driverId, 'member'); }}
|
||||
className="w-full text-left px-3 py-2 text-sm text-white hover:bg-iron-gray/20 rounded flex items-center gap-2 transition-colors"
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'member'); }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
w="full"
|
||||
textAlign="left"
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
fontSize="14px"
|
||||
color="text-white"
|
||||
hoverBg="bg-iron-gray/20"
|
||||
transition
|
||||
>
|
||||
<span>⬇️</span>
|
||||
<span>Demote to Member</span>
|
||||
</button>
|
||||
<Text>⬇️</Text>
|
||||
<Text>Demote to Member</Text>
|
||||
</Box>
|
||||
)}
|
||||
{membership!.role === 'member' && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleRoleChange(driverId, 'steward'); }}
|
||||
className="w-full text-left px-3 py-2 text-sm text-white hover:bg-blue-500/10 rounded flex items-center gap-2 transition-colors"
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'steward'); }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
w="full"
|
||||
textAlign="left"
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
fontSize="14px"
|
||||
color="text-white"
|
||||
hoverBg="bg-blue-500/10"
|
||||
transition
|
||||
>
|
||||
<span>🏁</span>
|
||||
<span>Make Steward</span>
|
||||
</button>
|
||||
<Text>🏁</Text>
|
||||
<Text>Make Steward</Text>
|
||||
</Box>
|
||||
)}
|
||||
{membership!.role === 'steward' && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleRoleChange(driverId, 'member'); }}
|
||||
className="w-full text-left px-3 py-2 text-sm text-white hover:bg-iron-gray/20 rounded flex items-center gap-2 transition-colors"
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'member'); }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
w="full"
|
||||
textAlign="left"
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
fontSize="14px"
|
||||
color="text-white"
|
||||
hoverBg="bg-iron-gray/20"
|
||||
transition
|
||||
>
|
||||
<span>🏁</span>
|
||||
<span>Remove Steward</span>
|
||||
</button>
|
||||
<Text>🏁</Text>
|
||||
<Text>Remove Steward</Text>
|
||||
</Box>
|
||||
)}
|
||||
<div className="border-t border-charcoal-outline my-1"></div>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleRemove(driverId); }}
|
||||
className="w-full text-left px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded flex items-center gap-2 transition-colors"
|
||||
<Box borderTop borderColor="border-charcoal-outline" my={1} />
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRemove(driverId); }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
w="full"
|
||||
textAlign="left"
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
fontSize="14px"
|
||||
color="text-red-400"
|
||||
hoverBg="bg-red-500/10"
|
||||
transition
|
||||
>
|
||||
<span>🚫</span>
|
||||
<span>Remove from League</span>
|
||||
</button>
|
||||
<Text>🚫</Text>
|
||||
<Text>Remove from League</Text>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Options for drivers without membership (participating but not formal members) */}
|
||||
<div className="text-xs text-yellow-400/80 px-2 py-1 mb-1 bg-yellow-500/10 rounded">
|
||||
Driver not a formal member
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); alert('Add as member - feature coming soon'); }}
|
||||
className="w-full text-left px-3 py-2 text-sm text-white hover:bg-green-500/10 rounded flex items-center gap-2 transition-colors"
|
||||
<Box bg="bg-yellow-500/10" rounded px={2} py={1} mb={1}>
|
||||
<Text size="xs" color="text-yellow-400/80">Driver not a formal member</Text>
|
||||
</Box>
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('Add as member - feature coming soon'); }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
w="full"
|
||||
textAlign="left"
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
fontSize="14px"
|
||||
color="text-white"
|
||||
hoverBg="bg-green-500/10"
|
||||
transition
|
||||
>
|
||||
<span>➕</span>
|
||||
<span>Add as Member</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleRemove(driverId); }}
|
||||
className="w-full text-left px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded flex items-center gap-2 transition-colors"
|
||||
<Text>➕</Text>
|
||||
<Text>Add as Member</Text>
|
||||
</Box>
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRemove(driverId); }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
w="full"
|
||||
textAlign="left"
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
fontSize="14px"
|
||||
color="text-red-400"
|
||||
hoverBg="bg-red-500/10"
|
||||
transition
|
||||
>
|
||||
<span>🚫</span>
|
||||
<span>Remove from Standings</span>
|
||||
</button>
|
||||
<Text>🚫</Text>
|
||||
<Text>Remove from Standings</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const PointsActionMenu = () => {
|
||||
return (
|
||||
<div
|
||||
<Box
|
||||
ref={menuRef}
|
||||
className="absolute right-0 top-full mt-1 z-50 bg-deep-graphite border border-charcoal-outline rounded-lg shadow-xl p-2 min-w-[180px]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
position="absolute"
|
||||
right="0"
|
||||
top="full"
|
||||
mt={1}
|
||||
zIndex={50}
|
||||
bg="bg-deep-graphite"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
rounded="lg"
|
||||
shadow="xl"
|
||||
p={2}
|
||||
minWidth="180px"
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
>
|
||||
<div className="text-xs text-gray-400 px-2 py-1 mb-1">
|
||||
<Text size="xs" color="text-gray-400" px={2} py={1} mb={1} block>
|
||||
Score Actions
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); alert('View detailed stats - feature coming soon'); }}
|
||||
className="w-full text-left px-3 py-2 text-sm text-white hover:bg-iron-gray/20 rounded flex items-center gap-2 transition-colors"
|
||||
</Text>
|
||||
<Stack gap={1}>
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('View detailed stats - feature coming soon'); }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
w="full"
|
||||
textAlign="left"
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
color="text-white"
|
||||
hoverBg="bg-iron-gray/20"
|
||||
transition
|
||||
fontSize="14px"
|
||||
>
|
||||
<span>📊</span>
|
||||
<span>View Details</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); alert('Manual adjustment - feature coming soon'); }}
|
||||
className="w-full text-left px-3 py-2 text-sm text-white hover:bg-iron-gray/20 rounded flex items-center gap-2 transition-colors"
|
||||
<Text>📊</Text>
|
||||
<Text>View Details</Text>
|
||||
</Box>
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('Manual adjustment - feature coming soon'); }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
w="full"
|
||||
textAlign="left"
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
color="text-white"
|
||||
hoverBg="bg-iron-gray/20"
|
||||
transition
|
||||
fontSize="14px"
|
||||
>
|
||||
<span>⚠️</span>
|
||||
<span>Adjust Points</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); alert('Race history - feature coming soon'); }}
|
||||
className="w-full text-left px-3 py-2 text-sm text-white hover:bg-iron-gray/20 rounded flex items-center gap-2 transition-colors"
|
||||
<Text>⚠️</Text>
|
||||
<Text>Adjust Points</Text>
|
||||
</Box>
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('Race history - feature coming soon'); }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
w="full"
|
||||
textAlign="left"
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
color="text-white"
|
||||
hoverBg="bg-iron-gray/20"
|
||||
transition
|
||||
fontSize="14px"
|
||||
>
|
||||
<span>📝</span>
|
||||
<span>Race History</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Text>📝</Text>
|
||||
<Text>Race History</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
if (standings.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
No standings available
|
||||
</div>
|
||||
<Box textAlign="center" py={8}>
|
||||
<Text color="text-gray-400">No standings available</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto overflow-y-visible">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-charcoal-outline">
|
||||
<th className="text-center py-3 px-3 font-semibold text-gray-400 w-14">Pos</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-400">Driver</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-400">Team</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-400">Points</th>
|
||||
<th className="text-center py-3 px-4 font-semibold text-gray-400">Races</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-400">Avg Finish</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-400">Penalty</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-400">Bonus</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Box overflow="auto">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader textAlign="center" w="14">Pos</TableHeader>
|
||||
<TableHeader>Driver</TableHeader>
|
||||
<TableHeader>Team</TableHeader>
|
||||
<TableHeader textAlign="right">Points</TableHeader>
|
||||
<TableHeader textAlign="center">Races</TableHeader>
|
||||
<TableHeader textAlign="right">Avg Finish</TableHeader>
|
||||
<TableHeader textAlign="right">Penalty</TableHeader>
|
||||
<TableHeader textAlign="right">Bonus</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{standings.map((row) => {
|
||||
const driver = getDriver(row.driverId);
|
||||
const membership = getMembership(row.driverId);
|
||||
@@ -311,11 +469,16 @@ export function StandingsTable({
|
||||
const isPointsMenuOpen = activeMenu?.driverId === row.driverId && activeMenu?.type === 'points';
|
||||
|
||||
const isMe = isCurrentUser(row.driverId);
|
||||
const posConfig = getPositionBgColor(row.position);
|
||||
|
||||
return (
|
||||
<tr
|
||||
<TableRow
|
||||
key={row.driverId}
|
||||
className={`border-b border-charcoal-outline/50 transition-all duration-200 ${getPositionBgColor(row.position)} ${isRowHovered ? 'bg-iron-gray/10' : ''} ${isMe ? 'ring-2 ring-primary-blue/50 ring-inset bg-primary-blue/5' : ''}`}
|
||||
bg={isMe ? 'bg-primary-blue/5' : posConfig.bg}
|
||||
borderLeft={posConfig.borderLeft}
|
||||
borderColor={posConfig.borderColor}
|
||||
hoverBg="bg-iron-gray/10"
|
||||
ring={isMe ? 'ring-2 ring-primary-blue/50 ring-inset' : ''}
|
||||
onMouseEnter={() => setHoveredRow(row.driverId)}
|
||||
onMouseLeave={() => {
|
||||
setHoveredRow(null);
|
||||
@@ -325,150 +488,198 @@ export function StandingsTable({
|
||||
}}
|
||||
>
|
||||
{/* Position */}
|
||||
<td className="py-3 px-3 text-center">
|
||||
<div className={`inline-flex items-center justify-center w-8 h-8 rounded-full font-bold ${
|
||||
row.position === 1 ? 'bg-yellow-500 text-black' :
|
||||
row.position === 2 ? 'bg-gray-400 text-black' :
|
||||
row.position === 3 ? 'bg-amber-600 text-white' :
|
||||
'bg-charcoal-outline text-white'
|
||||
}`}>
|
||||
<TableCell textAlign="center" w="14">
|
||||
<Box
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
w="8"
|
||||
h="8"
|
||||
rounded="full"
|
||||
weight="bold"
|
||||
bg={
|
||||
row.position === 1 ? 'bg-yellow-500' :
|
||||
row.position === 2 ? 'bg-gray-400' :
|
||||
row.position === 3 ? 'bg-amber-600' :
|
||||
'bg-charcoal-outline'
|
||||
}
|
||||
color={
|
||||
row.position === 1 ? 'text-black' :
|
||||
row.position === 2 ? 'text-black' :
|
||||
'text-white'
|
||||
}
|
||||
>
|
||||
{row.position}
|
||||
</div>
|
||||
</td>
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
{/* Driver with Rating and Nationality */}
|
||||
<td className="py-3 px-4 relative">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Avatar */}
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-blue/20 overflow-hidden flex items-center justify-center shrink-0">
|
||||
{driver && (
|
||||
driver.avatarUrl ? (
|
||||
<Image
|
||||
src={driver.avatarUrl}
|
||||
alt={driver.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={40} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{/* Nationality flag */}
|
||||
{driver && driver.country && (
|
||||
<div className="absolute -bottom-1 -right-1">
|
||||
<CountryFlag countryCode={driver.country} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Name and Rating */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={`/drivers/${row.driverId}`}
|
||||
className="font-medium text-white truncate hover:text-primary-blue transition-colors"
|
||||
>
|
||||
{driver?.name || 'Unknown Driver'}
|
||||
</Link>
|
||||
{isMe && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-primary-blue/20 text-primary-blue border border-primary-blue/30">
|
||||
You
|
||||
</span>
|
||||
{/* Driver with Rating and Nationality */}
|
||||
<TableCell position="relative">
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
{/* Avatar */}
|
||||
<Box position="relative">
|
||||
<Box
|
||||
w="10"
|
||||
h="10"
|
||||
rounded="full"
|
||||
bg="bg-primary-blue/20"
|
||||
overflow="hidden"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexShrink={0}
|
||||
>
|
||||
{driver && (
|
||||
driver.avatarUrl ? (
|
||||
<Image
|
||||
src={driver.avatarUrl}
|
||||
alt={driver.name}
|
||||
width={40}
|
||||
height={40}
|
||||
fullWidth
|
||||
fullHeight
|
||||
objectFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={40} />
|
||||
)
|
||||
)}
|
||||
{roleDisplay && roleDisplay.text !== 'Member' && (
|
||||
<span className={`px-2 py-0.5 text-xs font-medium rounded border ${roleDisplay.badgeClasses}`}>
|
||||
{roleDisplay.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs flex items-center gap-1">
|
||||
{/* Rating intentionally omitted until API provides driver stats */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover Actions for Member Management */}
|
||||
{isAdmin && canModify && (
|
||||
<div className="flex items-center gap-1" style={{ opacity: isRowHovered || isMemberMenuOpen ? 1 : 0, transition: 'opacity 0.2s', visibility: isRowHovered || isMemberMenuOpen ? 'visible' : 'hidden' }}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setActiveMenu(isMemberMenuOpen ? null : { driverId: row.driverId, type: 'member' }); }}
|
||||
className={`p-1.5 rounded transition-colors ${isMemberMenuOpen ? 'bg-primary-blue/20 text-primary-blue' : 'text-gray-400 hover:text-white hover:bg-iron-gray/30'}`}
|
||||
title="Manage member"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</Box>
|
||||
{/* Nationality flag */}
|
||||
{driver && driver.country && (
|
||||
<Box position="absolute" bottom="-1" right="-1">
|
||||
<CountryFlag countryCode={driver.country} size="sm" />
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
{isMemberMenuOpen && <MemberActionMenu driverId={row.driverId} />}
|
||||
</td>
|
||||
</Box>
|
||||
|
||||
{/* Name and Rating */}
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Link
|
||||
href={routes.driver.detail(row.driverId)}
|
||||
weight="medium"
|
||||
color="text-white"
|
||||
truncate
|
||||
hoverTextColor="text-primary-blue"
|
||||
transition
|
||||
>
|
||||
{driver?.name || 'Unknown Driver'}
|
||||
</Link>
|
||||
{isMe && (
|
||||
<Badge variant="primary">You</Badge>
|
||||
)}
|
||||
{roleDisplay && roleDisplay.text !== 'Member' && (
|
||||
<Box
|
||||
as="span"
|
||||
px={2}
|
||||
py={0.5}
|
||||
fontSize="12px"
|
||||
weight="medium"
|
||||
rounded="md"
|
||||
border
|
||||
bg={roleDisplay.bg}
|
||||
color={roleDisplay.color}
|
||||
borderColor={roleDisplay.borderColor}
|
||||
>
|
||||
{roleDisplay.text}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Team */}
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-gray-300">{row.teamName ?? '—'}</span>
|
||||
</td>
|
||||
{/* Hover Actions for Member Management */}
|
||||
{isAdmin && canModify && (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
opacity={isRowHovered || isMemberMenuOpen ? 1 : 0}
|
||||
transition
|
||||
visibility={isRowHovered || isMemberMenuOpen ? 'visible' : 'hidden'}
|
||||
>
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setActiveMenu(isMemberMenuOpen ? null : { driverId: row.driverId, type: 'member' }); }}
|
||||
p={1.5}
|
||||
rounded="md"
|
||||
bg={isMemberMenuOpen ? 'bg-primary-blue/20' : ''}
|
||||
color={isMemberMenuOpen ? 'text-primary-blue' : 'text-gray-400'}
|
||||
hoverColor={!isMemberMenuOpen ? 'text-white' : ''}
|
||||
hoverBg={!isMemberMenuOpen ? 'bg-iron-gray/30' : ''}
|
||||
transition
|
||||
title="Manage member"
|
||||
>
|
||||
<Icon icon={User} size={4} />
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{isMemberMenuOpen && <MemberActionMenu driverId={row.driverId} />}
|
||||
</TableCell>
|
||||
|
||||
{/* Total Points with Hover Action */}
|
||||
<td className="py-3 px-4 text-right relative">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<span className="text-white font-bold text-lg">{row.totalPoints}</span>
|
||||
{isAdmin && canModify && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setActiveMenu(isPointsMenuOpen ? null : { driverId: row.driverId, type: 'points' }); }}
|
||||
className={`p-1 rounded transition-colors ${isPointsMenuOpen ? 'bg-primary-blue/20 text-primary-blue' : 'text-gray-400 hover:text-white hover:bg-iron-gray/30'}`}
|
||||
style={{ opacity: isRowHovered || isPointsMenuOpen ? 1 : 0, transition: 'opacity 0.2s', visibility: isRowHovered || isPointsMenuOpen ? 'visible' : 'hidden' }}
|
||||
{/* Team */}
|
||||
<TableCell>
|
||||
<Text color="text-gray-300">{row.teamName ?? '—'}</Text>
|
||||
</TableCell>
|
||||
|
||||
{/* Total Points with Hover Action */}
|
||||
<TableCell textAlign="right" position="relative">
|
||||
<Box display="flex" alignItems="center" justifyContent="end" gap={2}>
|
||||
<Text color="text-white" weight="bold" size="lg">{row.totalPoints}</Text>
|
||||
{isAdmin && canModify && (
|
||||
<Box
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setActiveMenu(isPointsMenuOpen ? null : { driverId: row.driverId, type: 'points' }); }}
|
||||
p={1}
|
||||
rounded="md"
|
||||
bg={isPointsMenuOpen ? 'bg-primary-blue/20' : ''}
|
||||
color={isPointsMenuOpen ? 'text-primary-blue' : 'text-gray-400'}
|
||||
hoverColor={!isPointsMenuOpen ? 'text-white' : ''}
|
||||
hoverBg={!isPointsMenuOpen ? 'bg-iron-gray/30' : ''}
|
||||
transition
|
||||
opacity={isRowHovered || isPointsMenuOpen ? 1 : 0}
|
||||
visibility={isRowHovered || isPointsMenuOpen ? 'visible' : 'hidden'}
|
||||
title="Score actions"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isPointsMenuOpen && <PointsActionMenu />}
|
||||
</td>
|
||||
<Icon icon={Edit} size={3} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{isPointsMenuOpen && <PointsActionMenu />}
|
||||
</TableCell>
|
||||
|
||||
{/* Races (Finished/Started) */}
|
||||
<td className="py-3 px-4 text-center">
|
||||
<span className="text-white">{row.racesFinished}</span>
|
||||
<span className="text-gray-500">/{row.racesStarted}</span>
|
||||
</td>
|
||||
{/* Races (Finished/Started) */}
|
||||
<TableCell textAlign="center">
|
||||
<Text color="text-white">{row.racesFinished}</Text>
|
||||
<Text color="text-gray-500">/{row.racesStarted}</Text>
|
||||
</TableCell>
|
||||
|
||||
{/* Avg Finish */}
|
||||
<td className="py-3 px-4 text-right">
|
||||
<span className="text-gray-300">
|
||||
{row.avgFinish !== null ? row.avgFinish.toFixed(1) : '—'}
|
||||
</span>
|
||||
</td>
|
||||
{/* Avg Finish */}
|
||||
<TableCell textAlign="right">
|
||||
<Text color="text-gray-300">
|
||||
{row.avgFinish !== null ? row.avgFinish.toFixed(1) : '—'}
|
||||
</Text>
|
||||
</TableCell>
|
||||
|
||||
{/* Penalty */}
|
||||
<td className="py-3 px-4 text-right">
|
||||
<span className={row.penaltyPoints > 0 ? 'text-red-400 font-medium' : 'text-gray-500'}>
|
||||
{row.penaltyPoints > 0 ? `-${row.penaltyPoints}` : '—'}
|
||||
</span>
|
||||
</td>
|
||||
{/* Penalty */}
|
||||
<TableCell textAlign="right">
|
||||
<Text color={row.penaltyPoints > 0 ? 'text-red-400' : 'text-gray-500'} weight={row.penaltyPoints > 0 ? 'medium' : 'normal'}>
|
||||
{row.penaltyPoints > 0 ? `-${row.penaltyPoints}` : '—'}
|
||||
</Text>
|
||||
</TableCell>
|
||||
|
||||
{/* Bonus */}
|
||||
<td className="py-3 px-4 text-right">
|
||||
<span className={row.bonusPoints !== 0 ? 'text-green-400 font-medium' : 'text-gray-500'}>
|
||||
{row.bonusPoints !== 0 ? `+${row.bonusPoints}` : '—'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<style jsx>{`
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
{/* Bonus */}
|
||||
<TableCell textAlign="right">
|
||||
<Text color={row.bonusPoints !== 0 ? 'text-green-400' : 'text-gray-500'} weight={row.bonusPoints !== 0 ? 'medium' : 'normal'}>
|
||||
{row.bonusPoints !== 0 ? `+${row.bonusPoints}` : '—'}
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user