This commit is contained in:
2025-12-04 23:31:55 +01:00
parent 9fa21a488a
commit fb509607c1
96 changed files with 5839 additions and 1609 deletions

View File

@@ -8,15 +8,17 @@ import FeatureLimitationTooltip from '@/components/alpha/FeatureLimitationToolti
import type { Race } from '@gridpilot/racing/domain/entities/Race';
import type { League } from '@gridpilot/racing/domain/entities/League';
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { getRaceRepository, getLeagueRepository, getDriverRepository } from '@/lib/di-container';
import {
getMembership,
getCurrentDriverId,
isRegistered,
registerForRace,
withdrawFromRace,
getRegisteredDrivers,
} from '@/lib/racingLegacyFacade';
getRaceRepository,
getLeagueRepository,
getDriverRepository,
getGetRaceRegistrationsQuery,
getIsDriverRegisteredForRaceQuery,
getRegisterForRaceUseCase,
getWithdrawFromRaceUseCase,
} from '@/lib/di-container';
import { getMembership } from '@/lib/leagueMembership';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import CompanionStatus from '@/components/alpha/CompanionStatus';
import CompanionInstructions from '@/components/alpha/CompanionInstructions';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
@@ -36,7 +38,7 @@ export default function RaceDetailPage() {
const [isUserRegistered, setIsUserRegistered] = useState(false);
const [canRegister, setCanRegister] = useState(false);
const currentDriverId = getCurrentDriverId();
const currentDriverId = useEffectiveDriverId();
const loadRaceData = async () => {
try {
@@ -69,19 +71,22 @@ 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: string) => driverRepo.findById(id))
);
setEntryList(
drivers.filter((d: Driver | null): d is Driver => d !== null)
);
// Check user registration status
const userIsRegistered = isRegistered(raceId, currentDriverId);
const raceRegistrationsQuery = getGetRaceRegistrationsQuery();
const registeredDriverIds = await raceRegistrationsQuery.execute({ raceId });
const drivers = await Promise.all(
registeredDriverIds.map((id: string) => driverRepo.findById(id)),
);
setEntryList(drivers.filter((d: Driver | null): d is Driver => d !== null));
const isRegisteredQuery = getIsDriverRegisteredForRaceQuery();
const userIsRegistered = await isRegisteredQuery.execute({
raceId,
driverId: 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);
@@ -128,7 +133,12 @@ export default function RaceDetailPage() {
setRegistering(true);
try {
registerForRace(race.id, currentDriverId, league.id);
const useCase = getRegisterForRaceUseCase();
await useCase.execute({
raceId: race.id,
leagueId: league.id,
driverId: currentDriverId,
});
await loadEntryList(race.id, league.id);
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to register for race');
@@ -148,7 +158,11 @@ export default function RaceDetailPage() {
setRegistering(true);
try {
withdrawFromRace(race.id, currentDriverId);
const useCase = getWithdrawFromRaceUseCase();
await useCase.execute({
raceId: race.id,
driverId: currentDriverId,
});
await loadEntryList(race.id, league.id);
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to withdraw from race');

View File

@@ -2,10 +2,8 @@
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/races/RaceCard';
import ScheduleRaceForm from '@/components/leagues/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';
@@ -16,8 +14,6 @@ export default function RacesPage() {
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,14 +44,6 @@ 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 => {
@@ -90,37 +78,6 @@ export default function RacesPage() {
);
}
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">
@@ -128,12 +85,6 @@ export default function RacesPage() {
<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