277 lines
8.9 KiB
TypeScript
277 lines
8.9 KiB
TypeScript
'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>
|
|
);
|
|
} |