league service
This commit is contained in:
@@ -11,6 +11,7 @@ import AlphaFooter from '@/components/alpha/AlphaFooter';
|
||||
import { AuthProvider } from '@/lib/auth/AuthContext';
|
||||
import NotificationProvider from '@/components/notifications/NotificationProvider';
|
||||
import DevToolbar from '@/components/dev/DevToolbar';
|
||||
import { initializeDIContainer } from '@/lib/di-setup';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -49,6 +50,7 @@ export default async function RootLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
await initializeDIContainer();
|
||||
const mode = getAppMode();
|
||||
|
||||
if (mode === 'alpha') {
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
||||
import {
|
||||
loadLeagueSchedule,
|
||||
registerForRace,
|
||||
withdrawFromRace,
|
||||
type LeagueScheduleRaceItemViewModel,
|
||||
} from '@/lib/presenters/LeagueSchedulePresenter';
|
||||
import { createLeagueSchedulePresenter } from '@/lib/presenters/factories';
|
||||
import type { LeagueScheduleRaceItemViewModel } from '@/lib/presenters/LeagueSchedulePresenter';
|
||||
|
||||
interface LeagueScheduleProps {
|
||||
leagueId: string;
|
||||
|
||||
@@ -4,12 +4,11 @@ import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Button from '../ui/Button';
|
||||
import Input from '../ui/Input';
|
||||
import {
|
||||
loadScheduleRaceFormLeagues,
|
||||
scheduleRaceFromForm,
|
||||
type ScheduleRaceFormData,
|
||||
type ScheduledRaceViewModel,
|
||||
type LeagueOptionViewModel,
|
||||
import { createScheduleRaceFormPresenter } from '@/lib/presenters/factories';
|
||||
import type {
|
||||
ScheduleRaceFormData,
|
||||
ScheduledRaceViewModel,
|
||||
LeagueOptionViewModel,
|
||||
} from '@/lib/presenters/ScheduleRaceFormPresenter';
|
||||
|
||||
interface ScheduleRaceFormProps {
|
||||
|
||||
25
apps/website/lib/app.module.ts
Normal file
25
apps/website/lib/app.module.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LeagueModule } from './modules/league/LeagueModule';
|
||||
import { DriverModule } from './modules/driver/DriverModule';
|
||||
import { TeamModule } from './modules/team/TeamModule';
|
||||
import { RaceModule } from './modules/race/RaceModule';
|
||||
import { SponsorModule } from './modules/sponsor/SponsorModule';
|
||||
import { AuthModule } from './modules/auth/AuthModule';
|
||||
import { MediaModule } from './modules/media/MediaModule';
|
||||
import { AnalyticsModule } from './modules/analytics/AnalyticsModule';
|
||||
import { LoggingModule } from './modules/logging/LoggingModule';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
LoggingModule,
|
||||
LeagueModule,
|
||||
DriverModule,
|
||||
TeamModule,
|
||||
RaceModule,
|
||||
SponsorModule,
|
||||
AuthModule,
|
||||
MediaModule,
|
||||
AnalyticsModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
25
apps/website/lib/di-setup.ts
Normal file
25
apps/website/lib/di-setup.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { INestApplicationContext } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
let appContext: INestApplicationContext | null = null;
|
||||
|
||||
export async function initializeDIContainer(): Promise<void> {
|
||||
if (appContext) {
|
||||
return; // Already initialized
|
||||
}
|
||||
|
||||
appContext = await NestFactory.createApplicationContext(AppModule);
|
||||
}
|
||||
|
||||
export function getDIContainer(): INestApplicationContext {
|
||||
if (!appContext) {
|
||||
throw new Error('DI container not initialized. Call initializeDIContainer() first.');
|
||||
}
|
||||
return appContext;
|
||||
}
|
||||
|
||||
export async function getService<T>(token: string | symbol): Promise<T> {
|
||||
const container = getDIContainer();
|
||||
return container.get<T>(token);
|
||||
}
|
||||
9
apps/website/lib/modules/analytics/AnalyticsModule.ts
Normal file
9
apps/website/lib/modules/analytics/AnalyticsModule.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AnalyticsProviders, PAGE_VIEW_REPOSITORY_TOKEN, ENGAGEMENT_REPOSITORY_TOKEN } from './AnalyticsProviders';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: AnalyticsProviders,
|
||||
exports: [PAGE_VIEW_REPOSITORY_TOKEN, ENGAGEMENT_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
30
apps/website/lib/modules/analytics/AnalyticsProviders.ts
Normal file
30
apps/website/lib/modules/analytics/AnalyticsProviders.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
// Import core interfaces
|
||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
|
||||
// Import implementations
|
||||
import { InMemoryPageViewRepository } from '@gridpilot/adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
|
||||
import { InMemoryEngagementRepository } from '@gridpilot/adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
|
||||
|
||||
// Import tokens
|
||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
||||
|
||||
// Define injection tokens
|
||||
export const PAGE_VIEW_REPOSITORY_TOKEN = Symbol('IPageViewRepository');
|
||||
export const ENGAGEMENT_REPOSITORY_TOKEN = Symbol('IEngagementRepository');
|
||||
|
||||
export const AnalyticsProviders: Provider[] = [
|
||||
{
|
||||
provide: PAGE_VIEW_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryPageViewRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: ENGAGEMENT_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryEngagementRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/auth/AuthModule.ts
Normal file
9
apps/website/lib/modules/auth/AuthModule.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthProviders, AUTH_REPOSITORY_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: AuthProviders,
|
||||
exports: [AUTH_REPOSITORY_TOKEN, USER_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class AuthModule {}
|
||||
30
apps/website/lib/modules/auth/AuthProviders.ts
Normal file
30
apps/website/lib/modules/auth/AuthProviders.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
// Import core interfaces
|
||||
import { IAuthRepository } from '@gridpilot/identity/domain/repositories/IAuthRepository';
|
||||
import { IUserRepository } from '@gridpilot/identity/domain/repositories/IUserRepository';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
|
||||
// Import implementations
|
||||
import { InMemoryAuthRepository } from '@gridpilot/adapters/identity/persistence/inmemory/InMemoryAuthRepository';
|
||||
import { InMemoryUserRepository } from '@gridpilot/adapters/identity/persistence/inmemory/InMemoryUserRepository';
|
||||
|
||||
// Import tokens
|
||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
||||
|
||||
// Define injection tokens
|
||||
export const AUTH_REPOSITORY_TOKEN = Symbol('IAuthRepository');
|
||||
export const USER_REPOSITORY_TOKEN = Symbol('IUserRepository');
|
||||
|
||||
export const AuthProviders: Provider[] = [
|
||||
{
|
||||
provide: AUTH_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryAuthRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: USER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryUserRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/driver/DriverModule.ts
Normal file
9
apps/website/lib/modules/driver/DriverModule.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DriverProviders, DRIVER_REPOSITORY_TOKEN } from './DriverProviders';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: DriverProviders,
|
||||
exports: [DRIVER_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class DriverModule {}
|
||||
22
apps/website/lib/modules/driver/DriverProviders.ts
Normal file
22
apps/website/lib/modules/driver/DriverProviders.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
// Import core interfaces
|
||||
import { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
|
||||
// Import implementations
|
||||
import { InMemoryDriverRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
|
||||
// Import tokens
|
||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
||||
|
||||
// Define injection tokens
|
||||
export const DRIVER_REPOSITORY_TOKEN = Symbol('IDriverRepository');
|
||||
|
||||
export const DriverProviders: Provider[] = [
|
||||
{
|
||||
provide: DRIVER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/league/LeagueModule.ts
Normal file
9
apps/website/lib/modules/league/LeagueModule.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LeagueProviders, GET_LEAGUE_STANDINGS_USE_CASE_TOKEN } from './LeagueProviders';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: LeagueProviders,
|
||||
exports: [GET_LEAGUE_STANDINGS_USE_CASE_TOKEN],
|
||||
})
|
||||
export class LeagueModule {}
|
||||
30
apps/website/lib/modules/league/LeagueProviders.ts
Normal file
30
apps/website/lib/modules/league/LeagueProviders.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
// Import core interfaces
|
||||
import { GetLeagueStandingsUseCase } from '@gridpilot/league/application/use-cases/GetLeagueStandingsUseCase';
|
||||
import { ILeagueStandingsRepository } from '@gridpilot/league/application/ports/ILeagueStandingsRepository';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
|
||||
// Import implementations
|
||||
import { GetLeagueStandingsUseCaseImpl } from '@gridpilot/league/application/use-cases/GetLeagueStandingsUseCaseImpl';
|
||||
import { InMemoryLeagueStandingsRepository } from '@gridpilot/adapters/league/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
||||
|
||||
// Import tokens
|
||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
||||
|
||||
// Define injection tokens
|
||||
export const GET_LEAGUE_STANDINGS_USE_CASE_TOKEN = Symbol('GetLeagueStandingsUseCase');
|
||||
export const LEAGUE_STANDINGS_REPOSITORY_TOKEN = Symbol('ILeagueStandingsRepository');
|
||||
|
||||
export const LeagueProviders: Provider[] = [
|
||||
{
|
||||
provide: GET_LEAGUE_STANDINGS_USE_CASE_TOKEN,
|
||||
useFactory: (repository: ILeagueStandingsRepository, logger: Logger) => new GetLeagueStandingsUseCaseImpl(repository),
|
||||
inject: [LEAGUE_STANDINGS_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_STANDINGS_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueStandingsRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
17
apps/website/lib/modules/logging/LoggingModule.ts
Normal file
17
apps/website/lib/modules/logging/LoggingModule.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
import { ConsoleLogger } from '@gridpilot/adapters/logging/ConsoleLogger';
|
||||
|
||||
export const LOGGER_TOKEN = Symbol('Logger');
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
],
|
||||
exports: [LOGGER_TOKEN],
|
||||
})
|
||||
export class LoggingModule {}
|
||||
9
apps/website/lib/modules/media/MediaModule.ts
Normal file
9
apps/website/lib/modules/media/MediaModule.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MediaProviders, AVATAR_GENERATION_REPOSITORY_TOKEN } from './MediaProviders';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: MediaProviders,
|
||||
exports: [AVATAR_GENERATION_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class MediaModule {}
|
||||
22
apps/website/lib/modules/media/MediaProviders.ts
Normal file
22
apps/website/lib/modules/media/MediaProviders.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
// Import core interfaces
|
||||
import { IAvatarGenerationRepository } from '@gridpilot/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
|
||||
// Import implementations
|
||||
import { InMemoryAvatarGenerationRepository } from '@gridpilot/adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
|
||||
|
||||
// Import tokens
|
||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
||||
|
||||
// Define injection tokens
|
||||
export const AVATAR_GENERATION_REPOSITORY_TOKEN = Symbol('IAvatarGenerationRepository');
|
||||
|
||||
export const MediaProviders: Provider[] = [
|
||||
{
|
||||
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryAvatarGenerationRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/race/RaceModule.ts
Normal file
9
apps/website/lib/modules/race/RaceModule.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RaceProviders, RACE_REPOSITORY_TOKEN } from './RaceProviders';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: RaceProviders,
|
||||
exports: [RACE_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class RaceModule {}
|
||||
22
apps/website/lib/modules/race/RaceProviders.ts
Normal file
22
apps/website/lib/modules/race/RaceProviders.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
// Import core interfaces
|
||||
import { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
|
||||
// Import implementations
|
||||
import { InMemoryRaceRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
|
||||
// Import tokens
|
||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
||||
|
||||
// Define injection tokens
|
||||
export const RACE_REPOSITORY_TOKEN = Symbol('IRaceRepository');
|
||||
|
||||
export const RaceProviders: Provider[] = [
|
||||
{
|
||||
provide: RACE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/sponsor/SponsorModule.ts
Normal file
9
apps/website/lib/modules/sponsor/SponsorModule.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SponsorProviders, SPONSOR_REPOSITORY_TOKEN } from './SponsorProviders';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: SponsorProviders,
|
||||
exports: [SPONSOR_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class SponsorModule {}
|
||||
22
apps/website/lib/modules/sponsor/SponsorProviders.ts
Normal file
22
apps/website/lib/modules/sponsor/SponsorProviders.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
// Import core interfaces
|
||||
import { ISponsorRepository } from '@gridpilot/racing/domain/repositories/ISponsorRepository';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
|
||||
// Import implementations
|
||||
import { InMemorySponsorRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
||||
|
||||
// Import tokens
|
||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
||||
|
||||
// Define injection tokens
|
||||
export const SPONSOR_REPOSITORY_TOKEN = Symbol('ISponsorRepository');
|
||||
|
||||
export const SponsorProviders: Provider[] = [
|
||||
{
|
||||
provide: SPONSOR_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemorySponsorRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/team/TeamModule.ts
Normal file
9
apps/website/lib/modules/team/TeamModule.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TeamProviders, TEAM_REPOSITORY_TOKEN } from './TeamProviders';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: TeamProviders,
|
||||
exports: [TEAM_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class TeamModule {}
|
||||
22
apps/website/lib/modules/team/TeamProviders.ts
Normal file
22
apps/website/lib/modules/team/TeamProviders.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
// Import core interfaces
|
||||
import { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
|
||||
// Import implementations
|
||||
import { InMemoryTeamRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
|
||||
// Import tokens
|
||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
||||
|
||||
// Define injection tokens
|
||||
export const TEAM_REPOSITORY_TOKEN = Symbol('ITeamRepository');
|
||||
|
||||
export const TeamProviders: Provider[] = [
|
||||
{
|
||||
provide: TEAM_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryTeamRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
import {
|
||||
getRaceRepository,
|
||||
getIsDriverRegisteredForRaceQuery,
|
||||
getRegisterForRaceUseCase,
|
||||
getWithdrawFromRaceUseCase,
|
||||
} from '@/lib/di-container';
|
||||
import type { IRaceRepository } from '@gridpilot/racing/application/ports/IRaceRepository';
|
||||
import type { IIsDriverRegisteredForRaceQuery } from '@gridpilot/racing/application/queries/IIsDriverRegisteredForRaceQuery';
|
||||
import type { IRegisterForRaceUseCase } from '@gridpilot/racing/application/use-cases/IRegisterForRaceUseCase';
|
||||
import type { IWithdrawFromRaceUseCase } from '@gridpilot/racing/application/use-cases/IWithdrawFromRaceUseCase';
|
||||
|
||||
export interface LeagueScheduleRaceItemViewModel {
|
||||
id: string;
|
||||
@@ -23,85 +21,95 @@ export interface LeagueScheduleViewModel {
|
||||
races: LeagueScheduleRaceItemViewModel[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load league schedule with registration status for a given driver.
|
||||
*/
|
||||
export async function loadLeagueSchedule(
|
||||
leagueId: string,
|
||||
driverId: string,
|
||||
): Promise<LeagueScheduleViewModel> {
|
||||
const raceRepo = getRaceRepository();
|
||||
const isRegisteredQuery = getIsDriverRegisteredForRaceQuery();
|
||||
export interface ILeagueSchedulePresenter {
|
||||
loadLeagueSchedule(leagueId: string, driverId: string): Promise<LeagueScheduleViewModel>;
|
||||
registerForRace(raceId: string, leagueId: string, driverId: string): Promise<void>;
|
||||
withdrawFromRace(raceId: string, driverId: string): Promise<void>;
|
||||
}
|
||||
|
||||
const allRaces = await raceRepo.findAll();
|
||||
const leagueRaces = allRaces
|
||||
.filter((race) => race.leagueId === leagueId)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime(),
|
||||
export class LeagueSchedulePresenter implements ILeagueSchedulePresenter {
|
||||
constructor(
|
||||
private raceRepository: IRaceRepository,
|
||||
private isDriverRegisteredForRaceQuery: IIsDriverRegisteredForRaceQuery,
|
||||
private registerForRaceUseCase: IRegisterForRaceUseCase,
|
||||
private withdrawFromRaceUseCase: IWithdrawFromRaceUseCase,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Load league schedule with registration status for a given driver.
|
||||
*/
|
||||
async loadLeagueSchedule(
|
||||
leagueId: string,
|
||||
driverId: string,
|
||||
): Promise<LeagueScheduleViewModel> {
|
||||
const allRaces = await this.raceRepository.findAll();
|
||||
const leagueRaces = allRaces
|
||||
.filter((race) => race.leagueId === leagueId)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime(),
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const registrationStates: Record<string, boolean> = {};
|
||||
await Promise.all(
|
||||
leagueRaces.map(async (race) => {
|
||||
const registered = await this.isDriverRegisteredForRaceQuery.execute({
|
||||
raceId: race.id,
|
||||
driverId,
|
||||
});
|
||||
registrationStates[race.id] = registered;
|
||||
}),
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
const races: LeagueScheduleRaceItemViewModel[] = leagueRaces.map((race) => {
|
||||
const raceDate = new Date(race.scheduledAt);
|
||||
const isPast = race.status === 'completed' || raceDate <= now;
|
||||
const isUpcoming = race.status === 'scheduled' && raceDate > now;
|
||||
|
||||
const registrationStates: Record<string, boolean> = {};
|
||||
await Promise.all(
|
||||
leagueRaces.map(async (race) => {
|
||||
const registered = await isRegisteredQuery.execute({
|
||||
raceId: race.id,
|
||||
driverId,
|
||||
});
|
||||
registrationStates[race.id] = registered;
|
||||
}),
|
||||
);
|
||||
return {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
sessionType: race.sessionType,
|
||||
scheduledAt: raceDate,
|
||||
status: race.status,
|
||||
isUpcoming,
|
||||
isPast,
|
||||
isRegistered: registrationStates[race.id] ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
const races: LeagueScheduleRaceItemViewModel[] = leagueRaces.map((race) => {
|
||||
const raceDate = new Date(race.scheduledAt);
|
||||
const isPast = race.status === 'completed' || raceDate <= now;
|
||||
const isUpcoming = race.status === 'scheduled' && raceDate > now;
|
||||
return { races };
|
||||
}
|
||||
|
||||
return {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
sessionType: race.sessionType,
|
||||
scheduledAt: raceDate,
|
||||
status: race.status,
|
||||
isUpcoming,
|
||||
isPast,
|
||||
isRegistered: registrationStates[race.id] ?? false,
|
||||
};
|
||||
});
|
||||
/**
|
||||
* Register the driver for a race.
|
||||
*/
|
||||
async registerForRace(
|
||||
raceId: string,
|
||||
leagueId: string,
|
||||
driverId: string,
|
||||
): Promise<void> {
|
||||
await this.registerForRaceUseCase.execute({
|
||||
raceId,
|
||||
leagueId,
|
||||
driverId,
|
||||
});
|
||||
}
|
||||
|
||||
return { races };
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the driver for a race.
|
||||
*/
|
||||
export async function registerForRace(
|
||||
raceId: string,
|
||||
leagueId: string,
|
||||
driverId: string,
|
||||
): Promise<void> {
|
||||
const useCase = getRegisterForRaceUseCase();
|
||||
await useCase.execute({
|
||||
raceId,
|
||||
leagueId,
|
||||
driverId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw the driver from a race.
|
||||
*/
|
||||
export async function withdrawFromRace(
|
||||
raceId: string,
|
||||
driverId: string,
|
||||
): Promise<void> {
|
||||
const useCase = getWithdrawFromRaceUseCase();
|
||||
await useCase.execute({
|
||||
raceId,
|
||||
driverId,
|
||||
});
|
||||
/**
|
||||
* Withdraw the driver from a race.
|
||||
*/
|
||||
async withdrawFromRace(
|
||||
raceId: string,
|
||||
driverId: string,
|
||||
): Promise<void> {
|
||||
await this.withdrawFromRaceUseCase.execute({
|
||||
raceId,
|
||||
driverId,
|
||||
});
|
||||
}
|
||||
}
|
||||
101
apps/website/lib/services/LeagueMembershipService.ts
Normal file
101
apps/website/lib/services/LeagueMembershipService.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
import type {
|
||||
LeagueMembership as DomainLeagueMembership,
|
||||
MembershipRole,
|
||||
MembershipStatus,
|
||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
|
||||
/**
|
||||
* Lightweight league membership model mirroring the domain type but with
|
||||
* a stringified joinedAt for easier UI formatting.
|
||||
*/
|
||||
export interface LeagueMembership extends Omit<DomainLeagueMembership, 'joinedAt'> {
|
||||
joinedAt: string;
|
||||
}
|
||||
|
||||
export class LeagueMembershipService {
|
||||
private leagueMemberships = new Map<string, LeagueMembership[]>();
|
||||
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly membershipRepository: ILeagueMembershipRepository,
|
||||
) {
|
||||
this.initializeLeagueMembershipsFromRepository();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize league memberships once from the in-memory league membership repository
|
||||
* that is seeded via the static racing seed in the DI container.
|
||||
*
|
||||
* This avoids depending on raw testing-support seed exports and keeps all demo
|
||||
* membership data flowing through the same in-memory repositories used elsewhere.
|
||||
*/
|
||||
private async initializeLeagueMembershipsFromRepository() {
|
||||
if (this.leagueMemberships.size > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const allLeagues = await this.leagueRepository.findAll();
|
||||
const byLeague = new Map<string, LeagueMembership[]>();
|
||||
|
||||
for (const league of allLeagues) {
|
||||
const memberships = await this.membershipRepository.getLeagueMembers(league.id);
|
||||
|
||||
const mapped: LeagueMembership[] = memberships.map((membership) => ({
|
||||
id: membership.id,
|
||||
leagueId: membership.leagueId,
|
||||
driverId: membership.driverId,
|
||||
role: membership.role,
|
||||
status: membership.status,
|
||||
joinedAt:
|
||||
membership.joinedAt instanceof Date
|
||||
? membership.joinedAt.toISOString()
|
||||
: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
byLeague.set(league.id, mapped);
|
||||
}
|
||||
|
||||
for (const [leagueId, list] of byLeague.entries()) {
|
||||
this.leagueMemberships.set(leagueId, list);
|
||||
}
|
||||
} catch (error) {
|
||||
// In alpha/demo mode we tolerate failures here; callers will see empty memberships.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to initialize league memberships from repository', error);
|
||||
}
|
||||
}
|
||||
|
||||
getMembership(leagueId: string, driverId: string): LeagueMembership | null {
|
||||
const list = this.leagueMemberships.get(leagueId);
|
||||
if (!list) return null;
|
||||
return list.find((m) => m.driverId === driverId) ?? null;
|
||||
}
|
||||
|
||||
getLeagueMembers(leagueId: string): LeagueMembership[] {
|
||||
return [...(this.leagueMemberships.get(leagueId) ?? [])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a driver's primary league from in-memory league memberships.
|
||||
* Prefers any active membership and returns the first matching league.
|
||||
*/
|
||||
getPrimaryLeagueIdForDriver(driverId: string): string | null {
|
||||
for (const [leagueId, members] of this.leagueMemberships.entries()) {
|
||||
if (members.some((m) => m.driverId === driverId && m.status === 'active')) {
|
||||
return leagueId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
isOwnerOrAdmin(leagueId: string, driverId: string): boolean {
|
||||
const membership = this.getMembership(leagueId, driverId);
|
||||
if (!membership) return false;
|
||||
return membership.role === 'owner' || membership.role === 'admin';
|
||||
}
|
||||
}
|
||||
|
||||
export type { MembershipRole, MembershipStatus };
|
||||
Reference in New Issue
Block a user