This commit is contained in:
2025-12-21 19:53:22 +01:00
parent f2d8a23583
commit 3c64f328e2
105 changed files with 3191 additions and 1706 deletions

View File

@@ -1,7 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { DashboardController } from './DashboardController';
import { DashboardService } from './DashboardService';
import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
describe('DashboardController', () => {
@@ -13,7 +11,7 @@ describe('DashboardController', () => {
getDashboardOverview: vi.fn(),
};
controller = new DashboardController(mockService as any);
controller = new DashboardController(mockService as never);
});
describe('getDashboardOverview', () => {

View File

@@ -13,7 +13,6 @@ export class DashboardController {
@ApiQuery({ name: 'driverId', description: 'Driver ID' })
@ApiResponse({ status: 200, description: 'Dashboard overview', type: DashboardOverviewDTO })
async getDashboardOverview(@Query('driverId') driverId: string): Promise<DashboardOverviewDTO> {
const presenter = await this.dashboardService.getDashboardOverview(driverId);
return presenter.viewModel;
return this.dashboardService.getDashboardOverview(driverId);
}
}

View File

@@ -1,8 +1,9 @@
import { Provider } from '@nestjs/common';
import { DashboardService } from './DashboardService';
// Import core interfaces
import type { Logger } from '@core/shared/application/Logger';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { DashboardOverviewResult } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
@@ -12,7 +13,8 @@ import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/IL
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import { ImageServicePort } from '@core/media/application/ports/ImageServicePort';
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
// Import concrete implementations
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
@@ -24,25 +26,31 @@ import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemor
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
// Simple mock implementations for missing adapters
class MockFeedRepository implements IFeedRepository {
async getFeedForDriver(driverId: string, limit?: number) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getFeedForDriver(_driverId: string, _limit?: number) {
return [];
}
async getGlobalFeed(limit?: number) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getGlobalFeed(_limit?: number) {
return [];
}
}
class MockSocialGraphRepository implements ISocialGraphRepository {
async getFriends(driverId: string) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getFriends(_driverId: string) {
return [];
}
async getFriendIds(driverId: string) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getFriendIds(_driverId: string) {
return [];
}
async getSuggestedFriends(driverId: string, limit?: number) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getSuggestedFriends(_driverId: string, _limit?: number) {
return [];
}
}
@@ -59,8 +67,11 @@ export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
export const FEED_REPOSITORY_TOKEN = 'IFeedRepository';
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const DASHBOARD_OVERVIEW_USE_CASE_TOKEN = 'DashboardOverviewUseCase';
export const DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN = 'DashboardOverviewOutputPort';
export const DashboardProviders: Provider[] = [
DashboardOverviewPresenter,
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
@@ -113,4 +124,51 @@ export const DashboardProviders: Provider[] = [
useFactory: (logger: Logger) => new InMemoryImageServiceAdapter(logger),
inject: [LOGGER_TOKEN],
},
{
provide: DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
useExisting: DashboardOverviewPresenter,
},
{
provide: DASHBOARD_OVERVIEW_USE_CASE_TOKEN,
useFactory: (
driverRepo: IDriverRepository,
raceRepo: IRaceRepository,
resultRepo: IResultRepository,
leagueRepo: ILeagueRepository,
standingRepo: IStandingRepository,
membershipRepo: ILeagueMembershipRepository,
registrationRepo: IRaceRegistrationRepository,
feedRepo: IFeedRepository,
socialRepo: ISocialGraphRepository,
imageService: ImageServicePort,
output: UseCaseOutputPort<DashboardOverviewResult>,
) =>
new DashboardOverviewUseCase(
driverRepo,
raceRepo,
resultRepo,
leagueRepo,
standingRepo,
membershipRepo,
registrationRepo,
feedRepo,
socialRepo,
async (driverId: string) => imageService.getDriverAvatar(driverId),
() => null,
output,
),
inject: [
DRIVER_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN,
FEED_REPOSITORY_TOKEN,
SOCIAL_GRAPH_REPOSITORY_TOKEN,
IMAGE_SERVICE_TOKEN,
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
],
},
];

View File

@@ -1,80 +1,31 @@
import { Injectable, Inject } from '@nestjs/common';
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
import type { DashboardOverviewOutputPort } from '@core/racing/application/ports/output/DashboardOverviewOutputPort';
import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
// Core imports
import type { Logger } from '@core/shared/application/Logger';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Tokens
import {
LOGGER_TOKEN,
DRIVER_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN,
FEED_REPOSITORY_TOKEN,
SOCIAL_GRAPH_REPOSITORY_TOKEN,
IMAGE_SERVICE_TOKEN,
} from './DashboardProviders';
import { LOGGER_TOKEN, DASHBOARD_OVERVIEW_USE_CASE_TOKEN } from './DashboardProviders';
@Injectable()
export class DashboardService {
private readonly dashboardOverviewUseCase: DashboardOverviewUseCase;
constructor(
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository?: IDriverRepository,
@Inject(RACE_REPOSITORY_TOKEN) private readonly raceRepository?: IRaceRepository,
@Inject(RESULT_REPOSITORY_TOKEN) private readonly resultRepository?: IResultRepository,
@Inject(LEAGUE_REPOSITORY_TOKEN) private readonly leagueRepository?: ILeagueRepository,
@Inject(STANDING_REPOSITORY_TOKEN) private readonly standingRepository?: IStandingRepository,
@Inject(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN) private readonly leagueMembershipRepository?: ILeagueMembershipRepository,
@Inject(RACE_REGISTRATION_REPOSITORY_TOKEN) private readonly raceRegistrationRepository?: IRaceRegistrationRepository,
@Inject(FEED_REPOSITORY_TOKEN) private readonly feedRepository?: IFeedRepository,
@Inject(SOCIAL_GRAPH_REPOSITORY_TOKEN) private readonly socialRepository?: ISocialGraphRepository,
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService?: IImageServicePort,
) {
this.dashboardOverviewUseCase = new DashboardOverviewUseCase(
driverRepository,
raceRepository,
resultRepository,
leagueRepository,
standingRepository,
leagueMembershipRepository,
raceRegistrationRepository,
feedRepository,
socialRepository,
imageService,
() => null, // getDriverStats
);
}
@Inject(DASHBOARD_OVERVIEW_USE_CASE_TOKEN) private readonly dashboardOverviewUseCase: DashboardOverviewUseCase,
private readonly dashboardOverviewPresenter: DashboardOverviewPresenter,
) {}
async getDashboardOverview(driverId: string): Promise<DashboardOverviewPresenter> {
async getDashboardOverview(driverId: string): Promise<DashboardOverviewDTO> {
this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId });
const result = await this.dashboardOverviewUseCase.execute({ driverId });
if (result.isErr()) {
throw new Error(result.error?.message || 'Failed to get dashboard overview');
throw new Error(result.unwrapErr().details?.message ?? 'Failed to get dashboard overview');
}
const presenter = new DashboardOverviewPresenter();
presenter.present(result.value as DashboardOverviewOutputPort);
return presenter;
return this.dashboardOverviewPresenter.getResponseModel();
}
}

View File

@@ -15,9 +15,10 @@ export class DashboardDriverSummaryDTO {
@IsString()
country!: string;
@ApiProperty()
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
avatarUrl!: string;
avatarUrl?: string | null;
@ApiProperty({ nullable: true })
@IsOptional()
@@ -52,13 +53,15 @@ export class DashboardRaceSummaryDTO {
@IsString()
id!: string;
@ApiProperty()
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
leagueId!: string;
leagueId?: string | null;
@ApiProperty()
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
leagueName!: string;
leagueName?: string | null;
@ApiProperty()
@IsString()
@@ -90,13 +93,15 @@ export class DashboardRecentResultDTO {
@IsString()
raceName!: string;
@ApiProperty()
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
leagueId!: string;
leagueId?: string | null;
@ApiProperty()
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
leagueName!: string;
leagueName?: string | null;
@ApiProperty()
@IsString()
@@ -120,17 +125,19 @@ export class DashboardLeagueStandingSummaryDTO {
@IsString()
leagueName!: string;
@ApiProperty()
@ApiProperty({ nullable: true })
@IsOptional()
@IsNumber()
position!: number;
position?: number | null;
@ApiProperty()
@IsNumber()
totalDrivers!: number;
@ApiProperty()
@ApiProperty({ nullable: true })
@IsOptional()
@IsNumber()
points!: number;
points?: number | null;
}
export class DashboardFeedItemSummaryDTO {
@@ -191,9 +198,10 @@ export class DashboardFriendSummaryDTO {
@IsString()
country!: string;
@ApiProperty()
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
avatarUrl!: string;
avatarUrl?: string | null;
}
export class DashboardOverviewDTO {

View File

@@ -1,120 +1,137 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { DashboardOverviewPresenter } from './DashboardOverviewPresenter';
import type { DashboardOverviewOutputPort } from '@core/racing/application/ports/output/DashboardOverviewOutputPort';
import type { DashboardOverviewResult } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
import { Driver } from '@core/racing/domain/entities/Driver';
import { Race } from '@core/racing/domain/entities/Race';
import { League } from '@core/racing/domain/entities/League';
import { Standing } from '@core/racing/domain/entities/Standing';
import { Result as RaceResult } from '@core/racing/domain/entities/result/Result';
import type { FeedItem } from '@core/social/domain/types/FeedItem';
const createOutput = (): DashboardOverviewOutputPort => ({
currentDriver: {
id: 'driver-1',
name: 'Test Driver',
country: 'DE',
avatarUrl: 'https://example.com/avatar.jpg',
rating: 2500,
globalRank: 42,
totalRaces: 10,
wins: 3,
podiums: 5,
consistency: 90,
},
myUpcomingRaces: [
{
id: 'race-1',
leagueId: 'league-1',
leagueName: 'League 1',
track: 'Spa',
car: 'GT3',
scheduledAt: '2025-01-01T10:00:00Z',
status: 'scheduled',
isMyLeague: true,
},
],
otherUpcomingRaces: [
{
id: 'race-2',
leagueId: 'league-2',
leagueName: 'League 2',
track: 'Monza',
car: 'GT3',
scheduledAt: '2025-01-02T10:00:00Z',
status: 'scheduled',
isMyLeague: false,
},
],
upcomingRaces: [
{
id: 'race-1',
leagueId: 'league-1',
leagueName: 'League 1',
track: 'Spa',
car: 'GT3',
scheduledAt: '2025-01-01T10:00:00Z',
status: 'scheduled',
isMyLeague: true,
},
{
id: 'race-2',
leagueId: 'league-2',
leagueName: 'League 2',
track: 'Monza',
car: 'GT3',
scheduledAt: '2025-01-02T10:00:00Z',
status: 'scheduled',
isMyLeague: false,
},
],
activeLeaguesCount: 2,
nextRace: {
const createOutput = (): DashboardOverviewResult => {
const driver = Driver.create({ id: 'driver-1', iracingId: '12345', name: 'Test Driver', country: 'DE' });
const league1 = League.create({ id: 'league-1', name: 'League 1', description: 'First league', ownerId: 'owner-1' });
const league2 = League.create({ id: 'league-2', name: 'League 2', description: 'Second league', ownerId: 'owner-2' });
const league3 = League.create({ id: 'league-3', name: 'League 3', description: 'Third league', ownerId: 'owner-3' });
const race1 = Race.create({
id: 'race-1',
leagueId: 'league-1',
leagueName: 'League 1',
track: 'Spa',
car: 'GT3',
scheduledAt: '2025-01-01T10:00:00Z',
scheduledAt: new Date('2025-01-01T10:00:00Z'),
status: 'scheduled',
isMyLeague: true,
},
recentResults: [
{
raceId: 'race-3',
raceName: 'Nürburgring',
leagueId: 'league-3',
leagueName: 'League 3',
finishedAt: '2024-12-01T10:00:00Z',
position: 1,
incidents: 0,
});
const race2 = Race.create({
id: 'race-2',
leagueId: 'league-2',
track: 'Monza',
car: 'GT3',
scheduledAt: new Date('2025-01-02T10:00:00Z'),
status: 'scheduled',
});
const race3 = Race.create({
id: 'race-3',
leagueId: 'league-3',
track: 'Nürburgring',
car: 'GT3',
scheduledAt: new Date('2024-12-01T10:00:00Z'),
status: 'completed',
});
const standing1 = Standing.create({ leagueId: 'league-1', driverId: 'driver-1', position: 1, points: 150 });
const result1 = RaceResult.create({
id: 'result-1',
raceId: 'race-3',
driverId: 'driver-1',
position: 1,
fastestLap: 120,
incidents: 0,
startPosition: 1,
});
const feedItem: FeedItem = {
id: 'feed-1',
type: 'friend-joined-league',
headline: 'You won a race',
body: 'Congrats!',
timestamp: new Date('2024-12-02T10:00:00Z'),
ctaLabel: 'View',
ctaHref: '/races/race-3',
};
const friend = Driver.create({ id: 'friend-1', iracingId: '67890', name: 'Friend One', country: 'US' });
return {
currentDriver: {
driver,
avatarUrl: 'https://example.com/avatar.jpg',
rating: 2500,
globalRank: 42,
totalRaces: 10,
wins: 3,
podiums: 5,
consistency: 90,
},
],
leagueStandingsSummaries: [
{
leagueId: 'league-1',
leagueName: 'League 1',
position: 1,
totalDrivers: 20,
points: 150,
},
],
feedSummary: {
notificationCount: 3,
items: [
myUpcomingRaces: [
{
id: 'feed-1',
type: 'race_result' as any,
headline: 'You won a race',
body: 'Congrats!',
timestamp: '2024-12-02T10:00:00Z',
ctaLabel: 'View',
ctaHref: '/races/race-3',
race: race1,
league: league1,
isMyLeague: true,
},
],
},
friends: [
{
id: 'friend-1',
name: 'Friend One',
country: 'US',
avatarUrl: 'https://example.com/friend.jpg',
otherUpcomingRaces: [
{
race: race2,
league: league2,
isMyLeague: false,
},
],
upcomingRaces: [
{
race: race1,
league: league1,
isMyLeague: true,
},
{
race: race2,
league: league2,
isMyLeague: false,
},
],
activeLeaguesCount: 2,
nextRace: {
race: race1,
league: league1,
isMyLeague: true,
},
],
});
recentResults: [
{
race: race3,
league: league3,
result: result1,
},
],
leagueStandingsSummaries: [
{
league: league1,
standing: standing1,
totalDrivers: 20,
},
],
feedSummary: {
notificationCount: 3,
items: [feedItem],
},
friends: [
{
driver: friend,
avatarUrl: 'https://example.com/friend.jpg',
},
],
};
};
describe('DashboardOverviewPresenter', () => {
let presenter: DashboardOverviewPresenter;
@@ -123,44 +140,23 @@ describe('DashboardOverviewPresenter', () => {
presenter = new DashboardOverviewPresenter();
});
it('maps DashboardOverviewOutputPort to DashboardOverviewDTO correctly', () => {
it('maps DashboardOverviewResult to DashboardOverviewDTO correctly', () => {
const output = createOutput();
presenter.present(output);
const dto = presenter.getResponseModel();
const viewModel = presenter.viewModel;
expect(viewModel.activeLeaguesCount).toBe(2);
expect(viewModel.currentDriver?.id).toBe('driver-1');
expect(viewModel.myUpcomingRaces[0].id).toBe('race-1');
expect(viewModel.otherUpcomingRaces[0].id).toBe('race-2');
expect(viewModel.upcomingRaces).toHaveLength(2);
expect(viewModel.nextRace?.id).toBe('race-1');
expect(viewModel.recentResults[0].raceId).toBe('race-3');
expect(viewModel.leagueStandingsSummaries[0].leagueId).toBe('league-1');
expect(viewModel.feedSummary.notificationCount).toBe(3);
expect(viewModel.feedSummary.items[0].id).toBe('feed-1');
expect(viewModel.friends[0].id).toBe('friend-1');
expect(dto.activeLeaguesCount).toBe(2);
expect(dto.currentDriver?.id).toBe('driver-1');
expect(dto.myUpcomingRaces[0].id).toBe('race-1');
expect(dto.otherUpcomingRaces[0].id).toBe('race-2');
expect(dto.upcomingRaces).toHaveLength(2);
expect(dto.nextRace?.id).toBe('race-1');
expect(dto.recentResults[0].raceId).toBe('race-3');
expect(dto.leagueStandingsSummaries[0].leagueId).toBe('league-1');
expect(dto.feedSummary.notificationCount).toBe(3);
expect(dto.feedSummary.items[0].id).toBe('feed-1');
expect(dto.friends[0].id).toBe('friend-1');
});
it('reset clears state and causes viewModel to throw', () => {
const output = createOutput();
presenter.present(output);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('getViewModel returns null when not presented', () => {
expect(presenter.getViewModel()).toBeNull();
});
it('getViewModel returns same DTO after present', () => {
const output = createOutput();
presenter.present(output);
expect(presenter.getViewModel()).toEqual(presenter.viewModel);
});
});

View File

@@ -1,4 +1,7 @@
import type { DashboardOverviewOutputPort } from '@core/racing/application/ports/output/DashboardOverviewOutputPort';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type {
DashboardOverviewResult,
} from '@core/racing/application/use-cases/DashboardOverviewUseCase';
import {
DashboardOverviewDTO,
DashboardDriverSummaryDTO,
@@ -10,93 +13,89 @@ import {
DashboardFriendSummaryDTO,
} from '../dtos/DashboardOverviewDTO';
export class DashboardOverviewPresenter {
private result: DashboardOverviewDTO | null = null;
export class DashboardOverviewPresenter implements UseCaseOutputPort<DashboardOverviewResult> {
private responseModel: DashboardOverviewDTO | null = null;
reset() {
this.result = null;
}
present(output: DashboardOverviewOutputPort): void {
const currentDriver: DashboardDriverSummaryDTO | null = output.currentDriver
present(result: DashboardOverviewResult): void {
const currentDriver: DashboardDriverSummaryDTO | null = result.currentDriver
? {
id: output.currentDriver.id,
name: output.currentDriver.name,
country: output.currentDriver.country,
avatarUrl: output.currentDriver.avatarUrl,
rating: output.currentDriver.rating,
globalRank: output.currentDriver.globalRank,
totalRaces: output.currentDriver.totalRaces,
wins: output.currentDriver.wins,
podiums: output.currentDriver.podiums,
consistency: output.currentDriver.consistency,
id: result.currentDriver.driver.id,
name: String(result.currentDriver.driver.name),
country: String(result.currentDriver.driver.country),
avatarUrl: result.currentDriver.avatarUrl,
rating: result.currentDriver.rating,
globalRank: result.currentDriver.globalRank,
totalRaces: result.currentDriver.totalRaces,
wins: result.currentDriver.wins,
podiums: result.currentDriver.podiums,
consistency: result.currentDriver.consistency,
}
: null;
const mapRace = (race: typeof output.myUpcomingRaces[number]): DashboardRaceSummaryDTO => ({
id: race.id,
leagueId: race.leagueId,
leagueName: race.leagueName,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
status: race.status,
isMyLeague: race.isMyLeague,
const mapRace = (raceSummary: DashboardOverviewResult['myUpcomingRaces'][number]): DashboardRaceSummaryDTO => ({
id: raceSummary.race.id,
leagueId: raceSummary.league?.id ? String(raceSummary.league.id) : null,
leagueName: raceSummary.league?.name ? String(raceSummary.league.name) : null,
track: String(raceSummary.race.track),
car: String(raceSummary.race.car),
scheduledAt: raceSummary.race.scheduledAt.toISOString(),
status: raceSummary.race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
isMyLeague: raceSummary.isMyLeague,
});
const myUpcomingRaces: DashboardRaceSummaryDTO[] = output.myUpcomingRaces.map(mapRace);
const otherUpcomingRaces: DashboardRaceSummaryDTO[] = output.otherUpcomingRaces.map(mapRace);
const upcomingRaces: DashboardRaceSummaryDTO[] = output.upcomingRaces.map(mapRace);
const myUpcomingRaces: DashboardRaceSummaryDTO[] = result.myUpcomingRaces.map(mapRace);
const otherUpcomingRaces: DashboardRaceSummaryDTO[] = result.otherUpcomingRaces.map(mapRace);
const upcomingRaces: DashboardRaceSummaryDTO[] = result.upcomingRaces.map(mapRace);
const nextRace: DashboardRaceSummaryDTO | null = output.nextRace ? mapRace(output.nextRace) : null;
const nextRace: DashboardRaceSummaryDTO | null = result.nextRace ? mapRace(result.nextRace) : null;
const recentResults: DashboardRecentResultDTO[] = output.recentResults.map(result => ({
raceId: result.raceId,
raceName: result.raceName,
leagueId: result.leagueId,
leagueName: result.leagueName,
finishedAt: result.finishedAt,
position: result.position,
incidents: result.incidents,
const recentResults: DashboardRecentResultDTO[] = result.recentResults.map(resultSummary => ({
raceId: resultSummary.race.id,
raceName: String(resultSummary.race.track),
leagueId: resultSummary.league?.id ? String(resultSummary.league.id) : null,
leagueName: resultSummary.league?.name ? String(resultSummary.league.name) : null,
finishedAt: resultSummary.race.scheduledAt.toISOString(),
position: Number(resultSummary.result.position),
incidents: Number(resultSummary.result.incidents),
}));
const leagueStandingsSummaries: DashboardLeagueStandingSummaryDTO[] =
output.leagueStandingsSummaries.map(standing => ({
leagueId: standing.leagueId,
leagueName: standing.leagueName,
position: standing.position,
result.leagueStandingsSummaries.map(standing => ({
leagueId: String(standing.league.id),
leagueName: String(standing.league.name),
position: standing.standing?.position ? Number(standing.standing.position) : null,
totalDrivers: standing.totalDrivers,
points: standing.points,
points: standing.standing?.points ? Number(standing.standing.points) : null,
}));
const feedItems: DashboardFeedItemSummaryDTO[] = output.feedSummary.items.map(item => ({
const feedItems: DashboardFeedItemSummaryDTO[] = result.feedSummary.items.map(item => ({
id: item.id,
type: item.type,
type: String(item.type),
headline: item.headline,
body: item.body,
timestamp: item.timestamp,
ctaLabel: item.ctaLabel,
ctaHref: item.ctaHref,
body: item.body ?? '',
timestamp: item.timestamp.toISOString(),
ctaLabel: item.ctaLabel ?? '',
ctaHref: item.ctaHref ?? '',
}));
const feedSummary: DashboardFeedSummaryDTO = {
notificationCount: output.feedSummary.notificationCount,
notificationCount: result.feedSummary.notificationCount,
items: feedItems,
};
const friends: DashboardFriendSummaryDTO[] = output.friends.map(friend => ({
id: friend.id,
name: friend.name,
country: friend.country,
const friends: DashboardFriendSummaryDTO[] = result.friends.map(friend => ({
id: friend.driver.id,
name: String(friend.driver.name),
country: String(friend.driver.country),
avatarUrl: friend.avatarUrl,
}));
this.result = {
this.responseModel = {
currentDriver,
myUpcomingRaces,
otherUpcomingRaces,
upcomingRaces,
activeLeaguesCount: output.activeLeaguesCount,
activeLeaguesCount: result.activeLeaguesCount,
nextRace,
recentResults,
leagueStandingsSummaries,
@@ -105,12 +104,8 @@ export class DashboardOverviewPresenter {
};
}
get viewModel(): DashboardOverviewDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
getViewModel(): DashboardOverviewDTO | null {
return this.result;
getResponseModel(): DashboardOverviewDTO {
if (!this.responseModel) throw new Error('Presenter not presented');
return this.responseModel;
}
}