This commit is contained in:
2025-12-16 10:50:15 +01:00
parent 775d41e055
commit 8ed6ba1fd1
144 changed files with 5763 additions and 1985 deletions

View File

@@ -1,10 +1,87 @@
import { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService';
import type {
IDriversLeaderboardPresenter,
DriverLeaderboardItemViewModel,
DriversLeaderboardViewModel,
DriversLeaderboardResultDTO,
} from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
/**
* DriversLeaderboardPresenter - Pure data transformer
* Transforms API response to view model without DI dependencies.
*/
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;
@@ -13,63 +90,20 @@ export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter
this.viewModel = null;
}
present(input: DriversLeaderboardResultDTO): void {
const { drivers, rankings, stats, avatarUrls } = input;
const items: DriverLeaderboardItemViewModel[] = drivers.map((driver) => {
const driverStats = stats[driver.id];
const rating = driverStats?.rating ?? 0;
const wins = driverStats?.wins ?? 0;
const podiums = driverStats?.podiums ?? 0;
const totalRaces = driverStats?.totalRaces ?? 0;
let effectiveRank = Number.POSITIVE_INFINITY;
if (typeof driverStats?.overallRank === 'number' && driverStats.overallRank > 0) {
effectiveRank = driverStats.overallRank;
} else {
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
if (indexInGlobal !== -1) {
effectiveRank = indexInGlobal + 1;
}
}
const skillLevel = SkillLevelService.getSkillLevel(rating);
const isActive = rankings.some((r) => r.driverId === driver.id);
return {
id: driver.id,
name: driver.name,
rating,
skillLevel,
nationality: driver.country,
racesCompleted: totalRaces,
wins,
podiums,
isActive,
rank: effectiveRank,
avatarUrl: avatarUrls[driver.id] ?? '',
};
});
items.sort((a, b) => {
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
if (rankA !== rankB) return rankA - rankB;
return b.rating - a.rating;
});
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;
this.viewModel = {
drivers: items,
totalRaces,
totalWins,
activeCount,
};
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);
}