alpha wip
This commit is contained in:
277
apps/website/app/races/[id]/page.tsx
Normal file
277
apps/website/app/races/[id]/page.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import FeatureLimitationTooltip from '@/components/alpha/FeatureLimitationTooltip';
|
||||
import { Race } from '@/domain/entities/Race';
|
||||
import { League } from '@/domain/entities/League';
|
||||
import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
|
||||
import CompanionStatus from '@/components/alpha/CompanionStatus';
|
||||
import CompanionInstructions from '@/components/alpha/CompanionInstructions';
|
||||
|
||||
export default function RaceDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const raceId = params.id as string;
|
||||
|
||||
const [race, setRace] = useState<Race | null>(null);
|
||||
const [league, setLeague] = useState<League | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
|
||||
const loadRaceData = async () => {
|
||||
try {
|
||||
const raceRepo = getRaceRepository();
|
||||
const leagueRepo = getLeagueRepository();
|
||||
|
||||
const raceData = await raceRepo.findById(raceId);
|
||||
|
||||
if (!raceData) {
|
||||
setError('Race not found');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setRace(raceData);
|
||||
|
||||
// Load league data
|
||||
const leagueData = await leagueRepo.findById(raceData.leagueId);
|
||||
setLeague(leagueData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load race');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadRaceData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [raceId]);
|
||||
|
||||
const handleCancelRace = async () => {
|
||||
if (!race || race.status !== 'scheduled') return;
|
||||
|
||||
const confirmed = window.confirm(
|
||||
'Are you sure you want to cancel this race? This action cannot be undone.'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setCancelling(true);
|
||||
try {
|
||||
const raceRepo = getRaceRepository();
|
||||
const cancelledRace = race.cancel();
|
||||
await raceRepo.update(cancelledRace);
|
||||
setRace(cancelledRace);
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Failed to cancel race');
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return new Date(date).toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
};
|
||||
|
||||
const formatDateTime = (date: Date) => {
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
};
|
||||
|
||||
const statusColors = {
|
||||
scheduled: 'bg-primary-blue/20 text-primary-blue border-primary-blue/30',
|
||||
completed: 'bg-green-500/20 text-green-400 border-green-500/30',
|
||||
cancelled: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
|
||||
};
|
||||
|
||||
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-4xl mx-auto">
|
||||
<div className="text-center text-gray-400">Loading race details...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !race) {
|
||||
return (
|
||||
<div className="min-h-screen bg-deep-graphite py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Card className="text-center py-12">
|
||||
<div className="text-warning-amber mb-4">
|
||||
{error || 'Race not found'}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => router.push('/races')}
|
||||
>
|
||||
Back to Races
|
||||
</Button>
|
||||
</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-4xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => router.push('/races')}
|
||||
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>
|
||||
|
||||
{/* Companion Status */}
|
||||
<FeatureLimitationTooltip message="Companion automation available in production">
|
||||
<div className="mb-6">
|
||||
<CompanionStatus />
|
||||
</div>
|
||||
</FeatureLimitationTooltip>
|
||||
|
||||
{/* Race Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">{race.track}</h1>
|
||||
{league && (
|
||||
<p className="text-gray-400">{league.name}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className={`px-3 py-1 text-sm font-medium rounded border ${statusColors[race.status]}`}>
|
||||
{race.status.charAt(0).toUpperCase() + race.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Companion Instructions for Scheduled Races */}
|
||||
{race.status === 'scheduled' && (
|
||||
<div className="mb-6">
|
||||
<CompanionInstructions race={race} leagueName={league?.name} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Race Details */}
|
||||
<Card className="lg:col-span-2">
|
||||
<h2 className="text-xl font-semibold text-white mb-6">Race Details</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Date & Time */}
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 block mb-1">Scheduled Date & Time</label>
|
||||
<p className="text-white text-lg font-medium">
|
||||
{formatDateTime(race.scheduledAt)}
|
||||
</p>
|
||||
<div className="flex gap-4 mt-2 text-sm">
|
||||
<span className="text-gray-400">{formatDate(race.scheduledAt)}</span>
|
||||
<span className="text-gray-400">{formatTime(race.scheduledAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Track */}
|
||||
<div className="pt-4 border-t border-charcoal-outline">
|
||||
<label className="text-sm text-gray-500 block mb-1">Track</label>
|
||||
<p className="text-white">{race.track}</p>
|
||||
</div>
|
||||
|
||||
{/* Car */}
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 block mb-1">Car</label>
|
||||
<p className="text-white">{race.car}</p>
|
||||
</div>
|
||||
|
||||
{/* Session Type */}
|
||||
<div>
|
||||
<label className="text-sm text-gray-500 block mb-1">Session Type</label>
|
||||
<p className="text-white capitalize">{race.sessionType}</p>
|
||||
</div>
|
||||
|
||||
{/* League */}
|
||||
<div className="pt-4 border-t border-charcoal-outline">
|
||||
<label className="text-sm text-gray-500 block mb-1">League</label>
|
||||
{league ? (
|
||||
<button
|
||||
onClick={() => router.push(`/leagues/${league.id}`)}
|
||||
className="text-primary-blue hover:underline"
|
||||
>
|
||||
{league.name}
|
||||
</button>
|
||||
) : (
|
||||
<p className="text-white">ID: {race.leagueId.slice(0, 8)}...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<Card>
|
||||
<h2 className="text-xl font-semibold text-white mb-4">Actions</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
{race.status === 'completed' && (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => router.push(`/races/${race.id}/results`)}
|
||||
>
|
||||
View Results
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{race.status === 'scheduled' && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={handleCancelRace}
|
||||
disabled={cancelling}
|
||||
>
|
||||
{cancelling ? 'Cancelling...' : 'Cancel Race'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => router.push('/races')}
|
||||
>
|
||||
Back to Races
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user