api client refactor

This commit is contained in:
2025-12-17 19:25:10 +01:00
parent 4177644b18
commit 26f7a2b6aa
27 changed files with 543 additions and 1329 deletions

View File

@@ -9,7 +9,7 @@ import Breadcrumbs from '@/components/layout/Breadcrumbs';
import ResultsTable from '@/components/races/ResultsTable';
import ImportResultsForm from '@/components/races/ImportResultsForm';
import QuickPenaltyModal from '@/components/leagues/QuickPenaltyModal';
import { getRaceResults, getRaceSOF, importRaceResults } from '@/lib/services/races/RaceResultsService';
import { raceResultsService } from '@/lib/services/races/RaceResultsService';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import type { RaceResultsDetailViewModel } from '@/lib/view-models';
@@ -32,12 +32,12 @@ export default function RaceResultsPage() {
const loadData = async () => {
try {
const raceData = await getRaceResults(raceId, currentDriverId);
const raceData = await raceResultsService.getResultsDetail(raceId, currentDriverId);
setRaceData(raceData);
setError(null);
try {
const sofData = await getRaceSOF(raceId);
const sofData = await raceResultsService.getWithSOF(raceId);
setRaceSOF(sofData.strengthOfField);
} catch (sofErr) {
console.error('Failed to load SOF:', sofErr);
@@ -70,7 +70,7 @@ export default function RaceResultsPage() {
setError(null);
try {
await importRaceResults(raceId, {
await raceResultsService.importRaceResults(raceId, {
resultsFileContent: JSON.stringify(importedResults), // Assuming the API expects JSON string
});

View File

@@ -8,6 +8,7 @@ import type {
RegisterForRaceInputDto,
ImportRaceResultsInputDto,
ImportRaceResultsSummaryDto,
WithdrawFromRaceInputDto,
} from '../../dtos';
/**
@@ -50,4 +51,19 @@ export class RacesApiClient extends BaseApiClient {
importResults(raceId: string, input: ImportRaceResultsInputDto): Promise<ImportRaceResultsSummaryDto> {
return this.post<ImportRaceResultsSummaryDto>(`/races/${raceId}/import-results`, input);
}
/** Withdraw from race */
withdraw(raceId: string, input: WithdrawFromRaceInputDto): Promise<void> {
return this.post<void>(`/races/${raceId}/withdraw`, input);
}
/** Cancel race */
cancel(raceId: string): Promise<void> {
return this.post<void>(`/races/${raceId}/cancel`, {});
}
/** Complete race */
complete(raceId: string): Promise<void> {
return this.post<void>(`/races/${raceId}/complete`, {});
}
}

View File

@@ -1,6 +1,13 @@
import { RaceDetailDto } from '../dtos';
import { RaceDetailViewModel } from '../view-models';
export class RaceDetailPresenter {
present(dto: RaceDetailDto): RaceDetailViewModel {
return new RaceDetailViewModel(dto);
}
}
export const presentRaceDetail = (dto: RaceDetailDto): RaceDetailViewModel => {
return new RaceDetailViewModel(dto);
const presenter = new RaceDetailPresenter();
return presenter.present(dto);
};

View File

@@ -0,0 +1,43 @@
import { RaceDetailViewModel } from '../view-models/RaceDetailViewModel';
import type { RaceDetailDto } from '../dtos/RaceDetailDto';
import type { RacesPageDataDto } from '../dtos/RacesPageDataDto';
import type { RacesPageViewModel } from '../view-models/RacesPageViewModel';
/**
* Race Presenter
*
* Stateless presenter that transforms race DTOs into view models.
* All methods are pure functions with no side effects.
*/
export class RacePresenter {
presentRaceDetail(dto: RaceDetailDto): RaceDetailViewModel {
return new RaceDetailViewModel(dto);
}
presentRacesPage(dto: RacesPageDataDto): RacesPageViewModel {
return {
upcomingRaces: dto.races.filter(r => r.isUpcoming).map(r => this.presentRaceCard(r)),
completedRaces: dto.races.filter(r => r.status === 'completed').map(r => this.presentRaceCard(r)),
totalCount: dto.races.length,
};
}
private presentRaceCard(race: any): any {
return {
id: race.id,
title: race.title || race.track,
scheduledTime: race.scheduledTime || race.scheduledAt,
status: this.formatStatus(race.status),
};
}
private formatStatus(status: string): string {
const statusMap: Record<string, string> = {
scheduled: 'Scheduled',
running: 'Live',
completed: 'Finished',
cancelled: 'Cancelled',
};
return statusMap[status] || status;
}
}

View File

@@ -1,6 +1,13 @@
import { RaceResultsDetailDto } from '../dtos';
import { RaceResultsDetailViewModel } from '../view-models';
export const presentRaceResultsDetail = (dto: RaceResultsDetailDto, currentUserId: string): RaceResultsDetailViewModel => {
return new RaceResultsDetailViewModel(dto, currentUserId);
export class RaceResultsDetailPresenter {
present(dto: RaceResultsDetailDto, currentUserId?: string): RaceResultsDetailViewModel {
return new RaceResultsDetailViewModel(dto, currentUserId);
}
}
export const presentRaceResultsDetail = (dto: RaceResultsDetailDto, currentUserId?: string): RaceResultsDetailViewModel => {
const presenter = new RaceResultsDetailPresenter();
return presenter.present(dto, currentUserId);
};

View File

@@ -5,10 +5,8 @@ import type {
} from '@core/racing/application/presenters/IRaceWithSOFPresenter';
export class RaceWithSOFPresenter implements IRaceWithSOFPresenter {
private viewModel: RaceWithSOFViewModel | null = null;
present(dto: RaceWithSOFResultDTO): void {
this.viewModel = {
present(dto: RaceWithSOFResultDTO): RaceWithSOFViewModel {
return {
id: dto.raceId,
leagueId: dto.leagueId,
scheduledAt: dto.scheduledAt.toISOString(),
@@ -24,12 +22,4 @@ export class RaceWithSOFPresenter implements IRaceWithSOFPresenter {
participantCount: dto.participantCount,
};
}
getViewModel(): RaceWithSOFViewModel | null {
return this.viewModel;
}
reset(): void {
this.viewModel = null;
}
}

View File

@@ -22,7 +22,9 @@ export { presentWalletTransaction } from './WalletTransactionPresenter';
export { presentRaceDetail } from './RaceDetailPresenter';
export { presentRaceListItem } from './RaceListItemPresenter';
export { presentRaceResult } from './RaceResultsPresenter';
export { presentRaceResultsDetail } from './RaceResultsDetailPresenter';
export { presentRaceResultsDetail, RaceResultsDetailPresenter } from './RaceResultsDetailPresenter';
export { RaceWithSOFPresenter } from './RaceWithSOFPresenter';
export { ImportRaceResultsPresenter } from './ImportRaceResultsPresenter';
// Sponsor Presenters
export { presentSponsor } from './SponsorPresenter';

View File

@@ -2,15 +2,42 @@ import { api as api } from '../../api';
import { presentDriversLeaderboard } from '../../presenters';
import { DriverLeaderboardViewModel } from '../../view-models';
/**
* Driver Service
*
* Handles driver-related operations including profiles, leaderboards, and onboarding.
*/
export class DriverService {
constructor(
private readonly apiClient = api.drivers
) {}
async getDriverLeaderboard(): Promise<DriverLeaderboardViewModel> {
const dto = await this.apiClient.getLeaderboard();
return presentDriversLeaderboard(dto);
}
async completeDriverOnboarding(input: any): Promise<any> {
return await this.apiClient.completeOnboarding(input);
}
async getCurrentDriver(): Promise<any> {
return await this.apiClient.getCurrent();
}
}
// Singleton instance
export const driverService = new DriverService();
// Backward compatibility functional exports
export async function getDriverLeaderboard(): Promise<DriverLeaderboardViewModel> {
const dto = await api.drivers.getLeaderboard();
return presentDriversLeaderboard(dto);
return driverService.getDriverLeaderboard();
}
export async function completeDriverOnboarding(input: any): Promise<any> {
return await api.drivers.completeOnboarding(input);
return driverService.completeDriverOnboarding(input);
}
export async function getCurrentDriver(): Promise<any> {
return await api.drivers.getCurrent();
return driverService.getCurrentDriver();
}

View File

@@ -0,0 +1,6 @@
// Export the class-based service
export { DriverService, driverService } from './DriverService';
// Export backward compatibility functions
export { getDriverLeaderboard, completeDriverOnboarding, getCurrentDriver } from './DriverService';
export { registerDriver, getDriverRegistrationStatus } from './DriverRegistrationService';

View File

@@ -1,23 +1,46 @@
import { api as api } from '../../api';
import { presentRaceResultsDetail } from '../../presenters';
import { RaceResultsDetailPresenter, RaceWithSOFPresenter, ImportRaceResultsPresenter } from '../../presenters';
import { RaceResultsDetailViewModel } from '../../view-models';
export class RaceResultsService {
constructor(
private readonly apiClient = api.races,
private readonly resultsDetailPresenter = new RaceResultsDetailPresenter(),
private readonly sofPresenter = new RaceWithSOFPresenter(),
private readonly importPresenter = new ImportRaceResultsPresenter()
) {}
async importRaceResults(raceId: string, input: any): Promise<any> {
const dto = await this.apiClient.importResults(raceId, input);
return this.importPresenter.present(dto);
}
async getResultsDetail(raceId: string, currentUserId?: string): Promise<RaceResultsDetailViewModel> {
const dto = await this.apiClient.getResultsDetail(raceId);
return this.resultsDetailPresenter.present(dto, currentUserId);
}
async getWithSOF(raceId: string): Promise<any> {
const dto = await this.apiClient.getWithSOF(raceId);
return this.sofPresenter.present(dto);
}
}
// Singleton instance
export const raceResultsService = new RaceResultsService();
// Backward compatibility functions
export async function getRaceResults(
raceId: string,
currentUserId?: string
): Promise<RaceResultsDetailViewModel> {
const dto = await api.races.getResultsDetail(raceId);
return presentRaceResultsDetail(dto, currentUserId);
return raceResultsService.getResultsDetail(raceId, currentUserId);
}
export async function getRaceSOF(raceId: string): Promise<any> {
const dto = await api.races.getWithSOF(raceId);
// TODO: use presenter
return dto;
return raceResultsService.getWithSOF(raceId);
}
export async function importRaceResults(raceId: string, input: any): Promise<any> {
const dto = await api.races.importResults(raceId, input);
// TODO: use presenter
return dto;
return raceResultsService.importRaceResults(raceId, input);
}

View File

@@ -1,22 +1,48 @@
import { api as api } from '../../api';
import { presentRaceDetail } from '../../presenters';
import { RaceDetailPresenter } from '../../presenters';
import { RaceDetailViewModel } from '../../view-models';
export class RaceService {
constructor(
private readonly apiClient = api.races,
private readonly presenter = new RaceDetailPresenter()
) {}
async getRaceDetail(
raceId: string,
driverId: string
): Promise<RaceDetailViewModel> {
const dto = await this.apiClient.getDetail(raceId, driverId);
return this.presenter.present(dto);
}
async getRacesPageData(): Promise<any> {
const dto = await this.apiClient.getPageData();
// TODO: use presenter
return dto;
}
async getRacesTotal(): Promise<any> {
const dto = await this.apiClient.getTotal();
return dto;
}
}
// Singleton instance
export const raceService = new RaceService();
// Backward compatibility functions
export async function getRaceDetail(
raceId: string,
driverId: string
): Promise<RaceDetailViewModel> {
const dto = await api.races.getDetail(raceId, driverId);
return presentRaceDetail(dto);
return raceService.getRaceDetail(raceId, driverId);
}
export async function getRacesPageData(): Promise<any> {
const dto = await api.races.getPageData();
// TODO: use presenter
return dto;
return raceService.getRacesPageData();
}
export async function getRacesTotal(): Promise<any> {
const dto = await api.races.getTotal();
return dto;
return raceService.getRacesTotal();
}

View File

@@ -0,0 +1,7 @@
// Export the class-based service
export { RaceService, raceService } from './RaceService';
export { RaceResultsService, raceResultsService } from './RaceResultsService';
// Export backward compatibility functions
export { getRaceDetail, getRacesPageData, getRacesTotal } from './RaceService';
export { getRaceResults, getRaceSOF, importRaceResults } from './RaceResultsService';

View File

@@ -0,0 +1,12 @@
export interface RaceCardViewModel {
id: string;
title: string;
scheduledTime: string;
status: string;
}
export interface RacesPageViewModel {
upcomingRaces: RaceCardViewModel[];
completedRaces: RaceCardViewModel[];
totalCount: number;
}