alpha wip

This commit is contained in:
2025-12-03 00:46:08 +01:00
parent 3b55fd1a63
commit 97e29d3d80
51 changed files with 6321 additions and 237 deletions

View 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>
);
}

View File

@@ -0,0 +1,247 @@
'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 ResultsTable from '@/components/alpha/ResultsTable';
import ImportResultsForm from '@/components/alpha/ImportResultsForm';
import { Race } from '@/domain/entities/Race';
import { League } from '@/domain/entities/League';
import { Result } from '@/domain/entities/Result';
import { Driver } from '@/domain/entities/Driver';
import {
getRaceRepository,
getLeagueRepository,
getResultRepository,
getStandingRepository,
getDriverRepository
} from '@/lib/di-container';
export default function RaceResultsPage() {
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 [results, setResults] = useState<Result[]>([]);
const [drivers, setDrivers] = useState<Driver[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [importing, setImporting] = useState(false);
const [importSuccess, setImportSuccess] = useState(false);
const loadData = async () => {
try {
const raceRepo = getRaceRepository();
const leagueRepo = getLeagueRepository();
const resultRepo = getResultRepository();
const driverRepo = getDriverRepository();
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);
// Load results
const resultsData = await resultRepo.findByRaceId(raceId);
setResults(resultsData);
// Load drivers
const driversData = await driverRepo.findAll();
setDrivers(driversData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load race data');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [raceId]);
const handleImportSuccess = async (importedResults: Result[]) => {
setImporting(true);
setError(null);
try {
const resultRepo = getResultRepository();
const standingRepo = getStandingRepository();
// Check if results already exist
const existingResults = await resultRepo.existsByRaceId(raceId);
if (existingResults) {
throw new Error('Results already exist for this race');
}
// Create all results
await resultRepo.createMany(importedResults);
// Recalculate standings for the league
if (league) {
await standingRepo.recalculate(league.id);
}
// Reload results
const resultsData = await resultRepo.findByRaceId(raceId);
setResults(resultsData);
setImportSuccess(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to import results');
} finally {
setImporting(false);
}
};
const handleImportError = (errorMessage: string) => {
setError(errorMessage);
};
const getPointsSystem = (): Record<number, number> => {
if (!league) return {};
const pointsSystems: Record<string, Record<number, number>> = {
'f1-2024': {
1: 25, 2: 18, 3: 15, 4: 12, 5: 10,
6: 8, 7: 6, 8: 4, 9: 2, 10: 1
},
'indycar': {
1: 50, 2: 40, 3: 35, 4: 32, 5: 30,
6: 28, 7: 26, 8: 24, 9: 22, 10: 20,
11: 19, 12: 18, 13: 17, 14: 16, 15: 15
}
};
return league.settings.customPoints ||
pointsSystems[league.settings.pointsSystem] ||
pointsSystems['f1-2024'];
};
const getFastestLapTime = (): number | undefined => {
if (results.length === 0) return undefined;
return Math.min(...results.map(r => r.fastestLap));
};
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-6xl mx-auto">
<div className="text-center text-gray-400">Loading results...</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-6xl 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>
);
}
const hasResults = results.length > 0;
return (
<div className="min-h-screen bg-deep-graphite py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto">
{/* Breadcrumb */}
<div className="mb-6">
<button
onClick={() => router.push(`/races/${raceId}`)}
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 Race Details
</button>
</div>
{/* Page Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Race Results</h1>
{race && (
<div>
<p className="text-gray-400">{race.track}</p>
{league && (
<p className="text-sm text-gray-500">{league.name}</p>
)}
</div>
)}
</div>
{/* Success Message */}
{importSuccess && (
<div className="mb-6 p-4 bg-performance-green/10 border border-performance-green/30 rounded text-performance-green">
<strong>Success!</strong> Results imported and standings updated.
</div>
)}
{/* Error Message */}
{error && (
<div className="mb-6 p-4 bg-warning-amber/10 border border-warning-amber/30 rounded text-warning-amber">
<strong>Error:</strong> {error}
</div>
)}
{/* Content */}
<Card>
{hasResults ? (
<>
<h2 className="text-xl font-semibold text-white mb-6">Results</h2>
<ResultsTable
results={results}
drivers={drivers}
pointsSystem={getPointsSystem()}
fastestLapTime={getFastestLapTime()}
/>
</>
) : (
<>
<h2 className="text-xl font-semibold text-white mb-6">Import Results</h2>
<p className="text-gray-400 text-sm mb-6">
No results imported. Upload CSV to test the standings system.
</p>
{importing ? (
<div className="text-center py-8 text-gray-400">
Importing results and updating standings...
</div>
) : (
<ImportResultsForm
raceId={raceId}
onSuccess={handleImportSuccess}
onError={handleImportError}
/>
)}
</>
)}
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,224 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import RaceCard from '@/components/alpha/RaceCard';
import ScheduleRaceForm from '@/components/alpha/ScheduleRaceForm';
import { Race, RaceStatus } from '@/domain/entities/Race';
import { League } from '@/domain/entities/League';
import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
export default function RacesPage() {
const router = useRouter();
const searchParams = useSearchParams();
const [races, setRaces] = useState<Race[]>([]);
const [leagues, setLeagues] = useState<Map<string, League>>(new Map());
const [loading, setLoading] = useState(true);
const [showScheduleForm, setShowScheduleForm] = useState(false);
// Filters
const [statusFilter, setStatusFilter] = useState<RaceStatus | 'all'>('all');
const [leagueFilter, setLeagueFilter] = useState<string>('all');
const [timeFilter, setTimeFilter] = useState<'all' | 'upcoming' | 'past'>('all');
const loadRaces = async () => {
try {
const raceRepo = getRaceRepository();
const leagueRepo = getLeagueRepository();
const [allRaces, allLeagues] = await Promise.all([
raceRepo.findAll(),
leagueRepo.findAll()
]);
setRaces(allRaces.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime()));
const leagueMap = new Map<string, League>();
allLeagues.forEach(league => leagueMap.set(league.id, league));
setLeagues(leagueMap);
} catch (err) {
console.error('Failed to load races:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadRaces();
}, []);
const filteredRaces = races.filter(race => {
// Status filter
if (statusFilter !== 'all' && race.status !== statusFilter) {
return false;
}
// League filter
if (leagueFilter !== 'all' && race.leagueId !== leagueFilter) {
return false;
}
// Time filter
if (timeFilter === 'upcoming' && !race.isUpcoming()) {
return false;
}
if (timeFilter === 'past' && !race.isPast()) {
return false;
}
return true;
});
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-6xl mx-auto">
<div className="text-center text-gray-400">Loading races...</div>
</div>
</div>
);
}
if (showScheduleForm) {
return (
<div className="min-h-screen bg-deep-graphite py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl mx-auto">
<div className="mb-6">
<button
onClick={() => setShowScheduleForm(false)}
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>
<Card>
<h1 className="text-2xl font-bold text-white mb-6">Schedule New Race</h1>
<ScheduleRaceForm
preSelectedLeagueId={searchParams.get('leagueId') || undefined}
onSuccess={(race) => {
router.push(`/races/${race.id}`);
}}
onCancel={() => setShowScheduleForm(false)}
/>
</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-6xl mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h1 className="text-3xl font-bold text-white">Races</h1>
<Button
variant="primary"
onClick={() => setShowScheduleForm(true)}
>
Schedule Race
</Button>
</div>
<p className="text-gray-400">
Manage and view all scheduled races across your leagues
</p>
</div>
{/* Filters */}
<Card className="mb-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Time Filter */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Time
</label>
<select
value={timeFilter}
onChange={(e) => setTimeFilter(e.target.value as typeof timeFilter)}
className="w-full px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-blue"
>
<option value="all">All Races</option>
<option value="upcoming">Upcoming</option>
<option value="past">Past</option>
</select>
</div>
{/* Status Filter */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Status
</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
className="w-full px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-blue"
>
<option value="all">All Statuses</option>
<option value="scheduled">Scheduled</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
{/* League Filter */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
League
</label>
<select
value={leagueFilter}
onChange={(e) => setLeagueFilter(e.target.value)}
className="w-full px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-blue"
>
<option value="all">All Leagues</option>
{Array.from(leagues.values()).map(league => (
<option key={league.id} value={league.id}>
{league.name}
</option>
))}
</select>
</div>
</div>
</Card>
{/* Race List */}
{filteredRaces.length === 0 ? (
<Card className="text-center py-12">
<div className="text-gray-400 mb-4">
{races.length === 0 ? (
<>
<p className="mb-2">No races scheduled</p>
<p className="text-sm text-gray-500">Try the full workflow in alpha mode</p>
</>
) : (
<>
<p className="mb-2">No races match your filters</p>
<p className="text-sm text-gray-500">Try adjusting your filter criteria</p>
</>
)}
</div>
</Card>
) : (
<div className="space-y-4">
{filteredRaces.map(race => (
<RaceCard
key={race.id}
race={race}
leagueName={leagues.get(race.leagueId)?.name}
onClick={() => router.push(`/races/${race.id}`)}
/>
))}
</div>
)}
</div>
</div>
);
}