This commit is contained in:
2025-12-11 21:06:25 +01:00
parent c49ea2598d
commit ec3ddc3a5c
227 changed files with 3496 additions and 2083 deletions

View File

@@ -928,7 +928,7 @@ export default function RaceDetailPage() {
isOpen={showProtestModal}
onClose={() => setShowProtestModal(false)}
raceId={race.id}
leagueId={league?.id}
leagueId={league ? league.id : ''}
protestingDriverId={currentDriverId}
participants={entryList.map(d => ({ id: d.id, name: d.name }))}
/>

View File

@@ -115,13 +115,17 @@ export default function RaceResultsPage() {
setPointsSystem(viewModel.pointsSystem);
setFastestLapTime(viewModel.fastestLapTime);
setCurrentDriverId(viewModel.currentDriverId);
setPenalties(
viewModel.penalties.map((p) => ({
const mappedPenalties: PenaltyData[] = viewModel.penalties.map((p) => {
const base: PenaltyData = {
driverId: p.driverId,
type: p.type as PenaltyTypeDTO,
value: p.value,
})),
);
};
if (typeof p.value === 'number') {
return { ...base, value: p.value };
}
return base;
});
setPenalties(mappedPenalties);
}
try {
@@ -287,9 +291,9 @@ export default function RaceResultsPage() {
results={results}
drivers={drivers}
pointsSystem={pointsSystem}
fastestLapTime={fastestLapTime}
fastestLapTime={fastestLapTime ?? 0}
penalties={penalties}
currentDriverId={currentDriverId}
currentDriverId={currentDriverId ?? ''}
/>
) : (
<>

View File

@@ -31,6 +31,8 @@ import {
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import { RaceProtestsPresenter } from '@/lib/presenters/RaceProtestsPresenter';
import { RacePenaltiesPresenter } from '@/lib/presenters/RacePenaltiesPresenter';
import type { RaceProtestViewModel } from '@gridpilot/racing/application/presenters/IRaceProtestsPresenter';
import type { RacePenaltyViewModel } from '@gridpilot/racing/application/presenters/IRacePenaltiesPresenter';
import type { League } from '@gridpilot/racing/domain/entities/League';
@@ -41,7 +43,9 @@ export default function RaceStewardingPage() {
const router = useRouter();
const raceId = params.id as string;
const currentDriverId = useEffectiveDriverId();
const driversById: Record<string, { name?: string }> = {};
const [race, setRace] = useState<Race | null>(null);
const [league, setLeague] = useState<League | null>(null);
const [protests, setProtests] = useState<RaceProtestViewModel[]>([]);
@@ -78,13 +82,15 @@ export default function RaceStewardingPage() {
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
}
await protestsUseCase.execute(raceId);
const protestsViewModel = protestsUseCase.presenter.getViewModel();
setProtests(protestsViewModel.protests);
const protestsPresenter = new RaceProtestsPresenter();
await protestsUseCase.execute({ raceId }, protestsPresenter);
const protestsViewModel = protestsPresenter.getViewModel();
setProtests(protestsViewModel?.protests ?? []);
await penaltiesUseCase.execute(raceId);
const penaltiesViewModel = penaltiesUseCase.presenter.getViewModel();
setPenalties(penaltiesViewModel.penalties);
const penaltiesPresenter = new RacePenaltiesPresenter();
await penaltiesUseCase.execute({ raceId }, penaltiesPresenter);
const penaltiesViewModel = penaltiesPresenter.getViewModel();
setPenalties(penaltiesViewModel?.penalties ?? []);
} catch (err) {
console.error('Failed to load data:', err);
} finally {

View File

@@ -105,8 +105,9 @@ export default function AllRacesPage() {
setCurrentPage(1);
}, [statusFilter, leagueFilter, searchQuery]);
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
const formatDate = (date: Date | string) => {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
@@ -114,8 +115,9 @@ export default function AllRacesPage() {
});
};
const formatTime = (date: Date) => {
return new Date(date).toLocaleTimeString('en-US', {
const formatTime = (date: Date | string) => {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
});

View File

@@ -93,8 +93,11 @@ export default function RacesPage() {
// Group races by date for calendar view
const racesByDate = useMemo(() => {
const grouped = new Map<string, RaceListItemViewModel[]>();
filteredRaces.forEach(race => {
const dateKey = new Date(race.scheduledAt).toISOString().split('T')[0];
filteredRaces.forEach((race) => {
if (typeof race.scheduledAt !== 'string') {
return;
}
const dateKey = race.scheduledAt.split('T')[0]!;
if (!grouped.has(dateKey)) {
grouped.set(dateKey, []);
}
@@ -108,23 +111,26 @@ export default function RacesPage() {
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', {
const formatDate = (date: Date | string) => {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
});
};
const formatTime = (date: Date) => {
return new Date(date).toLocaleTimeString('en-US', {
const formatTime = (date: Date | string) => {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
});
};
const formatFullDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
const formatFullDate = (date: Date | string) => {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
@@ -132,9 +138,10 @@ export default function RacesPage() {
});
};
const getRelativeTime = (date: Date) => {
const getRelativeTime = (date?: Date | string) => {
if (!date) return '';
const now = new Date();
const targetDate = new Date(date);
const targetDate = typeof date === 'string' ? new Date(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));
@@ -144,7 +151,7 @@ export default function RacesPage() {
if (diffHours < 24) return `In ${diffHours}h`;
if (diffDays === 1) return 'Tomorrow';
if (diffDays < 7) return `In ${diffDays} days`;
return formatDate(date);
return formatDate(targetDate);
};
const statusConfig = {
@@ -368,6 +375,9 @@ export default function RacesPage() {
{/* Races for this date */}
<div className="space-y-2">
{dayRaces.map((race) => {
if (!race.scheduledAt) {
return null;
}
const config = statusConfig[race.status];
const StatusIcon = config.icon;
@@ -385,9 +395,13 @@ export default function RacesPage() {
<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(new Date(race.scheduledAt))}</p>
<p className="text-lg font-bold text-white">
{formatTime(race.scheduledAt)}
</p>
<p className={`text-xs ${config.color}`}>
{race.status === 'running' ? 'LIVE' : getRelativeTime(new Date(race.scheduledAt))}
{race.status === 'running'
? 'LIVE'
: getRelativeTime(race.scheduledAt)}
</p>
</div>
@@ -427,7 +441,7 @@ export default function RacesPage() {
{/* League Link */}
<div className="mt-3 pt-3 border-t border-charcoal-outline/50">
<Link
href={`/leagues/${race.leagueId}`}
href={`/leagues/${race.leagueId ?? ''}`}
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-2 text-sm text-primary-blue hover:underline"
>
@@ -482,24 +496,30 @@ export default function RacesPage() {
</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>
{upcomingRaces.map((race) => {
if (!race.scheduledAt) {
return null;
}
const scheduledAtDate = new Date(race.scheduledAt);
return (
<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">
{scheduledAtDate.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(scheduledAtDate)}</p>
</div>
<ChevronRight className="w-4 h-4 text-gray-500" />
</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(new Date(race.scheduledAt))}</p>
</div>
<ChevronRight className="w-4 h-4 text-gray-500" />
</div>
))}
);
})}
</div>
)}
</Card>