This commit is contained in:
2025-12-04 11:54:23 +01:00
parent c0fdae3d3c
commit 9d5caa87f3
83 changed files with 1579 additions and 2151 deletions

View File

@@ -0,0 +1,169 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import { createTeam, getCurrentDriverId } from '@gridpilot/racing/application';
interface CreateTeamFormProps {
onCancel?: () => void;
onSuccess?: (teamId: string) => void;
}
export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormProps) {
const router = useRouter();
const [formData, setFormData] = useState({
name: '',
tag: '',
description: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitting, setSubmitting] = useState(false);
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = 'Team name is required';
} else if (formData.name.length < 3) {
newErrors.name = 'Team name must be at least 3 characters';
}
if (!formData.tag.trim()) {
newErrors.tag = 'Team tag is required';
} else if (formData.tag.length > 4) {
newErrors.tag = 'Team tag must be 4 characters or less';
}
if (!formData.description.trim()) {
newErrors.description = 'Description is required';
} else if (formData.description.length < 10) {
newErrors.description = 'Description must be at least 10 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setSubmitting(true);
try {
const currentDriverId = getCurrentDriverId();
const team = createTeam(
formData.name,
formData.tag.toUpperCase(),
formData.description,
currentDriverId,
[] // Empty leagues array for now
);
if (onSuccess) {
onSuccess(team.id);
} else {
router.push(`/teams/${team.id}`);
}
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to create team');
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Team Name *
</label>
<Input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Enter team name..."
disabled={submitting}
/>
{errors.name && (
<p className="text-danger-red text-xs mt-1">{errors.name}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Team Tag *
</label>
<Input
type="text"
value={formData.tag}
onChange={(e) => setFormData({ ...formData, tag: e.target.value.toUpperCase() })}
placeholder="e.g., APEX"
maxLength={4}
disabled={submitting}
/>
<p className="text-xs text-gray-500 mt-1">Max 4 characters</p>
{errors.tag && (
<p className="text-danger-red text-xs mt-1">{errors.tag}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Description *
</label>
<textarea
className="w-full px-3 py-3 bg-iron-gray border-0 rounded-md text-white ring-1 ring-inset ring-charcoal-outline focus:ring-2 focus:ring-primary-blue transition-all duration-150 text-sm resize-none"
rows={4}
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Describe your team's goals and racing style..."
disabled={submitting}
/>
{errors.description && (
<p className="text-danger-red text-xs mt-1">{errors.description}</p>
)}
</div>
<div className="bg-deep-graphite border border-charcoal-outline rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-2xl"></div>
<div className="flex-1">
<h4 className="text-white font-medium mb-1">About Team Creation</h4>
<ul className="text-sm text-gray-400 space-y-1">
<li> You will be assigned as the team owner</li>
<li> You can invite other drivers to join your team</li>
<li> Team standings are calculated across leagues</li>
<li> This is alpha data - it resets on page reload</li>
</ul>
</div>
</div>
</div>
<div className="flex gap-3">
<Button
type="submit"
variant="primary"
disabled={submitting}
className="flex-1"
>
{submitting ? 'Creating Team...' : 'Create Team'}
</Button>
{onCancel && (
<Button
type="button"
variant="secondary"
onClick={onCancel}
disabled={submitting}
>
Cancel
</Button>
)}
</div>
</form>
);
}

View File

@@ -0,0 +1,109 @@
'use client';
import { useState } from 'react';
import Button from '@/components/ui/Button';
import {
getCurrentDriverId,
getTeamMembership,
getDriverTeam,
joinTeam,
requestToJoinTeam,
leaveTeam,
} from '@gridpilot/racing/application';
interface JoinTeamButtonProps {
teamId: string;
requiresApproval?: boolean;
onUpdate?: () => void;
}
export default function JoinTeamButton({
teamId,
requiresApproval = false,
onUpdate,
}: JoinTeamButtonProps) {
const [loading, setLoading] = useState(false);
const currentDriverId = getCurrentDriverId();
const membership = getTeamMembership(teamId, currentDriverId);
const currentTeam = getDriverTeam(currentDriverId);
const handleJoin = async () => {
setLoading(true);
try {
if (requiresApproval) {
requestToJoinTeam(teamId, currentDriverId);
alert('Join request sent! Wait for team approval.');
} else {
joinTeam(teamId, currentDriverId);
alert('Successfully joined team!');
}
onUpdate?.();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to join team');
} finally {
setLoading(false);
}
};
const handleLeave = async () => {
if (!confirm('Are you sure you want to leave this team?')) {
return;
}
setLoading(true);
try {
leaveTeam(teamId, currentDriverId);
alert('Successfully left team');
onUpdate?.();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to leave team');
} finally {
setLoading(false);
}
};
// Already a member
if (membership && membership.status === 'active') {
if (membership.role === 'owner') {
return (
<Button variant="secondary" disabled>
Team Owner
</Button>
);
}
return (
<Button
variant="danger"
onClick={handleLeave}
disabled={loading}
>
{loading ? 'Leaving...' : 'Leave Team'}
</Button>
);
}
// Already on another team
if (currentTeam && currentTeam.team.id !== teamId) {
return (
<Button variant="secondary" disabled>
Already on {currentTeam.team.name}
</Button>
);
}
// Can join
return (
<Button
variant="primary"
onClick={handleJoin}
disabled={loading}
>
{loading
? 'Processing...'
: requiresApproval
? 'Request to Join'
: 'Join Team'}
</Button>
);
}

View File

@@ -0,0 +1,249 @@
'use client';
import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import { getDriverRepository } from '@/lib/di-container';
import { EntityMappers, DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
import {
Team,
TeamJoinRequest,
getTeamJoinRequests,
approveTeamJoinRequest,
rejectTeamJoinRequest,
updateTeam,
} from '@gridpilot/racing/application';
interface TeamAdminProps {
team: Team;
onUpdate: () => void;
}
export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const [joinRequests, setJoinRequests] = useState<TeamJoinRequest[]>([]);
const [requestDrivers, setRequestDrivers] = useState<Record<string, DriverDTO>>({});
const [loading, setLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
const [editedTeam, setEditedTeam] = useState({
name: team.name,
tag: team.tag,
description: team.description,
});
useEffect(() => {
loadJoinRequests();
}, [team.id]);
const loadJoinRequests = async () => {
const requests = getTeamJoinRequests(team.id);
setJoinRequests(requests);
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const driverMap: Record<string, DriverDTO> = {};
for (const request of requests) {
const driver = allDrivers.find(d => d.id === request.driverId);
if (driver) {
const dto = EntityMappers.toDriverDTO(driver);
if (dto) {
driverMap[request.driverId] = dto;
}
}
}
setRequestDrivers(driverMap);
setLoading(false);
};
const handleApprove = async (requestId: string) => {
try {
approveTeamJoinRequest(requestId);
await loadJoinRequests();
onUpdate();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to approve request');
}
};
const handleReject = async (requestId: string) => {
try {
rejectTeamJoinRequest(requestId);
await loadJoinRequests();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to reject request');
}
};
const handleSaveChanges = () => {
try {
updateTeam(team.id, editedTeam, team.ownerId);
setEditMode(false);
onUpdate();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to update team');
}
};
return (
<div className="space-y-6">
<Card>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-white">Team Settings</h3>
{!editMode && (
<Button variant="secondary" onClick={() => setEditMode(true)}>
Edit Details
</Button>
)}
</div>
{editMode ? (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Team Name
</label>
<Input
type="text"
value={editedTeam.name}
onChange={(e) => setEditedTeam({ ...editedTeam, name: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Team Tag
</label>
<Input
type="text"
value={editedTeam.tag}
onChange={(e) => setEditedTeam({ ...editedTeam, tag: e.target.value })}
maxLength={4}
/>
<p className="text-xs text-gray-500 mt-1">Max 4 characters</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Description
</label>
<textarea
className="w-full px-3 py-3 bg-iron-gray border-0 rounded-md text-white ring-1 ring-inset ring-charcoal-outline focus:ring-2 focus:ring-primary-blue transition-all duration-150 text-sm resize-none"
rows={4}
value={editedTeam.description}
onChange={(e) => setEditedTeam({ ...editedTeam, description: e.target.value })}
/>
</div>
<div className="flex gap-2">
<Button variant="primary" onClick={handleSaveChanges}>
Save Changes
</Button>
<Button
variant="secondary"
onClick={() => {
setEditMode(false);
setEditedTeam({
name: team.name,
tag: team.tag,
description: team.description,
});
}}
>
Cancel
</Button>
</div>
</div>
) : (
<div className="space-y-4">
<div>
<div className="text-sm text-gray-400">Team Name</div>
<div className="text-white font-medium">{team.name}</div>
</div>
<div>
<div className="text-sm text-gray-400">Team Tag</div>
<div className="text-white font-medium">{team.tag}</div>
</div>
<div>
<div className="text-sm text-gray-400">Description</div>
<div className="text-white">{team.description}</div>
</div>
</div>
)}
</Card>
<Card>
<h3 className="text-xl font-semibold text-white mb-6">Join Requests</h3>
{loading ? (
<div className="text-center py-8 text-gray-400">Loading requests...</div>
) : joinRequests.length > 0 ? (
<div className="space-y-3">
{joinRequests.map((request) => {
const driver = requestDrivers[request.driverId];
if (!driver) return null;
return (
<div
key={request.id}
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline"
>
<div className="flex items-center gap-4 flex-1">
<div className="w-12 h-12 rounded-full bg-primary-blue/20 flex items-center justify-center text-lg font-bold text-white">
{driver.name.charAt(0)}
</div>
<div className="flex-1">
<h4 className="text-white font-medium">{driver.name}</h4>
<p className="text-sm text-gray-400">
{driver.country} Requested {new Date(request.requestedAt).toLocaleDateString()}
</p>
{request.message && (
<p className="text-sm text-gray-300 mt-1 italic">
"{request.message}"
</p>
)}
</div>
</div>
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => handleApprove(request.id)}
>
Approve
</Button>
<Button
variant="danger"
onClick={() => handleReject(request.id)}
>
Reject
</Button>
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-8 text-gray-400">
No pending join requests
</div>
)}
</Card>
<Card>
<h3 className="text-xl font-semibold text-white mb-4">Danger Zone</h3>
<div className="space-y-4">
<div className="p-4 rounded-lg bg-danger-red/10 border border-danger-red/30">
<h4 className="text-white font-medium mb-2">Disband Team</h4>
<p className="text-sm text-gray-400 mb-4">
Permanently delete this team. This action cannot be undone.
</p>
<Button variant="danger" disabled>
Disband Team (Coming Soon)
</Button>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,92 @@
'use client';
import Card from '../ui/Card';
interface TeamCardProps {
id: string;
name: string;
logo?: string;
memberCount: number;
leagues: string[];
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
onClick?: () => void;
}
export default function TeamCard({
id,
name,
logo,
memberCount,
leagues,
performanceLevel,
onClick,
}: TeamCardProps) {
const performanceBadgeColors = {
beginner: 'bg-green-500/20 text-green-400',
intermediate: 'bg-blue-500/20 text-blue-400',
advanced: 'bg-purple-500/20 text-purple-400',
pro: 'bg-red-500/20 text-red-400',
};
return (
<div
className="cursor-pointer hover:scale-[1.03] transition-transform duration-150"
onClick={onClick}
>
<Card>
<div className="space-y-4">
<div className="flex items-start gap-4">
<div className="w-16 h-16 bg-charcoal-outline rounded-lg flex items-center justify-center flex-shrink-0">
{logo ? (
<img src={logo} alt={name} className="w-full h-full object-cover rounded-lg" />
) : (
<span className="text-2xl font-bold text-gray-500">
{name.charAt(0)}
</span>
)}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-white truncate">
{name}
</h3>
<p className="text-sm text-gray-400">
{memberCount} {memberCount === 1 ? 'member' : 'members'}
</p>
</div>
</div>
{performanceLevel && (
<div>
<span
className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${
performanceBadgeColors[performanceLevel]
}`}
>
{performanceLevel.charAt(0).toUpperCase() + performanceLevel.slice(1)}
</span>
</div>
)}
<div className="space-y-2">
<p className="text-sm font-medium text-gray-400">Active in:</p>
<div className="flex flex-wrap gap-2">
{leagues.slice(0, 3).map((league, idx) => (
<span
key={idx}
className="inline-block px-2 py-1 bg-charcoal-outline text-gray-300 rounded text-xs"
>
{league}
</span>
))}
{leagues.length > 3 && (
<span className="inline-block px-2 py-1 bg-charcoal-outline text-gray-400 rounded text-xs">
+{leagues.length - 3} more
</span>
)}
</div>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,205 @@
'use client';
import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import { getDriverRepository, getDriverStats } from '@/lib/di-container';
import { EntityMappers, DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
import { TeamMembership, TeamRole } from '@gridpilot/racing/application';
interface TeamRosterProps {
teamId: string;
memberships: TeamMembership[];
isAdmin: boolean;
onRemoveMember?: (driverId: string) => void;
onChangeRole?: (driverId: string, newRole: TeamRole) => void;
}
export default function TeamRoster({
teamId,
memberships,
isAdmin,
onRemoveMember,
onChangeRole,
}: TeamRosterProps) {
const [drivers, setDrivers] = useState<Record<string, DriverDTO>>({});
const [loading, setLoading] = useState(true);
const [sortBy, setSortBy] = useState<'role' | 'rating' | 'name'>('rating');
useEffect(() => {
const loadDrivers = async () => {
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const driverMap: Record<string, DriverDTO> = {};
for (const membership of memberships) {
const driver = allDrivers.find(d => d.id === membership.driverId);
if (driver) {
const dto = EntityMappers.toDriverDTO(driver);
if (dto) {
driverMap[membership.driverId] = dto;
}
}
}
setDrivers(driverMap);
setLoading(false);
};
loadDrivers();
}, [memberships]);
const getRoleBadgeColor = (role: TeamRole) => {
switch (role) {
case 'owner':
return 'bg-warning-amber/20 text-warning-amber';
case 'manager':
return 'bg-primary-blue/20 text-primary-blue';
default:
return 'bg-charcoal-outline text-gray-300';
}
};
const getRoleLabel = (role: TeamRole) => {
return role.charAt(0).toUpperCase() + role.slice(1);
};
const sortedMemberships = [...memberships].sort((a, b) => {
switch (sortBy) {
case 'rating': {
const statsA = getDriverStats(a.driverId);
const statsB = getDriverStats(b.driverId);
return (statsB?.rating || 0) - (statsA?.rating || 0);
}
case 'role': {
const roleOrder = { owner: 0, manager: 1, driver: 2 };
return roleOrder[a.role] - roleOrder[b.role];
}
case 'name': {
const driverA = drivers[a.driverId];
const driverB = drivers[b.driverId];
return (driverA?.name || '').localeCompare(driverB?.name || '');
}
default:
return 0;
}
});
const teamAverageRating = memberships.length > 0
? Math.round(
memberships.reduce((sum, m) => {
const stats = getDriverStats(m.driverId);
return sum + (stats?.rating || 0);
}, 0) / memberships.length
)
: 0;
if (loading) {
return (
<Card>
<div className="text-center py-8 text-gray-400">Loading roster...</div>
</Card>
);
}
return (
<Card>
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-xl font-semibold text-white">Team Roster</h3>
<p className="text-sm text-gray-400 mt-1">
{memberships.length} {memberships.length === 1 ? 'member' : 'members'} Avg Rating: <span className="text-primary-blue font-medium">{teamAverageRating}</span>
</p>
</div>
<div className="flex items-center gap-2">
<label className="text-sm text-gray-400">Sort by:</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="px-3 py-1 bg-deep-graphite border border-charcoal-outline rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue"
>
<option value="rating">Rating</option>
<option value="role">Role</option>
<option value="name">Name</option>
</select>
</div>
</div>
<div className="space-y-3">
{sortedMemberships.map((membership) => {
const driver = drivers[membership.driverId];
const driverStats = getDriverStats(membership.driverId);
if (!driver) return null;
return (
<div
key={membership.driverId}
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline hover:border-charcoal-outline/60 transition-colors"
>
<div className="flex items-center gap-4 flex-1">
<div className="w-12 h-12 rounded-full bg-primary-blue/20 flex items-center justify-center text-lg font-bold text-white">
{driver.name.charAt(0)}
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h4 className="text-white font-medium">{driver.name}</h4>
<span className={`px-2 py-1 rounded text-xs font-medium ${getRoleBadgeColor(membership.role)}`}>
{getRoleLabel(membership.role)}
</span>
</div>
<p className="text-sm text-gray-400">
{driver.country} Joined {new Date(membership.joinedAt).toLocaleDateString()}
</p>
</div>
{driverStats && (
<div className="flex items-center gap-6 text-center">
<div>
<div className="text-lg font-bold text-primary-blue">{driverStats.rating}</div>
<div className="text-xs text-gray-400">Rating</div>
</div>
<div>
<div className="text-sm text-gray-300">#{driverStats.overallRank}</div>
<div className="text-xs text-gray-500">Rank</div>
</div>
</div>
)}
</div>
{isAdmin && membership.role !== 'owner' && (
<div className="flex items-center gap-2">
{onChangeRole && (
<select
className="px-3 py-2 bg-iron-gray border-0 rounded text-white ring-1 ring-inset ring-charcoal-outline focus:ring-2 focus:ring-primary-blue transition-all duration-150 text-sm"
value={membership.role}
onChange={(e) => onChangeRole(membership.driverId, e.target.value as TeamRole)}
>
<option value="driver">Driver</option>
<option value="manager">Manager</option>
</select>
)}
{onRemoveMember && (
<button
onClick={() => onRemoveMember(membership.driverId)}
className="px-3 py-2 bg-danger-red/20 hover:bg-danger-red/30 text-danger-red rounded text-sm font-medium transition-colors"
>
Remove
</button>
)}
</div>
)}
</div>
);
})}
</div>
{memberships.length === 0 && (
<div className="text-center py-8 text-gray-400">
No team members yet.
</div>
)}
</Card>
);
}

View File

@@ -0,0 +1,135 @@
'use client';
import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import { getStandingRepository, getLeagueRepository } from '@/lib/di-container';
import { EntityMappers, LeagueDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
import { getTeamMembers } from '@gridpilot/racing/application';
interface TeamStandingsProps {
teamId: string;
leagues: string[];
}
interface TeamLeagueStanding {
leagueId: string;
leagueName: string;
position: number;
points: number;
wins: number;
racesCompleted: number;
}
export default function TeamStandings({ teamId, leagues }: TeamStandingsProps) {
const [standings, setStandings] = useState<TeamLeagueStanding[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadStandings = async () => {
const standingRepo = getStandingRepository();
const leagueRepo = getLeagueRepository();
const members = getTeamMembers(teamId);
const memberIds = members.map(m => m.driverId);
const teamStandings: TeamLeagueStanding[] = [];
for (const leagueId of leagues) {
const league = await leagueRepo.findById(leagueId);
if (!league) continue;
const leagueStandings = await standingRepo.findByLeagueId(leagueId);
// Calculate team points (sum of all team members)
let totalPoints = 0;
let totalWins = 0;
let totalRaces = 0;
for (const standing of leagueStandings) {
if (memberIds.includes(standing.driverId)) {
totalPoints += standing.points;
totalWins += standing.wins;
totalRaces = Math.max(totalRaces, standing.racesCompleted);
}
}
// Calculate team position (simplified - based on total points)
const allTeamPoints = leagueStandings
.filter(s => memberIds.includes(s.driverId))
.reduce((sum, s) => sum + s.points, 0);
const position = leagueStandings
.filter((_, idx, arr) => {
const teamPoints = arr
.filter(s => memberIds.includes(s.driverId))
.reduce((sum, s) => sum + s.points, 0);
return teamPoints > allTeamPoints;
}).length + 1;
teamStandings.push({
leagueId,
leagueName: league.name,
position,
points: totalPoints,
wins: totalWins,
racesCompleted: totalRaces,
});
}
setStandings(teamStandings);
setLoading(false);
};
loadStandings();
}, [teamId, leagues]);
if (loading) {
return (
<Card>
<div className="text-center py-8 text-gray-400">Loading standings...</div>
</Card>
);
}
return (
<Card>
<h3 className="text-xl font-semibold text-white mb-6">League Standings</h3>
<div className="space-y-4">
{standings.map((standing) => (
<div
key={standing.leagueId}
className="p-4 rounded-lg bg-deep-graphite border border-charcoal-outline"
>
<div className="flex items-center justify-between mb-3">
<h4 className="text-white font-medium">{standing.leagueName}</h4>
<span className="px-3 py-1 bg-primary-blue/20 text-primary-blue rounded-full text-sm font-semibold">
P{standing.position}
</span>
</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-2xl font-bold text-white">{standing.points}</div>
<div className="text-xs text-gray-400">Points</div>
</div>
<div>
<div className="text-2xl font-bold text-white">{standing.wins}</div>
<div className="text-xs text-gray-400">Wins</div>
</div>
<div>
<div className="text-2xl font-bold text-white">{standing.racesCompleted}</div>
<div className="text-xs text-gray-400">Races</div>
</div>
</div>
</div>
))}
</div>
{standings.length === 0 && (
<div className="text-center py-8 text-gray-400">
No standings available yet.
</div>
)}
</Card>
);
}