api client refactor

This commit is contained in:
2025-12-17 18:01:47 +01:00
parent bab55955e1
commit 4177644b18
190 changed files with 6403 additions and 1624 deletions

View File

@@ -1,29 +1,6 @@
import type {
IDriverRegistrationStatusPresenter,
DriverRegistrationStatusViewModel,
} from '@core/racing/application/presenters/IDriverRegistrationStatusPresenter';
import { DriverRegistrationStatusDto } from '../dtos';
import { DriverRegistrationStatusViewModel } from '../view-models';
export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter {
private viewModel: DriverRegistrationStatusViewModel | null = null;
present(
isRegistered: boolean,
raceId: string,
driverId: string
): DriverRegistrationStatusViewModel {
this.viewModel = {
isRegistered,
raceId,
driverId,
};
return this.viewModel;
}
getViewModel(): DriverRegistrationStatusViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}
export const presentDriverRegistrationStatus = (dto: DriverRegistrationStatusDto): DriverRegistrationStatusViewModel => {
return new DriverRegistrationStatusViewModel(dto);
};

View File

@@ -1,109 +1,6 @@
/**
* DriversLeaderboardPresenter - Pure data transformer
* Transforms API response to view model without DI dependencies.
*/
import { DriversLeaderboardDto, DriverLeaderboardItemDto } from '../dtos';
import { DriverLeaderboardViewModel } from '../view-models';
import { apiClient, type DriversLeaderboardViewModel as ApiDriversLeaderboardViewModel } from '@/lib/apiClient';
export type SkillLevel = 'rookie' | 'amateur' | 'pro' | 'elite' | 'legend';
export interface DriverLeaderboardItemViewModel {
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality?: string | undefined;
racesCompleted: number;
wins: number;
podiums: number;
isActive: boolean;
rank: number;
avatarUrl?: string | undefined;
}
export interface DriversLeaderboardViewModel {
drivers: DriverLeaderboardItemViewModel[];
totalRaces: number;
totalWins: number;
activeCount: number;
}
export interface IDriversLeaderboardPresenter {
reset(): void;
getViewModel(): DriversLeaderboardViewModel | null;
}
/**
* Calculate skill level from rating
*/
function getSkillLevel(rating: number): SkillLevel {
if (rating >= 5000) return 'legend';
if (rating >= 3500) return 'elite';
if (rating >= 2000) return 'pro';
if (rating >= 1000) return 'amateur';
return 'rookie';
}
/**
* Transform API response to view model
*/
function transformApiResponse(apiResponse: ApiDriversLeaderboardViewModel): DriversLeaderboardViewModel {
const items: DriverLeaderboardItemViewModel[] = apiResponse.drivers.map((driver, index) => {
const rating = driver.rating ?? 0;
const skillLevel = getSkillLevel(rating);
const viewModel: DriverLeaderboardItemViewModel = {
id: driver.id,
name: driver.name,
rating,
skillLevel,
racesCompleted: driver.races ?? 0,
wins: driver.wins ?? 0,
podiums: 0, // API may not provide this, default to 0
isActive: true,
rank: index + 1,
};
if (driver.avatarUrl) {
viewModel.avatarUrl = driver.avatarUrl;
}
return viewModel;
});
const totalRaces = items.reduce((sum, d) => sum + d.racesCompleted, 0);
const totalWins = items.reduce((sum, d) => sum + d.wins, 0);
const activeCount = items.filter((d) => d.isActive).length;
return {
drivers: items,
totalRaces,
totalWins,
activeCount,
};
}
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
private viewModel: DriversLeaderboardViewModel | null = null;
reset(): void {
this.viewModel = null;
}
async fetchAndPresent(): Promise<void> {
const apiResponse = await apiClient.drivers.getLeaderboard();
this.viewModel = transformApiResponse(apiResponse);
}
getViewModel(): DriversLeaderboardViewModel | null {
return this.viewModel;
}
}
/**
* Convenience function to fetch and transform drivers leaderboard
*/
export async function fetchDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
const apiResponse = await apiClient.drivers.getLeaderboard();
return transformApiResponse(apiResponse);
}
export const presentDriversLeaderboard = (dto: DriversLeaderboardDto, previousDrivers?: DriverLeaderboardItemDto[]): DriverLeaderboardViewModel => {
return new DriverLeaderboardViewModel(dto, previousDrivers);
};

View File

@@ -0,0 +1,6 @@
import { LeagueMemberDto } from '../dtos';
import { LeagueMemberViewModel } from '../view-models';
export const presentLeagueMember = (dto: LeagueMemberDto, currentUserId: string): LeagueMemberViewModel => {
return new LeagueMemberViewModel(dto, currentUserId);
};

View File

