Files
gridpilot.gg/apps/api/src/domain/race/presenters/RaceDetailPresenter.ts
2025-12-20 17:06:11 +01:00

108 lines
4.2 KiB
TypeScript

import type { RaceDetailOutputPort } from '@core/racing/application/ports/output/RaceDetailOutputPort';
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import type { GetRaceDetailParamsDTO } from '../dtos/GetRaceDetailParamsDTO';
import type { RaceDetailDTO } from '../dtos/RaceDetailDTO';
import type { RaceDetailRaceDTO } from '../dtos/RaceDetailRaceDTO';
import type { RaceDetailLeagueDTO } from '../dtos/RaceDetailLeagueDTO';
import type { RaceDetailEntryDTO } from '../dtos/RaceDetailEntryDTO';
import type { RaceDetailRegistrationDTO } from '../dtos/RaceDetailRegistrationDTO';
import type { RaceDetailUserResultDTO } from '../dtos/RaceDetailUserResultDTO';
export class RaceDetailPresenter {
private result: RaceDetailDTO | null = null;
constructor(
private readonly driverRatingProvider: DriverRatingProvider,
private readonly imageService: IImageServicePort,
) {}
async present(outputPort: RaceDetailOutputPort, params: GetRaceDetailParamsDTO): Promise<void> {
const raceDTO: RaceDetailRaceDTO | null = outputPort.race
? {
id: outputPort.race.id,
leagueId: outputPort.race.leagueId,
track: outputPort.race.track,
car: outputPort.race.car,
scheduledAt: outputPort.race.scheduledAt.toISOString(),
sessionType: outputPort.race.sessionType,
status: outputPort.race.status,
strengthOfField: outputPort.race.strengthOfField ?? null,
registeredCount: outputPort.race.registeredCount ?? undefined,
maxParticipants: outputPort.race.maxParticipants ?? undefined,
}
: null;
const leagueDTO: RaceDetailLeagueDTO | null = outputPort.league
? {
id: outputPort.league.id.toString(),
name: outputPort.league.name.toString(),
description: outputPort.league.description.toString(),
settings: {
maxDrivers: outputPort.league.settings.maxDrivers ?? undefined,
qualifyingFormat: outputPort.league.settings.qualifyingFormat ?? undefined,
},
}
: null;
const entryListDTO: RaceDetailEntryDTO[] = await Promise.all(
outputPort.drivers.map(async driver => {
const ratingResult = await this.driverRatingProvider.getDriverRating({ driverId: driver.id });
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
return {
id: driver.id,
name: driver.name.toString(),
country: driver.country.toString(),
avatarUrl: avatarResult.avatarUrl,
rating: ratingResult.rating,
isCurrentUser: driver.id === params.driverId,
};
}),
);
const registrationDTO: RaceDetailRegistrationDTO = {
isUserRegistered: outputPort.isUserRegistered,
canRegister: outputPort.canRegister,
};
const userResultDTO: RaceDetailUserResultDTO | null = outputPort.userResult
? {
position: outputPort.userResult.position.toNumber(),
startPosition: outputPort.userResult.startPosition.toNumber(),
incidents: outputPort.userResult.incidents.toNumber(),
fastestLap: outputPort.userResult.fastestLap.toNumber(),
positionChange: outputPort.userResult.getPositionChange(),
isPodium: outputPort.userResult.isPodium(),
isClean: outputPort.userResult.isClean(),
ratingChange: this.calculateRatingChange(outputPort.userResult.position.toNumber()),
}
: null;
this.result = {
race: raceDTO,
league: leagueDTO,
entryList: entryListDTO,
registration: registrationDTO,
userResult: userResultDTO,
} as RaceDetailDTO;
}
getViewModel(): RaceDetailDTO | null {
return this.result;
}
get viewModel(): RaceDetailDTO {
if (!this.result) {
throw new Error('Presenter not presented');
}
return this.result;
}
private calculateRatingChange(position: number): number {
const baseChange = position <= 3 ? 25 : position <= 10 ? 10 : -5;
const positionBonus = Math.max(0, (20 - position) * 2);
return baseChange + positionBonus;
}
}