website refactor

This commit is contained in:
2026-01-21 12:55:22 +01:00
parent a6e93acb37
commit 7075765d98
14 changed files with 489 additions and 19 deletions

View File

@@ -35,11 +35,11 @@ export class LeagueDetailViewDataBuilder {
// Calculate info data
const membersCount = Array.isArray(memberships.members) ? memberships.members.length : 0;
const completedRacesCount = races.filter(r => {
const status = (r as any).status;
return status === 'completed' || status === 'past';
}).length;
// League overview wants total races, not just completed.
// (In seed/demo data many races are `status: running`, which should still count.)
const racesCount = races.length;
// Compute real avgSOF from races
const racesWithSOF = races.filter(r => {
const sof = (r as any).strengthOfField;
@@ -48,12 +48,25 @@ export class LeagueDetailViewDataBuilder {
const avgSOF = racesWithSOF.length > 0
? Math.round(racesWithSOF.reduce((sum, r) => sum + ((r as any).strengthOfField || 0), 0) / racesWithSOF.length)
: null;
if (process.env.NODE_ENV !== 'production') {
const race0 = races.length > 0 ? races[0] : null;
console.info(
'[LeagueDetailViewDataBuilder] leagueId=%s members=%d races=%d racesWithSOF=%d avgSOF=%s race0=%o',
league.id,
membersCount,
racesCount,
racesWithSOF.length,
String(avgSOF),
race0,
);
}
const info: LeagueInfoData = {
name: league.name,
description: league.description || '',
membersCount,
racesCount: completedRacesCount,
racesCount,
avgSOF,
structure: `Solo • ${league.settings?.maxDrivers ?? 32} max`,
scoring: scoringConfig?.scoringPresetId || 'Standard',

View File

@@ -8,6 +8,8 @@ describe('DashboardService', () => {
let service: DashboardService;
beforeEach(() => {
process.env.API_BASE_URL = 'http://localhost:3001';
mockApiClient = {
getDashboardOverview: vi.fn(),
getAnalyticsMetrics: vi.fn(),

View File

@@ -12,6 +12,8 @@ describe('LandingService', () => {
let service: LandingService;
beforeEach(() => {
process.env.API_BASE_URL = 'http://localhost:3001';
mockRacesApi = {
getPageData: vi.fn(),
} as unknown as Mocked<RacesApiClient>;

View File

@@ -1,15 +1,18 @@
import { describe, it, expect, vi, Mocked, beforeEach } from 'vitest';
import { LeagueService } from './LeagueService';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
import type { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO';
import type { RemoveLeagueMemberOutputDTO } from '@/lib/types/generated/RemoveLeagueMemberOutputDTO';
describe('LeagueService', () => {
let mockApiClient: Mocked<LeaguesApiClient>;
let mockRacesApiClient: Mocked<RacesApiClient>;
let service: LeagueService;
beforeEach(() => {
process.env.API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001';
mockApiClient = {
getAllWithCapacity: vi.fn(),
getAllWithCapacityAndScoring: vi.fn(),
@@ -17,12 +20,20 @@ describe('LeagueService', () => {
getTotal: vi.fn(),
getSchedule: vi.fn(),
getMemberships: vi.fn(),
getLeagueConfig: vi.fn(),
create: vi.fn(),
removeRosterMember: vi.fn(),
updateRosterMemberRole: vi.fn(),
} as unknown as Mocked<LeaguesApiClient>;
mockRacesApiClient = {
getPageData: vi.fn(),
} as unknown as Mocked<RacesApiClient>;
mockApiClient.getLeagueConfig.mockResolvedValue({ form: null } as any);
service = new LeagueService(mockApiClient);
(service as any).racesApiClient = mockRacesApiClient;
});
describe('getAllLeagues', () => {
@@ -145,6 +156,43 @@ describe('LeagueService', () => {
});
});
describe('getLeagueDetailData', () => {
it('should use races page-data to enrich races with status/strengthOfField', async () => {
const leagueId = 'league-123';
const league = { id: leagueId, name: 'League One', createdAt: '2024-01-01T00:00:00Z' } as any;
mockApiClient.getAllWithCapacityAndScoring.mockResolvedValue({ totalCount: 1, leagues: [league] } as any);
mockApiClient.getMemberships.mockResolvedValue({ members: [] } as any);
mockRacesApiClient.getPageData.mockResolvedValue({
races: [
{
id: 'race-1',
track: 'Monza',
car: 'GT3',
scheduledAt: '2026-01-01T00:00:00.000Z',
status: 'running',
leagueId,
leagueName: 'League One',
strengthOfField: 2500,
isUpcoming: false,
isLive: true,
isPast: false,
},
],
} as any);
const result = await service.getLeagueDetailData(leagueId);
expect(result.isOk()).toBe(true);
const dto = result.unwrap();
expect(dto.races).toHaveLength(1);
expect((dto.races[0] as any).status).toBe('running');
expect((dto.races[0] as any).strengthOfField).toBe(2500);
expect(dto.races[0]!.name).toBe('Monza - GT3');
expect(dto.races[0]!.date).toBe('2026-01-01T00:00:00.000Z');
});
});
describe('getLeagueMemberships', () => {
it('should call apiClient.getMemberships and return DTO', async () => {
const leagueId = 'league-123';

View File

@@ -58,6 +58,7 @@ export interface LeagueDetailData {
*/
@injectable()
export class LeagueService implements Service {
private readonly baseUrl: string;
private apiClient: LeaguesApiClient;
private driversApiClient: DriversApiClient;
private sponsorsApiClient: SponsorsApiClient;
@@ -65,6 +66,7 @@ export class LeagueService implements Service {
constructor(@unmanaged() apiClient?: LeaguesApiClient) {
const baseUrl = getWebsiteApiBaseUrl();
this.baseUrl = baseUrl;
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
@@ -148,11 +150,27 @@ export class LeagueService implements Service {
async getLeagueDetailData(leagueId: string): Promise<Result<LeagueDetailData, DomainError>> {
try {
const [apiDto, memberships, racesResponse] = await Promise.all([
const [apiDto, memberships, racesPageData] = await Promise.all([
this.apiClient.getAllWithCapacityAndScoring(),
this.apiClient.getMemberships(leagueId),
this.apiClient.getRaces(leagueId),
this.racesApiClient.getPageData(leagueId),
]);
if (process.env.NODE_ENV !== 'production') {
const membershipCount = Array.isArray(memberships?.members) ? memberships.members.length : 0;
const racesCount = Array.isArray(racesPageData?.races) ? racesPageData.races.length : 0;
const race0 = racesCount > 0 ? racesPageData.races[0] : null;
console.info(
'[LeagueService.getLeagueDetailData] baseUrl=%s leagueId=%s memberships=%d races=%d race0=%o',
this.baseUrl,
leagueId,
membershipCount,
racesCount,
race0,
);
}
if (!apiDto || !apiDto.leagues) {
return Result.err({ type: 'notFound', message: 'Leagues not found' });
@@ -189,12 +207,21 @@ export class LeagueService implements Service {
console.warn('Failed to fetch league scoring config', e);
}
const races: RaceDTO[] = (racesPageData.races || []).map((r) => ({
id: r.id,
name: `${r.track} - ${r.car}`,
date: r.scheduledAt,
leagueName: r.leagueName,
status: r.status,
strengthOfField: r.strengthOfField,
})) as unknown as RaceDTO[];
return Result.ok({
league,
owner,
scoringConfig,
memberships,
races: racesResponse.races,
races,
sponsors: [], // Sponsors integration can be added here
});
} catch (error: unknown) {

View File

@@ -11,6 +11,8 @@ describe('SponsorService', () => {
let mockApiClientInstance: any;
beforeEach(() => {
process.env.API_BASE_URL = 'http://localhost:3001';
vi.clearAllMocks();
service = new SponsorService();
// @ts-ignore - accessing private property for testing