This commit is contained in:
2025-12-11 11:25:22 +01:00
parent 6a427eab57
commit e4c1be628d
86 changed files with 1222 additions and 736 deletions

View File

@@ -2,14 +2,13 @@
import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { Race } from '@gridpilot/racing/domain/entities/Race';
import {
getRaceRepository,
getIsDriverRegisteredForRaceQuery,
getRegisterForRaceUseCase,
getWithdrawFromRaceUseCase,
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import {
loadLeagueSchedule,
registerForRace,
withdrawFromRace,
type LeagueScheduleRaceItemViewModel,
} from '@/lib/presenters/LeagueSchedulePresenter';
interface LeagueScheduleProps {
leagueId: string;
@@ -17,7 +16,7 @@ interface LeagueScheduleProps {
export default function LeagueSchedule({ leagueId }: LeagueScheduleProps) {
const router = useRouter();
const [races, setRaces] = useState<Race[]>([]);
const [races, setRaces] = useState<LeagueScheduleRaceItemViewModel[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<'all' | 'upcoming' | 'past'>('upcoming');
const [registrationStates, setRegistrationStates] = useState<Record<string, boolean>>({});
@@ -25,30 +24,16 @@ export default function LeagueSchedule({ leagueId }: LeagueScheduleProps) {
const currentDriverId = useEffectiveDriverId();
const loadRaces = useCallback(async () => {
const loadRacesCallback = useCallback(async () => {
setLoading(true);
try {
const raceRepo = getRaceRepository();
const allRaces = await raceRepo.findAll();
const leagueRaces = allRaces
.filter((race) => race.leagueId === leagueId)
.sort(
(a, b) =>
new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime(),
);
setRaces(leagueRaces);
const viewModel = await loadLeagueSchedule(leagueId, currentDriverId);
setRaces(viewModel.races);
const isRegisteredQuery = getIsDriverRegisteredForRaceQuery();
const states: Record<string, boolean> = {};
await Promise.all(
leagueRaces.map(async (race) => {
const registered = await isRegisteredQuery.execute({
raceId: race.id,
driverId: currentDriverId,
});
states[race.id] = registered;
}),
);
for (const race of viewModel.races) {
states[race.id] = race.isRegistered;
}
setRegistrationStates(states);
} catch (error) {
console.error('Failed to load races:', error);
@@ -58,27 +43,19 @@ export default function LeagueSchedule({ leagueId }: LeagueScheduleProps) {
}, [leagueId, currentDriverId]);
useEffect(() => {
loadRaces();
}, [loadRaces]);
void loadRacesCallback();
}, [loadRacesCallback]);
const handleRegister = async (race: Race, e: React.MouseEvent) => {
const handleRegister = async (race: LeagueScheduleRaceItemViewModel, e: React.MouseEvent) => {
e.stopPropagation();
const confirmed = window.confirm(
`Register for ${race.track}?`
);
const confirmed = window.confirm(`Register for ${race.track}?`);
if (!confirmed) return;
setProcessingRace(race.id);
try {
const useCase = getRegisterForRaceUseCase();
await useCase.execute({
raceId: race.id,
leagueId,
driverId: currentDriverId,
});
await registerForRace(race.id, leagueId, currentDriverId);
setRegistrationStates((prev) => ({ ...prev, [race.id]: true }));
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to register');
@@ -87,22 +64,16 @@ export default function LeagueSchedule({ leagueId }: LeagueScheduleProps) {
}
};
const handleWithdraw = async (race: Race, e: React.MouseEvent) => {
const handleWithdraw = async (race: LeagueScheduleRaceItemViewModel, e: React.MouseEvent) => {
e.stopPropagation();
const confirmed = window.confirm(
'Withdraw from this race?'
);
const confirmed = window.confirm('Withdraw from this race?');
if (!confirmed) return;
setProcessingRace(race.id);
try {
const useCase = getWithdrawFromRaceUseCase();
await useCase.execute({
raceId: race.id,
driverId: currentDriverId,
});
await withdrawFromRace(race.id, currentDriverId);
setRegistrationStates((prev) => ({ ...prev, [race.id]: false }));
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to withdraw');
@@ -111,18 +82,17 @@ export default function LeagueSchedule({ leagueId }: LeagueScheduleProps) {
}
};
const now = new Date();
const upcomingRaces = races.filter(race => race.status === 'scheduled' && new Date(race.scheduledAt) > now);
const pastRaces = races.filter(race => race.status === 'completed' || new Date(race.scheduledAt) <= now);
const upcomingRaces = races.filter((race) => race.isUpcoming);
const pastRaces = races.filter((race) => race.isPast);
const getDisplayRaces = () => {
switch (filter) {
case 'upcoming':
return upcomingRaces;
case 'past':
return pastRaces.reverse();
return [...pastRaces].reverse();
case 'all':
return [...upcomingRaces, ...pastRaces.reverse()];
return [...upcomingRaces, ...[...pastRaces].reverse()];
default:
return races;
}
@@ -190,8 +160,8 @@ export default function LeagueSchedule({ leagueId }: LeagueScheduleProps) {
) : (
<div className="space-y-3">
{displayRaces.map((race) => {
const isPast = race.status === 'completed' || new Date(race.scheduledAt) <= now;
const isUpcoming = race.status === 'scheduled' && new Date(race.scheduledAt) > now;
const isPast = race.isPast;
const isUpcoming = race.isUpcoming;
return (
<div
@@ -231,19 +201,19 @@ export default function LeagueSchedule({ leagueId }: LeagueScheduleProps) {
<div className="flex items-center gap-3">
<div className="text-right">
<p className="text-white font-medium">
{new Date(race.scheduledAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</p>
<p className="text-sm text-gray-400">
{new Date(race.scheduledAt).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</p>
<p className="text-white font-medium">
{race.scheduledAt.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</p>
<p className="text-sm text-gray-400">
{race.scheduledAt.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</p>
{isPast && race.status === 'completed' && (
<p className="text-xs text-primary-blue mt-1">View Results </p>
)}