639 lines
26 KiB
TypeScript
639 lines
26 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import Heading from '@/components/ui/Heading';
|
|
import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race';
|
|
import { getGetRacesPageDataUseCase } from '@/lib/di-container';
|
|
import {
|
|
Calendar,
|
|
Clock,
|
|
Flag,
|
|
ChevronRight,
|
|
Filter,
|
|
MapPin,
|
|
Car,
|
|
Trophy,
|
|
Users,
|
|
Zap,
|
|
PlayCircle,
|
|
CheckCircle2,
|
|
XCircle,
|
|
CalendarDays,
|
|
ArrowRight,
|
|
} from 'lucide-react';
|
|
|
|
type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
|
|
|
|
export default function RacesPage() {
|
|
const router = useRouter();
|
|
|
|
const [pageData, setPageData] = useState<{
|
|
races: Array<{ race: Race; leagueName: string }>;
|
|
stats: { total: number; scheduled: number; running: number; completed: number };
|
|
liveRaces: Array<{ race: Race; leagueName: string }>;
|
|
upcomingRaces: Array<{ race: Race; leagueName: string }>;
|
|
recentResults: Array<{ race: Race; leagueName: string }>;
|
|
} | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Filters
|
|
const [statusFilter, setStatusFilter] = useState<RaceStatus | 'all'>('all');
|
|
const [leagueFilter, setLeagueFilter] = useState<string>('all');
|
|
const [timeFilter, setTimeFilter] = useState<TimeFilter>('upcoming');
|
|
|
|
const loadRaces = async () => {
|
|
try {
|
|
const useCase = getGetRacesPageDataUseCase();
|
|
await useCase.execute();
|
|
const data = useCase.presenter.getViewModel();
|
|
|
|
// Transform ViewModel back to page format
|
|
setPageData({
|
|
races: data.races.map(r => ({
|
|
race: {
|
|
id: r.id,
|
|
track: r.track,
|
|
car: r.car,
|
|
scheduledAt: new Date(r.scheduledAt),
|
|
status: r.status,
|
|
leagueId: r.leagueId,
|
|
strengthOfField: r.strengthOfField,
|
|
isUpcoming: () => r.isUpcoming,
|
|
isLive: () => r.isLive,
|
|
isPast: () => r.isPast,
|
|
} as Race,
|
|
leagueName: r.leagueName,
|
|
})),
|
|
stats: data.stats,
|
|
liveRaces: data.liveRaces.map(r => ({
|
|
race: {
|
|
id: r.id,
|
|
track: r.track,
|
|
car: r.car,
|
|
scheduledAt: new Date(r.scheduledAt),
|
|
status: r.status,
|
|
leagueId: r.leagueId,
|
|
strengthOfField: r.strengthOfField,
|
|
isUpcoming: () => r.isUpcoming,
|
|
isLive: () => r.isLive,
|
|
isPast: () => r.isPast,
|
|
} as Race,
|
|
leagueName: r.leagueName,
|
|
})),
|
|
upcomingRaces: data.upcomingThisWeek.map(r => ({
|
|
race: {
|
|
id: r.id,
|
|
track: r.track,
|
|
car: r.car,
|
|
scheduledAt: new Date(r.scheduledAt),
|
|
status: r.status,
|
|
leagueId: r.leagueId,
|
|
strengthOfField: r.strengthOfField,
|
|
isUpcoming: () => r.isUpcoming,
|
|
isLive: () => r.isLive,
|
|
isPast: () => r.isPast,
|
|
} as Race,
|
|
leagueName: r.leagueName,
|
|
})),
|
|
recentResults: data.recentResults.map(r => ({
|
|
race: {
|
|
id: r.id,
|
|
track: r.track,
|
|
car: r.car,
|
|
scheduledAt: new Date(r.scheduledAt),
|
|
status: r.status,
|
|
leagueId: r.leagueId,
|
|
strengthOfField: r.strengthOfField,
|
|
isUpcoming: () => r.isUpcoming,
|
|
isLive: () => r.isLive,
|
|
isPast: () => r.isPast,
|
|
} as Race,
|
|
leagueName: r.leagueName,
|
|
})),
|
|
});
|
|
} catch (err) {
|
|
console.error('Failed to load races:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadRaces();
|
|
}, []);
|
|
|
|
// Filter races
|
|
const filteredRaces = useMemo(() => {
|
|
if (!pageData) return [];
|
|
|
|
return pageData.races.filter(({ race }) => {
|
|
// Status filter
|
|
if (statusFilter !== 'all' && race.status !== statusFilter) {
|
|
return false;
|
|
}
|
|
|
|
// League filter
|
|
if (leagueFilter !== 'all' && race.leagueId !== leagueFilter) {
|
|
return false;
|
|
}
|
|
|
|
// Time filter
|
|
if (timeFilter === 'upcoming' && !race.isUpcoming()) {
|
|
return false;
|
|
}
|
|
if (timeFilter === 'live' && !race.isLive()) {
|
|
return false;
|
|
}
|
|
if (timeFilter === 'past' && !race.isPast()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}, [pageData, statusFilter, leagueFilter, timeFilter]);
|
|
|
|
// Group races by date for calendar view
|
|
const racesByDate = useMemo(() => {
|
|
const grouped = new Map<string, Array<{ race: Race; leagueName: string }>>();
|
|
filteredRaces.forEach(item => {
|
|
const dateKey = item.race.scheduledAt.toISOString().split('T')[0];
|
|
if (!grouped.has(dateKey)) {
|
|
grouped.set(dateKey, []);
|
|
}
|
|
grouped.get(dateKey)!.push(item);
|
|
});
|
|
return grouped;
|
|
}, [filteredRaces]);
|
|
|
|
const upcomingRaces = pageData?.upcomingRaces ?? [];
|
|
const liveRaces = pageData?.liveRaces ?? [];
|
|
const recentResults = pageData?.recentResults ?? [];
|
|
const stats = pageData?.stats ?? { total: 0, scheduled: 0, running: 0, completed: 0 };
|
|
|
|
const formatDate = (date: Date) => {
|
|
return new Date(date).toLocaleDateString('en-US', {
|
|
weekday: 'short',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
});
|
|
};
|
|
|
|
const formatTime = (date: Date) => {
|
|
return new Date(date).toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
const formatFullDate = (date: Date) => {
|
|
return new Date(date).toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
});
|
|
};
|
|
|
|
const getRelativeTime = (date: Date) => {
|
|
const now = new Date();
|
|
const targetDate = new Date(date);
|
|
const diffMs = targetDate.getTime() - now.getTime();
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffMs < 0) return 'Past';
|
|
if (diffHours < 1) return 'Starting soon';
|
|
if (diffHours < 24) return `In ${diffHours}h`;
|
|
if (diffDays === 1) return 'Tomorrow';
|
|
if (diffDays < 7) return `In ${diffDays} days`;
|
|
return formatDate(date);
|
|
};
|
|
|
|
const statusConfig = {
|
|
scheduled: {
|
|
icon: Clock,
|
|
color: 'text-primary-blue',
|
|
bg: 'bg-primary-blue/10',
|
|
border: 'border-primary-blue/30',
|
|
label: 'Scheduled',
|
|
},
|
|
running: {
|
|
icon: PlayCircle,
|
|
color: 'text-performance-green',
|
|
bg: 'bg-performance-green/10',
|
|
border: 'border-performance-green/30',
|
|
label: 'LIVE',
|
|
},
|
|
completed: {
|
|
icon: CheckCircle2,
|
|
color: 'text-gray-400',
|
|
bg: 'bg-gray-500/10',
|
|
border: 'border-gray-500/30',
|
|
label: 'Completed',
|
|
},
|
|
cancelled: {
|
|
icon: XCircle,
|
|
color: 'text-warning-amber',
|
|
bg: 'bg-warning-amber/10',
|
|
border: 'border-warning-amber/30',
|
|
label: 'Cancelled',
|
|
},
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-deep-graphite py-8 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="animate-pulse space-y-6">
|
|
<div className="h-10 bg-iron-gray rounded w-1/4" />
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
{[1, 2, 3, 4].map(i => (
|
|
<div key={i} className="h-24 bg-iron-gray rounded-lg" />
|
|
))}
|
|
</div>
|
|
<div className="h-64 bg-iron-gray rounded-lg" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-deep-graphite py-8 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-7xl mx-auto space-y-8">
|
|
{/* Hero Header */}
|
|
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-iron-gray via-iron-gray to-charcoal-outline border border-charcoal-outline p-8">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-primary-blue/5 rounded-full blur-3xl" />
|
|
<div className="absolute bottom-0 left-0 w-48 h-48 bg-performance-green/5 rounded-full blur-3xl" />
|
|
|
|
<div className="relative z-10">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div className="p-2 bg-primary-blue/10 rounded-lg">
|
|
<Flag className="w-6 h-6 text-primary-blue" />
|
|
</div>
|
|
<Heading level={1} className="text-3xl font-bold text-white">
|
|
Race Calendar
|
|
</Heading>
|
|
</div>
|
|
<p className="text-gray-400 max-w-2xl">
|
|
Track upcoming races, view live events, and explore results across all your leagues.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Quick Stats */}
|
|
<div className="relative z-10 grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
|
|
<div className="bg-deep-graphite/60 backdrop-blur rounded-xl p-4 border border-charcoal-outline/50">
|
|
<div className="flex items-center gap-2 text-gray-400 text-sm mb-1">
|
|
<CalendarDays className="w-4 h-4" />
|
|
<span>Total</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-white">{stats.total}</p>
|
|
</div>
|
|
<div className="bg-deep-graphite/60 backdrop-blur rounded-xl p-4 border border-charcoal-outline/50">
|
|
<div className="flex items-center gap-2 text-primary-blue text-sm mb-1">
|
|
<Clock className="w-4 h-4" />
|
|
<span>Scheduled</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-white">{stats.scheduled}</p>
|
|
</div>
|
|
<div className="bg-deep-graphite/60 backdrop-blur rounded-xl p-4 border border-charcoal-outline/50">
|
|
<div className="flex items-center gap-2 text-performance-green text-sm mb-1">
|
|
<Zap className="w-4 h-4" />
|
|
<span>Live Now</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-white">{stats.running}</p>
|
|
</div>
|
|
<div className="bg-deep-graphite/60 backdrop-blur rounded-xl p-4 border border-charcoal-outline/50">
|
|
<div className="flex items-center gap-2 text-gray-400 text-sm mb-1">
|
|
<Trophy className="w-4 h-4" />
|
|
<span>Completed</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-white">{stats.completed}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Live Races Banner */}
|
|
{liveRaces.length > 0 && (
|
|
<div className="relative overflow-hidden rounded-xl bg-gradient-to-r from-performance-green/20 via-performance-green/10 to-transparent border border-performance-green/30 p-6">
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-performance-green/20 rounded-full blur-2xl animate-pulse" />
|
|
|
|
<div className="relative z-10">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<div className="flex items-center gap-2 px-3 py-1 bg-performance-green/20 rounded-full">
|
|
<span className="w-2 h-2 bg-performance-green rounded-full animate-pulse" />
|
|
<span className="text-performance-green font-semibold text-sm">LIVE NOW</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{liveRaces.map(({ race, leagueName }) => (
|
|
<div
|
|
key={race.id}
|
|
onClick={() => router.push(`/races/${race.id}`)}
|
|
className="flex items-center justify-between p-4 bg-deep-graphite/80 rounded-lg border border-performance-green/20 cursor-pointer hover:border-performance-green/40 transition-all"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-2 bg-performance-green/20 rounded-lg">
|
|
<PlayCircle className="w-5 h-5 text-performance-green" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-white">{race.track}</h3>
|
|
<p className="text-sm text-gray-400">{leagueName}</p>
|
|
</div>
|
|
</div>
|
|
<ChevronRight className="w-5 h-5 text-gray-400" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Main Content - Race List */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
{/* Filters */}
|
|
<Card className="!p-4">
|
|
<div className="flex flex-wrap gap-4">
|
|
{/* Time Filter Tabs */}
|
|
<div className="flex items-center gap-1 p-1 bg-deep-graphite rounded-lg">
|
|
{(['upcoming', 'live', 'past', 'all'] as TimeFilter[]).map(filter => (
|
|
<button
|
|
key={filter}
|
|
onClick={() => setTimeFilter(filter)}
|
|
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
|
|
timeFilter === filter
|
|
? 'bg-primary-blue text-white'
|
|
: 'text-gray-400 hover:text-white'
|
|
}`}
|
|
>
|
|
{filter === 'live' && <span className="inline-block w-2 h-2 bg-performance-green rounded-full mr-2 animate-pulse" />}
|
|
{filter.charAt(0).toUpperCase() + filter.slice(1)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* League Filter */}
|
|
<select
|
|
value={leagueFilter}
|
|
onChange={(e) => setLeagueFilter(e.target.value)}
|
|
className="px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
|
>
|
|
<option value="all">All Leagues</option>
|
|
{pageData && [...new Set(pageData.races.map(r => r.race.leagueId))].map(leagueId => {
|
|
const item = pageData.races.find(r => r.race.leagueId === leagueId);
|
|
return item ? (
|
|
<option key={leagueId} value={leagueId}>
|
|
{item.leagueName}
|
|
</option>
|
|
) : null;
|
|
})}
|
|
</select>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Race List by Date */}
|
|
{filteredRaces.length === 0 ? (
|
|
<Card className="text-center py-12">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div className="p-4 bg-iron-gray rounded-full">
|
|
<Calendar className="w-8 h-8 text-gray-500" />
|
|
</div>
|
|
<div>
|
|
<p className="text-white font-medium mb-1">No races found</p>
|
|
<p className="text-sm text-gray-500">
|
|
{pageData?.races.length === 0
|
|
? 'No races have been scheduled yet'
|
|
: 'Try adjusting your filters'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{Array.from(racesByDate.entries()).map(([dateKey, dayRaces]) => (
|
|
<div key={dateKey} className="space-y-3">
|
|
{/* Date Header */}
|
|
<div className="flex items-center gap-3 px-2">
|
|
<div className="p-2 bg-primary-blue/10 rounded-lg">
|
|
<Calendar className="w-4 h-4 text-primary-blue" />
|
|
</div>
|
|
<span className="text-sm font-semibold text-white">
|
|
{formatFullDate(new Date(dateKey))}
|
|
</span>
|
|
<span className="text-xs text-gray-500">
|
|
{dayRaces.length} race{dayRaces.length !== 1 ? 's' : ''}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Races for this date */}
|
|
<div className="space-y-2">
|
|
{dayRaces.map(({ race, leagueName }) => {
|
|
const config = statusConfig[race.status];
|
|
const StatusIcon = config.icon;
|
|
|
|
return (
|
|
<div
|
|
key={race.id}
|
|
onClick={() => router.push(`/races/${race.id}`)}
|
|
className={`group relative overflow-hidden rounded-xl bg-iron-gray border ${config.border} p-4 cursor-pointer transition-all duration-200 hover:scale-[1.01] hover:border-primary-blue`}
|
|
>
|
|
{/* Live indicator */}
|
|
{race.status === 'running' && (
|
|
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-performance-green via-performance-green/50 to-performance-green animate-pulse" />
|
|
)}
|
|
|
|
<div className="flex items-start gap-4">
|
|
{/* Time Column */}
|
|
<div className="flex-shrink-0 text-center min-w-[60px]">
|
|
<p className="text-lg font-bold text-white">{formatTime(race.scheduledAt)}</p>
|
|
<p className={`text-xs ${config.color}`}>
|
|
{race.status === 'running' ? 'LIVE' : getRelativeTime(race.scheduledAt)}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<div className={`w-px self-stretch ${config.bg}`} />
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="min-w-0">
|
|
<h3 className="font-semibold text-white truncate group-hover:text-primary-blue transition-colors">
|
|
{race.track}
|
|
</h3>
|
|
<div className="flex items-center gap-3 mt-1">
|
|
<span className="flex items-center gap-1 text-sm text-gray-400">
|
|
<Car className="w-3.5 h-3.5" />
|
|
{race.car}
|
|
</span>
|
|
{race.strengthOfField && (
|
|
<span className="flex items-center gap-1 text-sm text-gray-400">
|
|
<Zap className="w-3.5 h-3.5 text-warning-amber" />
|
|
SOF {race.strengthOfField}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Badge */}
|
|
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full ${config.bg} ${config.border} border`}>
|
|
<StatusIcon className={`w-3.5 h-3.5 ${config.color}`} />
|
|
<span className={`text-xs font-medium ${config.color}`}>
|
|
{config.label}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* League Link */}
|
|
<div className="mt-3 pt-3 border-t border-charcoal-outline/50">
|
|
<Link
|
|
href={`/leagues/${race.leagueId}`}
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="inline-flex items-center gap-2 text-sm text-primary-blue hover:underline"
|
|
>
|
|
<Trophy className="w-3.5 h-3.5" />
|
|
{leagueName}
|
|
<ArrowRight className="w-3 h-3" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Arrow */}
|
|
<ChevronRight className="w-5 h-5 text-gray-500 group-hover:text-primary-blue transition-colors flex-shrink-0" />
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* View All Link */}
|
|
{filteredRaces.length > 0 && (
|
|
<div className="text-center">
|
|
<Link
|
|
href="/races/all"
|
|
className="inline-flex items-center gap-2 px-6 py-3 bg-iron-gray border border-charcoal-outline rounded-lg text-white hover:border-primary-blue transition-colors"
|
|
>
|
|
View All Races
|
|
<ArrowRight className="w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<div className="space-y-6">
|
|
{/* Upcoming This Week */}
|
|
<Card>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="font-semibold text-white flex items-center gap-2">
|
|
<Clock className="w-4 h-4 text-primary-blue" />
|
|
Next Up
|
|
</h3>
|
|
<span className="text-xs text-gray-500">This week</span>
|
|
</div>
|
|
|
|
{upcomingRaces.length === 0 ? (
|
|
<p className="text-sm text-gray-500 text-center py-4">
|
|
No races scheduled this week
|
|
</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{upcomingRaces.map(({ race }) => (
|
|
<div
|
|
key={race.id}
|
|
onClick={() => router.push(`/races/${race.id}`)}
|
|
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite cursor-pointer transition-colors"
|
|
>
|
|
<div className="flex-shrink-0 w-10 h-10 bg-primary-blue/10 rounded-lg flex items-center justify-center">
|
|
<span className="text-sm font-bold text-primary-blue">
|
|
{new Date(race.scheduledAt).getDate()}
|
|
</span>
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium text-white truncate">{race.track}</p>
|
|
<p className="text-xs text-gray-500">{formatTime(race.scheduledAt)}</p>
|
|
</div>
|
|
<ChevronRight className="w-4 h-4 text-gray-500" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Recent Results */}
|
|
<Card>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="font-semibold text-white flex items-center gap-2">
|
|
<Trophy className="w-4 h-4 text-warning-amber" />
|
|
Recent Results
|
|
</h3>
|
|
</div>
|
|
|
|
{recentResults.length === 0 ? (
|
|
<p className="text-sm text-gray-500 text-center py-4">
|
|
No completed races yet
|
|
</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{recentResults.map(({ race }) => (
|
|
<div
|
|
key={race.id}
|
|
onClick={() => router.push(`/races/${race.id}/results`)}
|
|
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite cursor-pointer transition-colors"
|
|
>
|
|
<div className="flex-shrink-0 w-10 h-10 bg-gray-500/10 rounded-lg flex items-center justify-center">
|
|
<CheckCircle2 className="w-5 h-5 text-gray-400" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium text-white truncate">{race.track}</p>
|
|
<p className="text-xs text-gray-500">{formatDate(race.scheduledAt)}</p>
|
|
</div>
|
|
<ChevronRight className="w-4 h-4 text-gray-500" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Quick Actions */}
|
|
<Card>
|
|
<h3 className="font-semibold text-white mb-4">Quick Actions</h3>
|
|
<div className="space-y-2">
|
|
<Link
|
|
href="/leagues"
|
|
className="flex items-center gap-3 p-3 rounded-lg bg-deep-graphite hover:bg-charcoal-outline/50 transition-colors"
|
|
>
|
|
<div className="p-2 bg-primary-blue/10 rounded-lg">
|
|
<Users className="w-4 h-4 text-primary-blue" />
|
|
</div>
|
|
<span className="text-sm text-white">Browse Leagues</span>
|
|
<ChevronRight className="w-4 h-4 text-gray-500 ml-auto" />
|
|
</Link>
|
|
<Link
|
|
href="/leaderboards"
|
|
className="flex items-center gap-3 p-3 rounded-lg bg-deep-graphite hover:bg-charcoal-outline/50 transition-colors"
|
|
>
|
|
<div className="p-2 bg-warning-amber/10 rounded-lg">
|
|
<Trophy className="w-4 h-4 text-warning-amber" />
|
|
</div>
|
|
<span className="text-sm text-white">View Leaderboards</span>
|
|
<ChevronRight className="w-4 h-4 text-gray-500 ml-auto" />
|
|
</Link>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |