This commit is contained in:
2025-12-08 23:52:36 +01:00
parent 2d0860d66c
commit 35f988f885
46 changed files with 4624 additions and 1041 deletions

View File

@@ -18,6 +18,7 @@ type AuthContextValue = {
loading: boolean;
login: (returnTo?: string) => void;
logout: () => Promise<void>;
refreshSession: () => Promise<void>;
};
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
@@ -32,41 +33,34 @@ export function AuthProvider({ initialSession = null, children }: AuthProviderPr
const [session, setSession] = useState<AuthSession | null>(initialSession);
const [loading, setLoading] = useState(false);
const fetchSession = useCallback(async () => {
try {
const res = await fetch('/api/auth/session', {
method: 'GET',
credentials: 'include',
});
if (!res.ok) {
setSession(null);
return;
}
const data = (await res.json()) as { session: AuthSession | null };
setSession(data.session ?? null);
} catch {
setSession(null);
}
}, []);
const refreshSession = useCallback(async () => {
await fetchSession();
}, [fetchSession]);
useEffect(() => {
if (initialSession) return;
let cancelled = false;
async function loadSession() {
try {
const res = await fetch('/api/auth/session', {
method: 'GET',
credentials: 'include',
});
if (!res.ok) {
if (!cancelled) setSession(null);
return;
}
const data = (await res.json()) as { session: AuthSession | null };
if (!cancelled) {
setSession(data.session ?? null);
}
} catch {
if (!cancelled) {
setSession(null);
}
}
}
loadSession();
return () => {
cancelled = true;
};
}, [initialSession]);
fetchSession();
}, [initialSession, fetchSession]);
const login = useCallback(
(returnTo?: string) => {
@@ -105,8 +99,9 @@ export function AuthProvider({ initialSession = null, children }: AuthProviderPr
loading,
login,
logout,
refreshSession,
}),
[session, loading, login, logout],
[session, loading, login, logout, refreshSession],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;

View File

@@ -12,6 +12,8 @@ import { Result } from '@gridpilot/racing/domain/entities/Result';
import { Standing } from '@gridpilot/racing/domain/entities/Standing';
import { Game } from '@gridpilot/racing/domain/entities/Game';
import { Season } from '@gridpilot/racing/domain/entities/Season';
import { Track } from '@gridpilot/racing/domain/entities/Track';
import { Car } from '@gridpilot/racing/domain/entities/Car';
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
@@ -22,6 +24,8 @@ import type { IPenaltyRepository } from '@gridpilot/racing/domain/repositories/I
import type { IGameRepository } from '@gridpilot/racing/domain/repositories/IGameRepository';
import type { ISeasonRepository } from '@gridpilot/racing/domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '@gridpilot/racing/domain/repositories/ILeagueScoringConfigRepository';
import type { ITrackRepository } from '@gridpilot/racing/domain/repositories/ITrackRepository';
import type { ICarRepository } from '@gridpilot/racing/domain/repositories/ICarRepository';
import type {
ITeamRepository,
ITeamMembershipRepository,
@@ -39,6 +43,8 @@ import { InMemoryRaceRepository } from '@gridpilot/racing/infrastructure/reposit
import { InMemoryResultRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryResultRepository';
import { InMemoryStandingRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryStandingRepository';
import { InMemoryPenaltyRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryPenaltyRepository';
import { InMemoryTrackRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryTrackRepository';
import { InMemoryCarRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryCarRepository';
import {
InMemoryGameRepository,
InMemorySeasonRepository,
@@ -75,7 +81,10 @@ import {
GetLeagueScoringConfigQuery,
CreateLeagueWithSeasonAndScoringUseCase,
GetLeagueFullConfigQuery,
GetRaceWithSOFQuery,
GetLeagueStatsQuery,
} from '@gridpilot/racing/application';
import type { DriverRatingProvider } from '@gridpilot/racing/application';
import {
createStaticRacingSeed,
type RacingSeedData,
@@ -172,6 +181,8 @@ class DIContainer {
private _feedRepository: IFeedRepository;
private _socialRepository: ISocialGraphRepository;
private _imageService: ImageServicePort;
private _trackRepository: ITrackRepository;
private _carRepository: ICarRepository;
// Racing application use-cases / queries
private _joinLeagueUseCase: JoinLeagueUseCase;
@@ -189,6 +200,9 @@ class DIContainer {
private _getLeagueFullConfigQuery: GetLeagueFullConfigQuery;
// Placeholder for future schedule preview wiring
private _previewLeagueScheduleQuery: PreviewLeagueScheduleQuery;
private _getRaceWithSOFQuery: GetRaceWithSOFQuery;
private _getLeagueStatsQuery: GetLeagueStatsQuery;
private _driverRatingProvider: DriverRatingProvider;
private _createTeamUseCase: CreateTeamUseCase;
private _joinTeamUseCase: JoinTeamUseCase;
@@ -226,8 +240,33 @@ class DIContainer {
this._leagueRepository
);
// Race registrations (start empty; populated via use-cases)
this._raceRegistrationRepository = new InMemoryRaceRegistrationRepository();
// Race registrations - seed from results for completed races, plus some upcoming races
const seedRaceRegistrations: Array<{ raceId: string; driverId: string; registeredAt: Date }> = [];
// For completed races, extract driver registrations from results
for (const result of seedData.results) {
seedRaceRegistrations.push({
raceId: result.raceId,
driverId: result.driverId,
registeredAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
});
}
// For some upcoming races, add random registrations
const upcomingRaces = seedData.races.filter(r => r.status === 'scheduled').slice(0, 10);
for (const race of upcomingRaces) {
const participantCount = Math.floor(Math.random() * 12) + 8; // 8-20 participants
const shuffledDrivers = [...seedData.drivers].sort(() => Math.random() - 0.5).slice(0, participantCount);
for (const driver of shuffledDrivers) {
seedRaceRegistrations.push({
raceId: race.id,
driverId: driver.id,
registeredAt: new Date(Date.now() - Math.floor(Math.random() * 5) * 24 * 60 * 60 * 1000),
});
}
}
this._raceRegistrationRepository = new InMemoryRaceRegistrationRepository(seedRaceRegistrations);
// Penalties (seeded in-memory adapter)
this._penaltyRepository = new InMemoryPenaltyRepository();
@@ -490,6 +529,39 @@ class DIContainer {
// Schedule preview query (used by league creation wizard step 3)
this._previewLeagueScheduleQuery = new PreviewLeagueScheduleQuery();
// DriverRatingProvider adapter using driverStats
this._driverRatingProvider = {
getRating: (driverId: string): number | null => {
const stats = driverStats[driverId];
return stats?.rating ?? null;
},
getRatings: (driverIds: string[]): Map<string, number> => {
const result = new Map<string, number>();
for (const id of driverIds) {
const stats = driverStats[id];
if (stats?.rating) {
result.set(id, stats.rating);
}
}
return result;
},
};
// SOF queries
this._getRaceWithSOFQuery = new GetRaceWithSOFQuery(
this._raceRepository,
this._raceRegistrationRepository,
this._resultRepository,
this._driverRatingProvider,
);
this._getLeagueStatsQuery = new GetLeagueStatsQuery(
this._leagueRepository,
this._raceRepository,
this._resultRepository,
this._driverRatingProvider,
);
this._createTeamUseCase = new CreateTeamUseCase(
this._teamRepository,
this._teamMembershipRepository,
@@ -529,6 +601,184 @@ class DIContainer {
// Image service backed by demo adapter
this._imageService = new DemoImageServiceAdapter();
// Seed Track and Car data for demo
const seedTracks = [
Track.create({
id: 'track-spa',
name: 'Spa-Francorchamps',
shortName: 'SPA',
country: 'Belgium',
category: 'road',
difficulty: 'advanced',
lengthKm: 7.004,
turns: 19,
imageUrl: '/images/tracks/spa.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-monza',
name: 'Autodromo Nazionale Monza',
shortName: 'MON',
country: 'Italy',
category: 'road',
difficulty: 'intermediate',
lengthKm: 5.793,
turns: 11,
imageUrl: '/images/tracks/monza.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-nurburgring',
name: 'Nürburgring Grand Prix',
shortName: 'NUR',
country: 'Germany',
category: 'road',
difficulty: 'advanced',
lengthKm: 5.148,
turns: 15,
imageUrl: '/images/tracks/nurburgring.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-silverstone',
name: 'Silverstone Circuit',
shortName: 'SIL',
country: 'United Kingdom',
category: 'road',
difficulty: 'intermediate',
lengthKm: 5.891,
turns: 18,
imageUrl: '/images/tracks/silverstone.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-suzuka',
name: 'Suzuka International Racing Course',
shortName: 'SUZ',
country: 'Japan',
category: 'road',
difficulty: 'expert',
lengthKm: 5.807,
turns: 18,
imageUrl: '/images/tracks/suzuka.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-daytona',
name: 'Daytona International Speedway',
shortName: 'DAY',
country: 'United States',
category: 'oval',
difficulty: 'intermediate',
lengthKm: 4.023,
turns: 4,
imageUrl: '/images/tracks/daytona.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-laguna',
name: 'WeatherTech Raceway Laguna Seca',
shortName: 'LAG',
country: 'United States',
category: 'road',
difficulty: 'advanced',
lengthKm: 3.602,
turns: 11,
imageUrl: '/images/tracks/laguna.jpg',
gameId: 'iracing',
}),
];
const seedCars = [
Car.create({
id: 'car-porsche-992',
name: '911 GT3 R',
shortName: '992 GT3R',
manufacturer: 'Porsche',
carClass: 'gt',
license: 'B',
year: 2023,
horsepower: 565,
weight: 1300,
gameId: 'iracing',
}),
Car.create({
id: 'car-ferrari-296',
name: '296 GT3',
shortName: '296 GT3',
manufacturer: 'Ferrari',
carClass: 'gt',
license: 'B',
year: 2023,
horsepower: 600,
weight: 1270,
gameId: 'iracing',
}),
Car.create({
id: 'car-mclaren-720s',
name: '720S GT3 Evo',
shortName: '720S',
manufacturer: 'McLaren',
carClass: 'gt',
license: 'B',
year: 2023,
horsepower: 552,
weight: 1290,
gameId: 'iracing',
}),
Car.create({
id: 'car-mercedes-gt3',
name: 'AMG GT3 2020',
shortName: 'AMG GT3',
manufacturer: 'Mercedes',
carClass: 'gt',
license: 'B',
year: 2020,
horsepower: 550,
weight: 1285,
gameId: 'iracing',
}),
Car.create({
id: 'car-lmp2',
name: 'Dallara P217 LMP2',
shortName: 'LMP2',
manufacturer: 'Dallara',
carClass: 'prototype',
license: 'A',
year: 2021,
horsepower: 600,
weight: 930,
gameId: 'iracing',
}),
Car.create({
id: 'car-f4',
name: 'Formula 4',
shortName: 'F4',
manufacturer: 'Tatuus',
carClass: 'formula',
license: 'D',
year: 2022,
horsepower: 160,
weight: 570,
gameId: 'iracing',
}),
Car.create({
id: 'car-mx5',
name: 'MX-5 Cup',
shortName: 'MX5',
manufacturer: 'Mazda',
carClass: 'sports',
license: 'D',
year: 2023,
horsepower: 181,
weight: 1128,
gameId: 'iracing',
}),
];
this._trackRepository = new InMemoryTrackRepository(seedTracks);
this._carRepository = new InMemoryCarRepository(seedCars);
}
/**
@@ -652,6 +902,18 @@ class DIContainer {
return this._previewLeagueScheduleQuery;
}
get getRaceWithSOFQuery(): GetRaceWithSOFQuery {
return this._getRaceWithSOFQuery;
}
get getLeagueStatsQuery(): GetLeagueStatsQuery {
return this._getLeagueStatsQuery;
}
get driverRatingProvider(): DriverRatingProvider {
return this._driverRatingProvider;
}
get createLeagueWithSeasonAndScoringUseCase(): CreateLeagueWithSeasonAndScoringUseCase {
return this._createLeagueWithSeasonAndScoringUseCase;
}
@@ -719,6 +981,14 @@ class DIContainer {
get imageService(): ImageServicePort {
return this._imageService;
}
get trackRepository(): ITrackRepository {
return this._trackRepository;
}
get carRepository(): ICarRepository {
return this._carRepository;
}
}
/**
@@ -813,6 +1083,18 @@ export function getCreateLeagueWithSeasonAndScoringUseCase(): CreateLeagueWithSe
return DIContainer.getInstance().createLeagueWithSeasonAndScoringUseCase;
}
export function getGetRaceWithSOFQuery(): GetRaceWithSOFQuery {
return DIContainer.getInstance().getRaceWithSOFQuery;
}
export function getGetLeagueStatsQuery(): GetLeagueStatsQuery {
return DIContainer.getInstance().getLeagueStatsQuery;
}
export function getDriverRatingProvider(): DriverRatingProvider {
return DIContainer.getInstance().driverRatingProvider;
}
export function getTeamRepository(): ITeamRepository {
return DIContainer.getInstance().teamRepository;
}
@@ -877,6 +1159,14 @@ export function getImageService(): ImageServicePort {
return DIContainer.getInstance().imageService;
}
export function getTrackRepository(): ITrackRepository {
return DIContainer.getInstance().trackRepository;
}
export function getCarRepository(): ICarRepository {
return DIContainer.getInstance().carRepository;
}
/**
* Reset function for testing
*/