This commit is contained in:
2025-12-03 16:33:12 +01:00
parent a572e6edce
commit c0fdae3d3c
157 changed files with 7824 additions and 1042 deletions

View File

@@ -7,9 +7,19 @@ import Card from '@/components/ui/Card';
import FeatureLimitationTooltip from '@/components/alpha/FeatureLimitationTooltip';
import { Race } from '@gridpilot/racing-domain/entities/Race';
import { League } from '@gridpilot/racing-domain/entities/League';
import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
import { getRaceRepository, getLeagueRepository, getDriverRepository } from '@/lib/di-container';
import { getMembership, getCurrentDriverId } from '@/lib/membership-data';
import {
isRegistered,
registerForRace,
withdrawFromRace,
getRegisteredDrivers
} from '@/lib/registration-data';
import CompanionStatus from '@/components/alpha/CompanionStatus';
import CompanionInstructions from '@/components/alpha/CompanionInstructions';
import DataWarning from '@/components/alpha/DataWarning';
import Breadcrumbs from '@/components/alpha/Breadcrumbs';
export default function RaceDetailPage() {
const router = useRouter();
@@ -21,6 +31,12 @@ export default function RaceDetailPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [cancelling, setCancelling] = useState(false);
const [registering, setRegistering] = useState(false);
const [entryList, setEntryList] = useState<Driver[]>([]);
const [isUserRegistered, setIsUserRegistered] = useState(false);
const [canRegister, setCanRegister] = useState(false);
const currentDriverId = getCurrentDriverId();
const loadRaceData = async () => {
try {
@@ -40,6 +56,9 @@ export default function RaceDetailPage() {
// Load league data
const leagueData = await leagueRepo.findById(raceData.leagueId);
setLeague(leagueData);
// Load entry list
await loadEntryList(raceData.id, raceData.leagueId);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load race');
} finally {
@@ -47,6 +66,28 @@ 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 => driverRepo.findById(id))
);
setEntryList(drivers.filter((d): d is Driver => d !== null));
// Check user registration status
const userIsRegistered = isRegistered(raceId, 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);
} catch (err) {
console.error('Failed to load entry list:', err);
}
};
useEffect(() => {
loadRaceData();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -74,6 +115,46 @@ export default function RaceDetailPage() {
}
};
const handleRegister = async () => {
if (!race || !league) return;
const confirmed = window.confirm(
`Register for ${race.track}?\n\nYou'll be added to the entry list for this race.`
);
if (!confirmed) return;
setRegistering(true);
try {
registerForRace(race.id, currentDriverId, league.id);
await loadEntryList(race.id, league.id);
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to register for race');
} finally {
setRegistering(false);
}
};
const handleWithdraw = async () => {
if (!race || !league) return;
const confirmed = window.confirm(
'Withdraw from this race?\n\nYou can register again later if you change your mind.'
);
if (!confirmed) return;
setRegistering(true);
try {
withdrawFromRace(race.id, currentDriverId);
await loadEntryList(race.id, league.id);
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to withdraw from race');
} finally {
setRegistering(false);
}
};
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
@@ -240,6 +321,34 @@ export default function RaceDetailPage() {
<h2 className="text-xl font-semibold text-white mb-4">Actions</h2>
<div className="space-y-3">
{/* Registration Actions */}
{race.status === 'scheduled' && canRegister && !isUserRegistered && (
<Button
variant="primary"
className="w-full"
onClick={handleRegister}
disabled={registering}
>
{registering ? 'Registering...' : 'Register for Race'}
</Button>
)}
{race.status === 'scheduled' && isUserRegistered && (
<div className="space-y-2">
<div className="px-3 py-2 bg-green-500/10 border border-green-500/30 rounded text-green-400 text-sm text-center">
Registered
</div>
<Button
variant="secondary"
className="w-full"
onClick={handleWithdraw}
disabled={registering}
>
{registering ? 'Withdrawing...' : 'Withdraw'}
</Button>
</div>
)}
{race.status === 'completed' && (
<Button
variant="primary"
@@ -271,6 +380,55 @@ export default function RaceDetailPage() {
</div>
</Card>
</div>
{/* Entry List */}
{race.status === 'scheduled' && (
<Card className="mt-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-white">Entry List</h2>
<span className="text-sm text-gray-400">
{entryList.length} {entryList.length === 1 ? 'driver' : 'drivers'} registered
</span>
</div>
<DataWarning className="mb-4" />
{entryList.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<p className="mb-2">No drivers registered yet</p>
<p className="text-sm text-gray-500">Be the first to register!</p>
</div>
) : (
<div className="space-y-2">
{entryList.map((driver, index) => (
<div
key={driver.id}
className="flex items-center gap-4 p-3 bg-iron-gray/50 rounded-lg border border-charcoal-outline hover:border-primary-blue/50 transition-colors cursor-pointer"
onClick={() => router.push(`/drivers/${driver.id}`)}
>
<div className="w-8 text-center text-gray-400 font-mono text-sm">
#{index + 1}
</div>
<div className="w-10 h-10 bg-charcoal-outline rounded-full flex items-center justify-center flex-shrink-0">
<span className="text-lg font-bold text-gray-500">
{driver.name.charAt(0)}
</span>
</div>
<div className="flex-1">
<p className="text-white font-medium">{driver.name}</p>
<p className="text-sm text-gray-400">{driver.country}</p>
</div>
{driver.id === currentDriverId && (
<span className="px-2 py-1 text-xs font-medium bg-primary-blue/20 text-primary-blue rounded">
You
</span>
)}
</div>
))}
</div>
)}
</Card>
)}
</div>
</div>
);