@@ -1,81 +1,6 @@
/**
* LeagueStandingsPresenter - Pure data transformer
* Transforms API response to view model without DI dependencies.
*/
import { LeagueStandingsDto, StandingEntryDto } from '../dtos';
import { LeagueStandingsViewModel } from '../view-models';
import { apiClient, type LeagueStandingsViewModel as ApiLeagueStandingsViewModel } from '@/lib/apiClient';
export interface LeagueStandingsEntryViewModel {
driverId: string;
driverName: string;
position: number;
points: number;
wins: number;
podiums: number;
races: number;
avatarUrl?: string | undefined;
}
export interface LeagueStandingsViewModel {
leagueId: string;
standings: LeagueStandingsEntryViewModel[];
totalDrivers: number;
}
export interface ILeagueStandingsPresenter {
present(leagueId: string): Promise<void>;
getViewModel(): LeagueStandingsViewModel | null;
reset(): void;
}
/**
* Transform API response to view model
*/
function transformApiResponse(leagueId: string, apiResponse: ApiLeagueStandingsViewModel): LeagueStandingsViewModel {
const standings: LeagueStandingsEntryViewModel[] = apiResponse.standings.map((entry) => {
const viewModel: LeagueStandingsEntryViewModel = {
driverId: entry.driverId,
driverName: entry.driver?.name ?? 'Unknown Driver',
position: entry.position,
points: entry.points,
wins: entry.wins,
podiums: entry.podiums,
races: entry.races,
};
if (entry.driver?.avatarUrl) {
viewModel.avatarUrl = entry.driver.avatarUrl;
}
return viewModel;
});
return {
leagueId,
standings,
totalDrivers: standings.length,
};
}
export class LeagueStandingsPresenter implements ILeagueStandingsPresenter {
private viewModel: LeagueStandingsViewModel | null = null;
reset(): void {
this.viewModel = null;
}
async present(leagueId: string): Promise<void> {
const apiResponse = await apiClient.leagues.getStandings(leagueId);
this.viewModel = transformApiResponse(leagueId, apiResponse);
}
getViewModel(): LeagueStandingsViewModel | null {
return this.viewModel;
}
}
/**
* Convenience function to fetch and transform standings
*/
export async function fetchLeagueStandings(leagueId: string): Promise<LeagueStandingsViewModel> {
const apiResponse = await apiClient.leagues.getStandings(leagueId);
return transformApiResponse(leagueId, apiResponse);
}
export const presentLeagueStandings = (dto: LeagueStandingsDto, currentUserId: string, previousStandings?: StandingEntryDto[]): LeagueStandingsViewModel => {
return new LeagueStandingsViewModel(dto, currentUserId, previousStandings);
};

View File

@@ -0,0 +1,10 @@
import { LeagueSummaryDto } from '../dtos';
import { LeagueSummaryViewModel } from '../view-models';
export const presentLeagueSummary = (dto: LeagueSummaryDto): LeagueSummaryViewModel => {
return new LeagueSummaryViewModel(dto);
};
export const presentLeagueSummaries = (dtos: LeagueSummaryDto[]): LeagueSummaryViewModel[] => {
return dtos.map(presentLeagueSummary);
};

View File

@@ -0,0 +1,6 @@
import { MembershipFeeDto } from '../dtos';
import { MembershipFeeViewModel } from '../view-models';
export const presentMembershipFee = (dto: MembershipFeeDto): MembershipFeeViewModel => {
return new MembershipFeeViewModel(dto);
};

View File

@@ -0,0 +1,6 @@
import { PaymentDto } from '../dtos';
import { PaymentViewModel } from '../view-models';
export const presentPayment = (dto: PaymentDto): PaymentViewModel => {
return new PaymentViewModel(dto);
};

View File

@@ -0,0 +1,6 @@
import { PrizeDto } from '../dtos';
import { PrizeViewModel } from '../view-models';
export const presentPrize = (dto: PrizeDto): PrizeViewModel => {
return new PrizeViewModel(dto);
};

View File

@@ -1,21 +1,6 @@
import type {
IRaceDetailPresenter,
RaceDetailViewModel,
} from '@core/racing/application/presenters/IRaceDetailPresenter';
import { RaceDetailDto } from '../dtos';
import { RaceDetailViewModel } from '../view-models';
export class RaceDetailPresenter implements IRaceDetailPresenter {
private viewModel: RaceDetailViewModel | null = null;
reset(): void {
this.viewModel = null;
}
present(viewModel: RaceDetailViewModel): RaceDetailViewModel {
this.viewModel = viewModel;
return viewModel;
}
getViewModel(): RaceDetailViewModel | null {
return this.viewModel;
}
}
export const presentRaceDetail = (dto: RaceDetailDto): RaceDetailViewModel => {
return new RaceDetailViewModel(dto);
};

