website refactor

This commit is contained in:
2026-01-20 22:31:14 +01:00
parent 51288234f4
commit 7cbec00474
52 changed files with 577 additions and 146 deletions

View File

@@ -1,4 +1,5 @@
import type { GetAllTeamsOutputDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO';
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO';
import type { GetTeamMembersOutputDTO } from '@/lib/types/generated/GetTeamMembersOutputDTO';
import type { GetTeamJoinRequestsOutputDTO } from '@/lib/types/generated/GetTeamJoinRequestsOutputDTO';
@@ -21,6 +22,11 @@ export class TeamsApiClient extends BaseApiClient {
return this.get<GetAllTeamsOutputDTO>('/teams/all');
}
/** Get teams leaderboard */
getLeaderboard(): Promise<GetTeamsLeaderboardOutputDTO> {
return this.get<GetTeamsLeaderboardOutputDTO>('/teams/leaderboard');
}
/** Get team details */
getDetails(teamId: string): Promise<GetTeamDetailsOutputDTO | null> {
return this.get<GetTeamDetailsOutputDTO | null>(`/teams/${teamId}`);

View File

@@ -1,10 +1,10 @@
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
export class LeaderboardsViewDataBuilder {
static build(
apiDto: { drivers: { drivers: DriverLeaderboardItemDTO[] }; teams: { teams: TeamListItemDTO[] } }
apiDto: { drivers: { drivers: DriverLeaderboardItemDTO[] }; teams: GetTeamsLeaderboardOutputDTO }
): LeaderboardsViewData {
return {
drivers: apiDto.drivers.drivers.slice(0, 10).map(driver => ({
@@ -18,19 +18,19 @@ export class LeaderboardsViewDataBuilder {
avatarUrl: driver.avatarUrl || '',
position: driver.rank,
})),
teams: apiDto.teams.teams.slice(0, 10).map((team, index) => ({
teams: apiDto.teams.topTeams.map((team, index) => ({
id: team.id,
name: team.name,
tag: team.tag,
memberCount: team.memberCount,
category: team.category,
category: undefined,
totalWins: team.totalWins || 0,
logoUrl: team.logoUrl || '',
position: index + 1,
isRecruiting: team.isRecruiting,
performanceLevel: team.performanceLevel || 'N/A',
rating: team.rating,
rating: team.rating || 0,
})),
};
}
}
}

View File

@@ -0,0 +1,27 @@
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData';
export class TeamRankingsViewDataBuilder {
static build(apiDto: GetTeamsLeaderboardOutputDTO): TeamRankingsViewData {
const allTeams = apiDto.teams.map((team, index) => ({
id: team.id,
name: team.name,
tag: team.tag,
memberCount: team.memberCount,
category: undefined,
totalWins: team.totalWins || 0,
logoUrl: team.logoUrl || '',
position: index + 1,
isRecruiting: team.isRecruiting,
performanceLevel: team.performanceLevel || 'N/A',
rating: team.rating || 0,
totalRaces: team.totalRaces || 0,
}));
return {
teams: allTeams,
podium: allTeams.slice(0, 3),
recruitingCount: apiDto.recruitingCount,
};
}
}

View File

@@ -0,0 +1,31 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { TeamRankingsViewDataBuilder } from '@/lib/builders/view-data/TeamRankingsViewDataBuilder';
import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData';
import { TeamRankingsService } from '@/lib/services/leaderboards/TeamRankingsService';
import { mapToPresentationError, type PresentationError } from '@/lib/contracts/page-queries/PresentationError';
/**
* Team Rankings page query
* Returns Result<TeamRankingsViewData, PresentationError>
*/
export class TeamRankingsPageQuery implements PageQuery<TeamRankingsViewData, void, PresentationError> {
async execute(): Promise<Result<TeamRankingsViewData, PresentationError>> {
const service = new TeamRankingsService();
const serviceResult = await service.getTeamRankings();
if (serviceResult.isErr()) {
return Result.err(mapToPresentationError(serviceResult.getError()));
}
const apiDto = serviceResult.unwrap();
const viewData = TeamRankingsViewDataBuilder.build(apiDto);
return Result.ok(viewData);
}
static async execute(): Promise<Result<TeamRankingsViewData, PresentationError>> {
const query = new TeamRankingsPageQuery();
return query.execute();
}
}

View File

@@ -93,6 +93,7 @@ export interface RouteGroup {
leaderboards: {
root: string;
drivers: string;
teams: string;
};
error: {
notFound: string;
@@ -119,7 +120,7 @@ export interface RouteGroup {
* }
* ```
*/
export const routes: RouteGroup & { leaderboards: { root: string; drivers: string } } = {
export const routes: RouteGroup & { leaderboards: { root: string; drivers: string; teams: string } } = {
auth: {
login: '/auth/login',
signup: '/auth/signup',
@@ -192,6 +193,7 @@ export const routes: RouteGroup & { leaderboards: { root: string; drivers: strin
leaderboards: {
root: '/leaderboards',
drivers: '/leaderboards/drivers',
teams: '/leaderboards/teams',
},
error: {
notFound: '/404',

View File

@@ -20,7 +20,7 @@ export class LeaderboardsService implements Service {
const [driverResult, teamResult] = await Promise.all([
driversApiClient.getLeaderboard(),
teamsApiClient.getAll()
teamsApiClient.getLeaderboard()
]);
if (!driverResult) {

View File

@@ -0,0 +1,50 @@
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import { Result } from '@/lib/contracts/Result';
import { Service, DomainError } from '@/lib/contracts/services/Service';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ApiError } from '@/lib/api/base/ApiError';
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
export class TeamRankingsService implements Service {
async getTeamRankings(): Promise<Result<GetTeamsLeaderboardOutputDTO, DomainError>> {
try {
const baseUrl = getWebsiteApiBaseUrl();
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
const teamResult = await teamsApiClient.getLeaderboard();
if (!teamResult) {
return Result.err({ type: 'notFound', message: 'No team leaderboard data available' });
}
return Result.ok(teamResult);
} catch (error) {
if (error instanceof ApiError) {
switch (error.type) {
case 'NOT_FOUND':
return Result.err({ type: 'notFound', message: error.message });
case 'AUTH_ERROR':
return Result.err({ type: 'unauthorized', message: error.message });
case 'SERVER_ERROR':
return Result.err({ type: 'serverError', message: error.message });
case 'NETWORK_ERROR':
case 'TIMEOUT_ERROR':
return Result.err({ type: 'networkError', message: error.message });
default:
return Result.err({ type: 'unknown', message: error.message });
}
}
if (error instanceof Error) {
return Result.err({ type: 'unknown', message: error.message });
}
return Result.err({ type: 'unknown', message: 'Team rankings fetch failed' });
}
}
}

View File

@@ -1,7 +1,7 @@
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
export interface LeaderboardsData {
drivers: { drivers: DriverLeaderboardItemDTO[] };
teams: { teams: TeamListItemDTO[] };
}
teams: GetTeamsLeaderboardOutputDTO;
}

View File

@@ -8,6 +8,8 @@
export interface TeamLeaderboardItemDTO {
id: string;
name: string;
tag: string;
logoUrl?: string;
memberCount: number;
rating?: number;
totalWins: number;

View File

@@ -5,9 +5,10 @@ export interface LeaderboardTeamItem {
memberCount: number;
category?: string;
totalWins: number;
totalRaces?: number;
logoUrl: string;
position: number;
isRecruiting: boolean;
performanceLevel: string;
rating?: number;
}
}

View File

@@ -0,0 +1,7 @@
import type { LeaderboardTeamItem } from './LeaderboardTeamItem';
export interface TeamRankingsViewData {
teams: LeaderboardTeamItem[];
podium: LeaderboardTeamItem[];
recruitingCount: number;
}