232 lines
8.0 KiB
TypeScript
232 lines
8.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import Button from '@/components/ui/Button';
|
|
import Card from '@/components/ui/Card';
|
|
import RaceCard from '@/components/alpha/RaceCard';
|
|
import ScheduleRaceForm from '@/components/alpha/ScheduleRaceForm';
|
|
import { Race, RaceStatus } from '@gridpilot/racing-domain/entities/Race';
|
|
import { League } from '@gridpilot/racing-domain/entities/League';
|
|
import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
|
|
|
|
export default function RacesPage() {
|
|
const router = useRouter();
|
|
|
|
const [races, setRaces] = useState<Race[]>([]);
|
|
const [leagues, setLeagues] = useState<Map<string, League>>(new Map());
|
|
const [loading, setLoading] = useState(true);
|
|
const [showScheduleForm, setShowScheduleForm] = useState(false);
|
|
const [preselectedLeagueId, setPreselectedLeagueId] = useState<string | undefined>(undefined);
|
|
|
|
// Filters
|
|
const [statusFilter, setStatusFilter] = useState<RaceStatus | 'all'>('all');
|
|
const [leagueFilter, setLeagueFilter] = useState<string>('all');
|
|
const [timeFilter, setTimeFilter] = useState<'all' | 'upcoming' | 'past'>('all');
|
|
|
|
const loadRaces = async () => {
|
|
try {
|
|
const raceRepo = getRaceRepository();
|
|
const leagueRepo = getLeagueRepository();
|
|
|
|
const [allRaces, allLeagues] = await Promise.all([
|
|
raceRepo.findAll(),
|
|
leagueRepo.findAll()
|
|
]);
|
|
|
|
setRaces(allRaces.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime()));
|
|
|
|
const leagueMap = new Map<string, League>();
|
|
allLeagues.forEach(league => leagueMap.set(league.id, league));
|
|
setLeagues(leagueMap);
|
|
} catch (err) {
|
|
console.error('Failed to load races:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadRaces();
|
|
|
|
try {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const leagueId = params.get('leagueId') || undefined;
|
|
setPreselectedLeagueId(leagueId || undefined);
|
|
} catch {
|
|
setPreselectedLeagueId(undefined);
|
|
}
|
|
}, []);
|
|
|
|
const filteredRaces = 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 === 'past' && !race.isPast()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-deep-graphite py-12 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-6xl mx-auto">
|
|
<div className="text-center text-gray-400">Loading races...</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (showScheduleForm) {
|
|
return (
|
|
<div className="min-h-screen bg-deep-graphite py-12 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-2xl mx-auto">
|
|
<div className="mb-6">
|
|
<button
|
|
onClick={() => setShowScheduleForm(false)}
|
|
className="text-gray-400 hover:text-primary-blue transition-colors text-sm flex items-center gap-2"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Back to Races
|
|
</button>
|
|
</div>
|
|
|
|
<Card>
|
|
<h1 className="text-2xl font-bold text-white mb-6">Schedule New Race</h1>
|
|
<ScheduleRaceForm
|
|
preSelectedLeagueId={preselectedLeagueId}
|
|
onSuccess={(race) => {
|
|
router.push(`/races/${race.id}`);
|
|
}}
|
|
onCancel={() => setShowScheduleForm(false)}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-deep-graphite py-12 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-6xl mx-auto">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h1 className="text-3xl font-bold text-white">Races</h1>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => setShowScheduleForm(true)}
|
|
>
|
|
Schedule Race
|
|
</Button>
|
|
</div>
|
|
<p className="text-gray-400">
|
|
Manage and view all scheduled races across your leagues
|
|
</p>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<Card className="mb-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{/* Time Filter */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Time
|
|
</label>
|
|
<select
|
|
value={timeFilter}
|
|
onChange={(e) => setTimeFilter(e.target.value as typeof timeFilter)}
|
|
className="w-full px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
|
>
|
|
<option value="all">All Races</option>
|
|
<option value="upcoming">Upcoming</option>
|
|
<option value="past">Past</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Status Filter */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Status
|
|
</label>
|
|
<select
|
|
value={statusFilter}
|
|
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
|
|
className="w-full px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
|
>
|
|
<option value="all">All Statuses</option>
|
|
<option value="scheduled">Scheduled</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* League Filter */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
League
|
|
</label>
|
|
<select
|
|
value={leagueFilter}
|
|
onChange={(e) => setLeagueFilter(e.target.value)}
|
|
className="w-full px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
|
>
|
|
<option value="all">All Leagues</option>
|
|
{Array.from(leagues.values()).map(league => (
|
|
<option key={league.id} value={league.id}>
|
|
{league.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Race List */}
|
|
{filteredRaces.length === 0 ? (
|
|
<Card className="text-center py-12">
|
|
<div className="text-gray-400 mb-4">
|
|
{races.length === 0 ? (
|
|
<>
|
|
<p className="mb-2">No races scheduled</p>
|
|
<p className="text-sm text-gray-500">Try the full workflow in alpha mode</p>
|
|
</>
|
|
) : (
|
|
<>
|
|
<p className="mb-2">No races match your filters</p>
|
|
<p className="text-sm text-gray-500">Try adjusting your filter criteria</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{filteredRaces.map(race => (
|
|
<RaceCard
|
|
key={race.id}
|
|
race={race}
|
|
leagueName={leagues.get(race.leagueId)?.name}
|
|
onClick={() => router.push(`/races/${race.id}`)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |