refactor
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user