website refactor

This commit is contained in:
2026-01-16 11:13:42 +01:00
parent d86aa4583b
commit d6b94e21df
30 changed files with 217 additions and 182 deletions

View File

@@ -0,0 +1,60 @@
import { AdminApiClient } from './admin/AdminApiClient';
import { AnalyticsApiClient } from './analytics/AnalyticsApiClient';
import { AuthApiClient } from './auth/AuthApiClient';
import { DashboardApiClient } from './dashboard/DashboardApiClient';
import { DriversApiClient } from './drivers/DriversApiClient';
import { LeaguesApiClient } from './leagues/LeaguesApiClient';
import { MediaApiClient } from './media/MediaApiClient';
import { PaymentsApiClient } from './payments/PaymentsApiClient';
import { PenaltiesApiClient } from './penalties/PenaltiesApiClient';
import { PolicyApiClient } from './policy/PolicyApiClient';
import { ProtestsApiClient } from './protests/ProtestsApiClient';
import { RacesApiClient } from './races/RacesApiClient';
import { SponsorsApiClient } from './sponsors/SponsorsApiClient';
import { TeamsApiClient } from './teams/TeamsApiClient';
import { WalletsApiClient } from './wallets/WalletsApiClient';
import { ErrorReporter } from '../interfaces/ErrorReporter';
import { Logger } from '../interfaces/Logger';
export class ApiClient {
public readonly admin: AdminApiClient;
public readonly analytics: AnalyticsApiClient;
public readonly auth: AuthApiClient;
public readonly dashboard: DashboardApiClient;
public readonly drivers: DriversApiClient;
public readonly leagues: LeaguesApiClient;
public readonly media: MediaApiClient;
public readonly payments: PaymentsApiClient;
public readonly penalties: PenaltiesApiClient;
public readonly policy: PolicyApiClient;
public readonly protests: ProtestsApiClient;
public readonly races: RacesApiClient;
public readonly sponsors: SponsorsApiClient;
public readonly teams: TeamsApiClient;
public readonly wallets: WalletsApiClient;
constructor(baseUrl: string) {
// Default implementations for logger and error reporter if needed
const logger: Logger = console;
const errorReporter: ErrorReporter = { report: (error) => console.error(error) };
this.admin = new AdminApiClient(baseUrl, errorReporter, logger);
this.analytics = new AnalyticsApiClient(baseUrl, errorReporter, logger);
this.auth = new AuthApiClient(baseUrl, errorReporter, logger);
this.dashboard = new DashboardApiClient(baseUrl, errorReporter, logger);
this.drivers = new DriversApiClient(baseUrl, errorReporter, logger);
this.leagues = new LeaguesApiClient(baseUrl, errorReporter, logger);
this.media = new MediaApiClient(baseUrl, errorReporter, logger);
this.payments = new PaymentsApiClient(baseUrl, errorReporter, logger);
this.penalties = new PenaltiesApiClient(baseUrl, errorReporter, logger);
this.policy = new PolicyApiClient(baseUrl, errorReporter, logger);
this.protests = new ProtestsApiClient(baseUrl, errorReporter, logger);
this.races = new RacesApiClient(baseUrl, errorReporter, logger);
this.sponsors = new SponsorsApiClient(baseUrl, errorReporter, logger);
this.teams = new TeamsApiClient(baseUrl, errorReporter, logger);
this.wallets = new WalletsApiClient(baseUrl, errorReporter, logger);
}
}
// Export a default instance if needed, but apiClient.ts seems to handle it
export const api = new ApiClient(process.env.NEXT_PUBLIC_API_URL || '');

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { ApiClient, api } from './index';
import { ApiClient, api } from './ApiClient';
describe('ApiClient', () => {
it('should be defined', () => {

View File

@@ -1,4 +1,4 @@
import { ApiClient } from './api/index';
import { ApiClient } from './api/ApiClient';
import { getWebsiteApiBaseUrl } from './config/apiBaseUrl';
export const apiClient = new ApiClient(getWebsiteApiBaseUrl());
export const apiClient = new ApiClient(getWebsiteApiBaseUrl());

View File

@@ -1,27 +0,0 @@
import { RacesAllViewData } from '@/lib/view-data/races/RacesAllViewData';
/**
* Races All View Data Builder
*
* Transforms API DTO into ViewData for the all races template.
* Deterministic, side-effect free.
*/
export class RacesAllViewDataBuilder {
static build(apiDto: any): RacesAllViewData {
const races = apiDto.races.map((race: any) => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
status: race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
sessionType: 'race',
leagueId: race.leagueId,
leagueName: race.leagueName,
strengthOfField: race.strengthOfField ?? undefined,
}));
return {
races,
};
}
}

View File

@@ -68,7 +68,7 @@ export class TeamDetailPageQuery implements PageQuery<TeamDetailViewData, string
const teamData = teamResult.unwrap();
// Fetch team members
const membersResult = await service.getTeamMembers(teamId, currentDriverId, teamData.ownerId);
const membersResult = await service.getTeamMembers(teamId, currentDriverId, teamData.team.ownerId);
if (membersResult.isErr()) {
return Result.err(mapToPresentationError(membersResult.getError()));
@@ -79,17 +79,17 @@ export class TeamDetailPageQuery implements PageQuery<TeamDetailViewData, string
// Transform to raw serializable DTO
const dto: TeamDetailPageDto = {
team: {
id: teamData.id,
name: teamData.name,
tag: teamData.tag,
description: teamData.description,
ownerId: teamData.ownerId,
leagues: teamData.leagues,
createdAt: teamData.createdAt,
specialization: teamData.specialization,
region: teamData.region,
languages: teamData.languages,
category: teamData.category,
id: teamData.team.id,
name: teamData.team.name,
tag: teamData.team.tag,
description: teamData.team.description,
ownerId: teamData.team.ownerId,
leagues: teamData.team.leagues,
createdAt: teamData.team.createdAt,
specialization: (teamData.team as any).specialization,
region: (teamData.team as any).region,
languages: (teamData.team as any).languages,
category: teamData.team.category,
membership: teamData.membership ? {
role: teamData.membership.role,
joinedAt: teamData.membership.joinedAt,

View File

@@ -1,18 +1,18 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { RacesAllViewData } from '@/lib/view-data/races/RacesAllViewData';
import { RacesViewData } from '@/lib/view-data/RacesViewData';
import { RacesService } from '@/lib/services/races/RacesService';
import { RacesAllViewDataBuilder } from '@/lib/builders/view-data/RacesAllViewDataBuilder';
import { RacesViewDataBuilder } from '@/lib/builders/view-data/RacesViewDataBuilder';
/**
* Races All Page Query
*
* Fetches all races data for the all races page.
* Returns Result<RacesAllViewData, PresentationError>
* Returns Result<RacesViewData, PresentationError>
*/
export class RacesAllPageQuery implements PageQuery<RacesAllViewData, void> {
async execute(): Promise<Result<RacesAllViewData, PresentationError>> {
export class RacesAllPageQuery implements PageQuery<RacesViewData, void> {
async execute(): Promise<Result<RacesViewData, PresentationError>> {
// Manual wiring: Service creates its own dependencies
const service = new RacesService();
@@ -24,12 +24,12 @@ export class RacesAllPageQuery implements PageQuery<RacesAllViewData, void> {
}
// Transform to ViewData using builder
const viewData = RacesAllViewDataBuilder.build(result.unwrap());
const viewData = RacesViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
// Static method to avoid object construction in server code
static async execute(): Promise<Result<RacesAllViewData, PresentationError>> {
static async execute(): Promise<Result<RacesViewData, PresentationError>> {
const query = new RacesAllPageQuery();
return await query.execute();
}

View File

@@ -29,6 +29,10 @@ export interface ErrorStats {
};
}
export function getErrorAnalyticsStats(): ErrorStats {
return ErrorAnalyticsService.getErrorAnalyticsStats();
}
export class ErrorAnalyticsService implements Service {
static getErrorAnalyticsStats(): ErrorStats {
const globalHandler = getGlobalErrorHandler();

View File

@@ -1,4 +1,6 @@
import { ApiClient } from '@/lib/api';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { Result } from '@/lib/contracts/Result';
import { Service, type DomainError } from '@/lib/contracts/services/Service';
@@ -28,9 +30,11 @@ export class ProfileLeaguesService implements Service {
async getProfileLeagues(driverId: string): Promise<Result<ProfileLeaguesPageDto, DomainError>> {
try {
const baseUrl = getWebsiteApiBaseUrl();
const apiClient = new ApiClient(baseUrl);
const logger = new ConsoleLogger();
const errorReporter = new ConsoleErrorReporter();
const leaguesApiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
const leaguesDto = await apiClient.leagues.getAllWithCapacity();
const leaguesDto = await leaguesApiClient.getAllWithCapacity();
if (!leaguesDto?.leagues) {
return Result.err({ type: 'notFound', message: 'Leagues not found' });
@@ -40,7 +44,7 @@ export class ProfileLeaguesService implements Service {
const leagueMemberships = await Promise.all(
leaguesDto.leagues.map(async (league) => {
try {
const membershipsDto = await apiClient.leagues.getMemberships(league.id);
const membershipsDto = await leaguesApiClient.getMemberships(league.id);
let memberships: MembershipDTO[] = [];
if (membershipsDto && typeof membershipsDto === 'object') {

View File

@@ -41,6 +41,15 @@ export class RaceService implements Service {
}
}
async getAllRacesPageData(): Promise<Result<any, DomainError>> {
try {
const data = await this.apiClient.getPageData();
return Result.ok(data);
} catch (error: unknown) {
return Result.err(this.mapError(error, 'Failed to fetch all races page data'));
}
}
async findByLeagueId(leagueId: string): Promise<Result<unknown[], DomainError>> {
try {
const result = await this.apiClient.getPageData(leagueId);

View File

@@ -1,5 +1,12 @@
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
import type { CreateTeamInputDTO } from '@/lib/types/generated/CreateTeamInputDTO';
import type { CreateTeamOutputDTO } from '@/lib/types/generated/CreateTeamOutputDTO';
import type { UpdateTeamInputDTO } from '@/lib/types/generated/UpdateTeamInputDTO';
import type { UpdateTeamOutputDTO } from '@/lib/types/generated/UpdateTeamOutputDTO';
import type { GetDriverTeamOutputDTO } from '@/lib/types/generated/GetDriverTeamOutputDTO';
import type { GetTeamMembershipOutputDTO } from '@/lib/types/generated/GetTeamMembershipOutputDTO';
import type { GetTeamJoinRequestsOutputDTO } from '@/lib/types/generated/GetTeamJoinRequestsOutputDTO';
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
import { Result } from '@/lib/contracts/Result';
import { DomainError, Service } from '@/lib/contracts/services/Service';
@@ -37,7 +44,7 @@ export class TeamService implements Service {
}
}
async getTeamDetails(teamId: string, _: string): Promise<Result<unknown, DomainError>> {
async getTeamDetails(teamId: string, _: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getDetails(teamId);
if (!result) {
@@ -58,23 +65,48 @@ export class TeamService implements Service {
}
}
async getTeamJoinRequests(_: string): Promise<Result<unknown, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getTeamJoinRequests' });
async getTeamJoinRequests(teamId: string): Promise<Result<GetTeamJoinRequestsOutputDTO, DomainError>> {
try {
const result = await this.apiClient.getJoinRequests(teamId);
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch team join requests' });
}
}
async createTeam(__: unknown): Promise<Result<unknown, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'createTeam' });
async createTeam(input: CreateTeamInputDTO): Promise<Result<CreateTeamOutputDTO, DomainError>> {
try {
const result = await this.apiClient.create(input);
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to create team' });
}
}
async updateTeam(_: string, __: unknown): Promise<Result<unknown, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'updateTeam' });
async updateTeam(teamId: string, input: UpdateTeamInputDTO): Promise<Result<UpdateTeamOutputDTO, DomainError>> {
try {
const result = await this.apiClient.update(teamId, input);
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to update team' });
}
}
async getDriverTeam(_: string): Promise<Result<unknown, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getDriverTeam' });
async getDriverTeam(driverId: string): Promise<Result<GetDriverTeamOutputDTO | null, DomainError>> {
try {
const result = await this.apiClient.getDriverTeam(driverId);
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch driver team' });
}
}
async getMembership(_: string, __: string): Promise<Result<unknown, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getMembership' });
async getMembership(teamId: string, driverId: string): Promise<Result<GetTeamMembershipOutputDTO | null, DomainError>> {
try {
const result = await this.apiClient.getMembership(teamId, driverId);
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch team membership' });
}
}
}

View File

@@ -18,6 +18,18 @@ export interface SponsorDashboardViewData {
activeSponsorships: number;
formattedTotalInvestment: string;
costPerThousandViews: string;
upcomingRenewals: unknown[];
recentActivity: unknown[];
}
upcomingRenewals: Array<{
id: string;
type: 'league' | 'team' | 'driver' | 'race' | 'platform';
name: string;
formattedRenewDate: string;
formattedPrice: string;
}>;
recentActivity: Array<{
id: string;
message: string;
time: string;
typeColor: string;
formattedImpressions?: string | null;
}>;
}

View File

@@ -1,22 +0,0 @@
/**
* Races All View Data
*
* ViewData for the all races page template.
* JSON-serializable, template-ready data structure.
*/
export interface RacesAllRace {
id: string;
track: string;
car: string;
scheduledAt: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
sessionType: string;
leagueId?: string;
leagueName?: string;
strengthOfField?: number;
}
export interface RacesAllViewData {
races: RacesAllRace[];
}

View File

@@ -1,29 +0,0 @@
/**
* Races View Data
*
* ViewData for the main races page template.
* JSON-serializable, template-ready data structure.
*/
export interface RacesRace {
id: string;
track: string;
car: string;
scheduledAt: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
sessionType: string;
leagueId?: string;
leagueName?: string;
strengthOfField?: number;
isUpcoming: boolean;
isLive: boolean;
isPast: boolean;
}
export interface RacesViewData {
races: RacesRace[];
totalCount: number;
scheduledRaces: RacesRace[];
runningRaces: RacesRace[];
completedRaces: RacesRace[];
}