wip
This commit is contained in:
@@ -7,9 +7,19 @@ import Card from '@/components/ui/Card';
|
||||
import FeatureLimitationTooltip from '@/components/alpha/FeatureLimitationTooltip';
|
||||
import { Race } from '@gridpilot/racing-domain/entities/Race';
|
||||
import { League } from '@gridpilot/racing-domain/entities/League';
|
||||
import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
|
||||
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
|
||||
import { getRaceRepository, getLeagueRepository, getDriverRepository } from '@/lib/di-container';
|
||||
import { getMembership, getCurrentDriverId } from '@/lib/membership-data';
|
||||
import {
|
||||
isRegistered,
|
||||
registerForRace,
|
||||
withdrawFromRace,
|
||||
getRegisteredDrivers
|
||||
} from '@/lib/registration-data';
|
||||
import CompanionStatus from '@/components/alpha/CompanionStatus';
|
||||
import CompanionInstructions from '@/components/alpha/CompanionInstructions';
|
||||
import DataWarning from '@/components/alpha/DataWarning';
|
||||
import Breadcrumbs from '@/components/alpha/Breadcrumbs';
|
||||
|
||||
export default function RaceDetailPage() {
|
||||
const router = useRouter();
|
||||
@@ -21,6 +31,12 @@ export default function RaceDetailPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
const [registering, setRegistering] = useState(false);
|
||||
const [entryList, setEntryList] = useState<Driver[]>([]);
|
||||
const [isUserRegistered, setIsUserRegistered] = useState(false);
|
||||
const [canRegister, setCanRegister] = useState(false);
|
||||
|
||||
const currentDriverId = getCurrentDriverId();
|
||||
|
||||
const loadRaceData = async () => {
|
||||
try {
|
||||
@@ -40,6 +56,9 @@ export default function RaceDetailPage() {
|
||||
// Load league data
|
||||
const leagueData = await leagueRepo.findById(raceData.leagueId);
|
||||
setLeague(leagueData);
|
||||
|
||||
// Load entry list
|
||||
await loadEntryList(raceData.id, raceData.leagueId);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load race');
|
||||
} finally {
|
||||
@@ -47,6 +66,28 @@ export default function RaceDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadEntryList = async (raceId: string, leagueId: string) => {
|
||||
try {
|
||||
const driverRepo = getDriverRepository();
|
||||
const registeredDriverIds = getRegisteredDrivers(raceId);
|
||||
const drivers = await Promise.all(
|
||||
registeredDriverIds.map(id => driverRepo.findById(id))
|
||||
);
|
||||
setEntryList(drivers.filter((d): d is Driver => d !== null));
|
||||
|
||||
// Check user registration status
|
||||
const userIsRegistered = isRegistered(raceId, currentDriverId);
|
||||
setIsUserRegistered(userIsRegistered);
|
||||
|
||||
// Check if user can register (is league member and race is upcoming)
|
||||
const membership = getMembership(leagueId, currentDriverId);
|
||||
const isUpcoming = race?.status === 'scheduled';
|
||||
setCanRegister(!!membership && membership.status === 'active' && !!isUpcoming);
|
||||
} catch (err) {
|
||||
console.error('Failed to load entry list:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadRaceData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -74,6 +115,46 @@ export default function RaceDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!race || !league) return;
|
||||
|
||||
const confirmed = window.confirm(
|
||||
`Register for ${race.track}?\n\nYou'll be added to the entry list for this race.`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setRegistering(true);
|
||||
try {
|
||||
registerForRace(race.id, currentDriverId, league.id);
|
||||
await loadEntryList(race.id, league.id);
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Failed to register for race');
|
||||
} finally {
|
||||
setRegistering(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWithdraw = async () => {
|
||||
if (!race || !league) return;
|
||||
|
||||
const confirmed = window.confirm(
|
||||
'Withdraw from this race?\n\nYou can register again later if you change your mind.'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setRegistering(true);
|
||||
try {
|
||||
withdrawFromRace(race.id, currentDriverId);
|
||||
await loadEntryList(race.id, league.id);
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Failed to withdraw from race');
|
||||
} finally {
|
||||
setRegistering(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
@@ -240,6 +321,34 @@ export default function RaceDetailPage() {
|
||||
<h2 className="text-xl font-semibold text-white mb-4">Actions</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Registration Actions */}
|
||||
{race.status === 'scheduled' && canRegister && !isUserRegistered && (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={handleRegister}
|
||||
disabled={registering}
|
||||
>
|
||||
{registering ? 'Registering...' : 'Register for Race'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{race.status === 'scheduled' && isUserRegistered && (
|
||||
<div className="space-y-2">
|
||||
<div className="px-3 py-2 bg-green-500/10 border border-green-500/30 rounded text-green-400 text-sm text-center">
|
||||
✓ Registered
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={handleWithdraw}
|
||||
disabled={registering}
|
||||
>
|
||||
{registering ? 'Withdrawing...' : 'Withdraw'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{race.status === 'completed' && (
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -271,6 +380,55 @@ export default function RaceDetailPage() {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Entry List */}
|
||||
{race.status === 'scheduled' && (
|
||||
<Card className="mt-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-white">Entry List</h2>
|
||||
<span className="text-sm text-gray-400">
|
||||
{entryList.length} {entryList.length === 1 ? 'driver' : 'drivers'} registered
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<DataWarning className="mb-4" />
|
||||
|
||||
{entryList.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<p className="mb-2">No drivers registered yet</p>
|
||||
<p className="text-sm text-gray-500">Be the first to register!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{entryList.map((driver, index) => (
|
||||
<div
|
||||
key={driver.id}
|
||||
className="flex items-center gap-4 p-3 bg-iron-gray/50 rounded-lg border border-charcoal-outline hover:border-primary-blue/50 transition-colors cursor-pointer"
|
||||
onClick={() => router.push(`/drivers/${driver.id}`)}
|
||||
>
|
||||
<div className="w-8 text-center text-gray-400 font-mono text-sm">
|
||||
#{index + 1}
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-charcoal-outline rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-lg font-bold text-gray-500">
|
||||
{driver.name.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-white font-medium">{driver.name}</p>
|
||||
<p className="text-sm text-gray-400">{driver.country}</p>
|
||||
</div>
|
||||
{driver.id === currentDriverId && (
|
||||
<span className="px-2 py-1 text-xs font-medium bg-primary-blue/20 text-primary-blue rounded">
|
||||
You
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import RaceCard from '@/components/alpha/RaceCard';
|
||||
@@ -12,12 +12,12 @@ import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
|
||||
|
||||
export default function RacesPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
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');
|
||||
@@ -48,6 +48,14 @@ export default function RacesPage() {
|
||||
|
||||
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 => {
|
||||
@@ -101,7 +109,7 @@ export default function RacesPage() {
|
||||
<Card>
|
||||
<h1 className="text-2xl font-bold text-white mb-6">Schedule New Race</h1>
|
||||
<ScheduleRaceForm
|
||||
preSelectedLeagueId={searchParams.get('leagueId') || undefined}
|
||||
preSelectedLeagueId={preselectedLeagueId}
|
||||
onSuccess={(race) => {
|
||||
router.push(`/races/${race.id}`);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user