View File

@@ -0,0 +1,6 @@
import { RaceListItemDto } from '../dtos';
import { RaceListItemViewModel } from '../view-models';
export const presentRaceListItem = (dto: RaceListItemDto): RaceListItemViewModel => {
return new RaceListItemViewModel(dto);
};

View File

@@ -1,20 +1,6 @@
import type {
IRaceResultsDetailPresenter,
RaceResultsDetailViewModel,
} from '@core/racing/application/presenters/IRaceResultsDetailPresenter';
import { RaceResultsDetailDto } from '../dtos';
import { RaceResultsDetailViewModel } from '../view-models';
export class RaceResultsDetailPresenter implements IRaceResultsDetailPresenter {
private viewModel: RaceResultsDetailViewModel | null = null;
reset(): void {
this.viewModel = null;
}
present(viewModel: RaceResultsDetailViewModel): void {
this.viewModel = viewModel;
}
getViewModel(): RaceResultsDetailViewModel | null {
return this.viewModel;
}
}
export const presentRaceResultsDetail = (dto: RaceResultsDetailDto, currentUserId: string): RaceResultsDetailViewModel => {
return new RaceResultsDetailViewModel(dto, currentUserId);
};

View File

@@ -0,0 +1,52 @@
import { describe, it, expect } from 'vitest';
import { presentRaceResult } from './RaceResultsPresenter';
import { RaceResultViewModel } from '../view-models';
import type { RaceResultRowDto } from '../dtos';
describe('RaceResultsPresenter', () => {
describe('presentRaceResult', () => {
it('should transform RaceResultRowDto into RaceResultViewModel', () => {
const dto: RaceResultRowDto = {
id: '1',
raceId: 'race-1',
driverId: 'driver-1',
position: 1,
fastestLap: 85.5,
incidents: 0,
startPosition: 2,
};
const result = presentRaceResult(dto);
expect(result).toBeInstanceOf(RaceResultViewModel);
expect(result.id).toBe('1');
expect(result.raceId).toBe('race-1');
expect(result.driverId).toBe('driver-1');
expect(result.position).toBe(1);
expect(result.fastestLap).toBe(85.5);
expect(result.incidents).toBe(0);
expect(result.startPosition).toBe(2);
});
it('should handle zero values correctly', () => {
const dto: RaceResultRowDto = {
id: '2',
raceId: 'race-2',
driverId: 'driver-2',
position: 5,
fastestLap: 0,
incidents: 3,
startPosition: 5,
};
const result = presentRaceResult(dto);
expect(result).toBeInstanceOf(RaceResultViewModel);
expect(result.id).toBe('2');
expect(result.position).toBe(5);
expect(result.fastestLap).toBe(0);
expect(result.incidents).toBe(3);
expect(result.startPosition).toBe(5);
});
});
});

View File

@@ -0,0 +1,6 @@
import { RaceResultRowDto } from '../dtos';
import { RaceResultViewModel } from '../view-models';
export const presentRaceResult = (dto: RaceResultRowDto): RaceResultViewModel => {
return new RaceResultViewModel(dto);
};

View File

@@ -0,0 +1,6 @@
import { SponsorDto } from '../dtos';
import { SponsorViewModel } from '../view-models';
export const presentSponsor = (dto: SponsorDto): SponsorViewModel => {
return new SponsorViewModel(dto);
};

View File

@@ -0,0 +1,6 @@
import { SponsorshipDetailDto } from '../dtos';
import { SponsorshipDetailViewModel } from '../view-models';
export const presentSponsorshipDetail = (dto: SponsorshipDetailDto): SponsorshipDetailViewModel => {
return new SponsorshipDetailViewModel(dto);
};

View File

