presenter refactoring

This commit is contained in:
2025-12-20 17:06:11 +01:00
parent 92be9d2e1b
commit e9d6f90bb2
109 changed files with 4159 additions and 1283 deletions

View File

@@ -0,0 +1,21 @@
import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO';
export class AllRacesPageDataPresenter {
private result: AllRacesPageDTO | null = null;
present(output: AllRacesPageDTO): void {
this.result = output;
}
getViewModel(): AllRacesPageDTO | null {
return this.result;
}
get viewModel(): AllRacesPageDTO {
if (!this.result) {
throw new Error('Presenter not presented');
}
return this.result;
}
}

View File

@@ -0,0 +1,36 @@
export interface CommandResultViewModel {
success: boolean;
errorCode?: string;
message?: string;
}
export class CommandResultPresenter {
private result: CommandResultViewModel | null = null;
presentSuccess(message?: string): void {
this.result = {
success: true,
message,
};
}
presentFailure(errorCode: string, message?: string): void {
this.result = {
success: false,
errorCode,
message,
};
}
getViewModel(): CommandResultViewModel | null {
return this.result;
}
get viewModel(): CommandResultViewModel {
if (!this.result) {
throw new Error('Presenter not presented');
}
return this.result;
}
}

View File

@@ -0,0 +1,107 @@
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;
}
}

View File

@@ -0,0 +1,42 @@
import type { RacePenaltiesOutputPort } from '@core/racing/application/ports/output/RacePenaltiesOutputPort';
import type { RacePenaltiesDTO } from '../dtos/RacePenaltiesDTO';
import type { RacePenaltyDTO } from '../dtos/RacePenaltyDTO';
export class RacePenaltiesPresenter {
private result: RacePenaltiesDTO | null = null;
present(outputPort: RacePenaltiesOutputPort): void {
const penalties: RacePenaltyDTO[] = outputPort.penalties.map(penalty => ({
id: penalty.id,
driverId: penalty.driverId,
type: penalty.type,
value: penalty.value ?? 0,
reason: penalty.reason,
issuedBy: penalty.issuedBy,
issuedAt: penalty.issuedAt.toISOString(),
notes: penalty.notes,
} as RacePenaltyDTO));
const driverMap: Record<string, string> = {};
outputPort.drivers.forEach(driver => {
driverMap[driver.id] = driver.name.toString();
});
this.result = {
penalties,
driverMap,
} as RacePenaltiesDTO;
}
getViewModel(): RacePenaltiesDTO | null {
return this.result;
}
get viewModel(): RacePenaltiesDTO {
if (!this.result) {
throw new Error('Presenter not presented');
}
return this.result;
}
}

View File

@@ -0,0 +1,43 @@
import type { RaceProtestsOutputPort } from '@core/racing/application/ports/output/RaceProtestsOutputPort';
import type { RaceProtestsDTO } from '../dtos/RaceProtestsDTO';
import type { RaceProtestDTO } from '../dtos/RaceProtestDTO';
export class RaceProtestsPresenter {
private result: RaceProtestsDTO | null = null;
present(outputPort: RaceProtestsOutputPort): void {
const protests: RaceProtestDTO[] = outputPort.protests.map(protest => ({
id: protest.id,
protestingDriverId: protest.protestingDriverId,
accusedDriverId: protest.accusedDriverId,
incident: {
lap: protest.incident.lap,
description: protest.incident.description,
},
status: protest.status,
filedAt: protest.filedAt.toISOString(),
} as RaceProtestDTO));
const driverMap: Record<string, string> = {};
outputPort.drivers.forEach(driver => {
driverMap[driver.id] = driver.name.toString();
});
this.result = {
protests,
driverMap,
} as RaceProtestsDTO;
}
getViewModel(): RaceProtestsDTO | null {
return this.result;
}
get viewModel(): RaceProtestsDTO {
if (!this.result) {
throw new Error('Presenter not presented');
}
return this.result;
}
}

View File

@@ -0,0 +1,56 @@
import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import type { RaceResultsDetailDTO } from '../dtos/RaceResultsDetailDTO';
import type { RaceResultDTO } from '../dtos/RaceResultDTO';
export class RaceResultsDetailPresenter {
private result: RaceResultsDetailDTO | null = null;
constructor(private readonly imageService: IImageServicePort) {}
async present(outputPort: RaceResultsDetailOutputPort): Promise<void> {
const driverMap = new Map(outputPort.drivers.map(driver => [driver.id, driver]));
const results: RaceResultDTO[] = await Promise.all(
outputPort.results.map(async singleResult => {
const driver = driverMap.get(singleResult.driverId.toString());
if (!driver) {
throw new Error(`Driver not found for result: ${singleResult.driverId}`);
}
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
return {
driverId: singleResult.driverId.toString(),
driverName: driver.name.toString(),
avatarUrl: avatarResult.avatarUrl,
position: singleResult.position.toNumber(),
startPosition: singleResult.startPosition.toNumber(),
incidents: singleResult.incidents.toNumber(),
fastestLap: singleResult.fastestLap.toNumber(),
positionChange: singleResult.getPositionChange(),
isPodium: singleResult.isPodium(),
isClean: singleResult.isClean(),
} as RaceResultDTO;
}),
);
this.result = {
raceId: outputPort.race.id,
track: outputPort.race.track,
results,
} as RaceResultsDetailDTO;
}
getViewModel(): RaceResultsDetailDTO | null {
return this.result;
}
get viewModel(): RaceResultsDetailDTO {
if (!this.result) {
throw new Error('Presenter not presented');
}
return this.result;
}
}

View File

@@ -0,0 +1,26 @@
import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort';
import type { RaceWithSOFDTO } from '../dtos/RaceWithSOFDTO';
export class RaceWithSOFPresenter {
private result: RaceWithSOFDTO | null = null;
present(outputPort: RaceWithSOFOutputPort): void {
this.result = {
id: outputPort.id,
track: outputPort.track,
strengthOfField: outputPort.strengthOfField,
} as RaceWithSOFDTO;
}
getViewModel(): RaceWithSOFDTO | null {
return this.result;
}
get viewModel(): RaceWithSOFDTO {
if (!this.result) {
throw new Error('Presenter not presented');
}
return this.result;
}
}

View File

@@ -0,0 +1,43 @@
import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { RacesPageDataDTO } from '../dtos/RacesPageDataDTO';
import type { RacesPageDataRaceDTO } from '../dtos/RacesPageDataRaceDTO';
export class RacesPageDataPresenter {
private result: RacesPageDataDTO | null = null;
constructor(private readonly leagueRepository: ILeagueRepository) {}
async present(outputPort: RacesPageOutputPort): Promise<void> {
const allLeagues = await this.leagueRepository.findAll();
const leagueMap = new Map(allLeagues.map(l => [l.id, l.name]));
const races: RacesPageDataRaceDTO[] = outputPort.races.map(race => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt.toISOString(),
status: race.status,
leagueId: race.leagueId,
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
strengthOfField: race.strengthOfField,
isUpcoming: race.scheduledAt > new Date(),
isLive: race.status === 'running',
isPast: race.scheduledAt < new Date() && race.status === 'completed',
}));
this.result = { races } as RacesPageDataDTO;
}
getViewModel(): RacesPageDataDTO | null {
return this.result;
}
get viewModel(): RacesPageDataDTO {
if (!this.result) {
throw new Error('Presenter not presented');
}
return this.result;
}
}