@@ -1,82 +1,6 @@
/**
* TeamDetailsPresenter - Pure data transformer
* Transforms API response to view model without DI dependencies.
*/
import { TeamDetailsDto, TeamMemberDto } from '../dtos';
import { TeamDetailsViewModel } from '../view-models';
import { apiClient, type TeamDetailsViewModel as ApiTeamDetailsViewModel } from '@/lib/apiClient';
export interface TeamMembershipViewModel {
role: string;
joinedAt: string;
isActive: boolean;
}
export interface TeamInfoViewModel {
id: string;
name: string;
tag?: string | undefined;
description?: string | undefined;
ownerId: string;
leagues?: string[] | undefined;
createdAt: string;
}
export interface TeamDetailsViewModel {
team: TeamInfoViewModel;
membership: TeamMembershipViewModel | null;
canManage: boolean;
}
export interface ITeamDetailsPresenter {
reset(): void;
getViewModel(): TeamDetailsViewModel | null;
}
/**
* Transform API response to view model
*/
function transformApiResponse(apiResponse: ApiTeamDetailsViewModel): TeamDetailsViewModel {
return {
team: {
id: apiResponse.id,
name: apiResponse.name,
description: apiResponse.description,
ownerId: apiResponse.ownerId,
createdAt: new Date().toISOString(), // Would need from API
},
membership: null, // Would need from API based on current user
canManage: false, // Would need from API based on current user
};
}
export class TeamDetailsPresenter implements ITeamDetailsPresenter {
private viewModel: TeamDetailsViewModel | null = null;
reset(): void {
this.viewModel = null;
}
async fetchAndPresent(teamId: string): Promise<void> {
const apiResponse = await apiClient.teams.getDetails(teamId);
if (apiResponse) {
this.viewModel = transformApiResponse(apiResponse);
} else {
this.viewModel = null;
}
}
getViewModel(): TeamDetailsViewModel | null {
return this.viewModel;
}
}
/**
* Convenience function to fetch and transform team details
*/
export async function fetchTeamDetails(teamId: string): Promise<TeamDetailsViewModel | null> {
const apiResponse = await apiClient.teams.getDetails(teamId);
if (!apiResponse) {
return null;
}
return transformApiResponse(apiResponse);
}
export const presentTeamDetails = (dto: TeamDetailsDto, currentUserId: string): TeamDetailsViewModel => {
return new TeamDetailsViewModel(dto, currentUserId);
};

View File

@@ -0,0 +1,6 @@
import { TeamJoinRequestItemDto } from '../dtos';
import { TeamJoinRequestViewModel } from '../view-models';
export const presentTeamJoinRequest = (dto: TeamJoinRequestItemDto, currentUserId: string, isOwner: boolean): TeamJoinRequestViewModel => {
return new TeamJoinRequestViewModel(dto, currentUserId, isOwner);
};

View File

@@ -0,0 +1,6 @@
import { TeamMemberDto } from '../dtos';
import { TeamMemberViewModel } from '../view-models';
export const presentTeamMember = (dto: TeamMemberDto, currentUserId: string, teamOwnerId: string): TeamMemberViewModel => {
return new TeamMemberViewModel(dto, currentUserId, teamOwnerId);
};

View File

@@ -0,0 +1,6 @@
import { TeamSummaryDto } from '../dtos';
import { TeamSummaryViewModel } from '../view-models';
export const presentTeamSummary = (dto: TeamSummaryDto): TeamSummaryViewModel => {
return new TeamSummaryViewModel(dto);
};

View File

@@ -0,0 +1,6 @@
import { WalletDto, WalletTransactionDto } from '../dtos';
import { WalletViewModel } from '../view-models';
export const presentWallet = (dto: WalletDto): WalletViewModel => {
return new WalletViewModel(dto);
};

View File

@@ -0,0 +1,6 @@
import { WalletTransactionDto } from '../dtos';
import { WalletTransactionViewModel } from '../view-models';
export const presentWalletTransaction = (dto: WalletTransactionDto): WalletTransactionViewModel => {
return new WalletTransactionViewModel(dto);
};

View File

@@ -0,0 +1,35 @@
// Analytics Presenters
// Auth Presenters
// Driver Presenters
export { presentDriversLeaderboard } from './DriversLeaderboardPresenter';
export { presentDriverRegistrationStatus } from './DriverRegistrationStatusPresenter';
// League Presenters
export { presentLeagueMember } from './LeagueMemberPresenter';
export { presentLeagueStandings } from './LeagueStandingsPresenter';
export { presentLeagueSummaries, presentLeagueSummary } from './LeagueSummaryPresenter';
// Payments Presenters
export { presentMembershipFee } from './MembershipFeePresenter';
export { presentPayment } from './PaymentPresenter';
export { presentPrize } from './PrizePresenter';
export { presentWallet } from './WalletPresenter';
export { presentWalletTransaction } from './WalletTransactionPresenter';
// Race Presenters
export { presentRaceDetail } from './RaceDetailPresenter';
export { presentRaceListItem } from './RaceListItemPresenter';
export { presentRaceResult } from './RaceResultsPresenter';
export { presentRaceResultsDetail } from './RaceResultsDetailPresenter';
// Sponsor Presenters
export { presentSponsor } from './SponsorPresenter';
export { presentSponsorshipDetail } from './SponsorshipDetailPresenter';
// Team Presenters
export { presentTeamDetails } from './TeamDetailsPresenter';
export { presentTeamJoinRequest } from './TeamJoinRequestPresenter';
export { presentTeamMember } from './TeamMemberPresenter';
export { presentTeamSummary } from './TeamSummaryPresenter';