website cleanup

This commit is contained in:
2025-12-24 13:04:18 +01:00
parent 5e491d9724
commit a7aee42409
69 changed files with 1624 additions and 938 deletions

View File

@@ -10,6 +10,8 @@ import { PaymentsApiClient } from './payments/PaymentsApiClient';
import { DashboardApiClient } from './dashboard/DashboardApiClient';
import { PenaltiesApiClient } from './penalties/PenaltiesApiClient';
import { ProtestsApiClient } from './protests/ProtestsApiClient';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
/**
* Main API Client
@@ -31,18 +33,21 @@ export class ApiClient {
public readonly protests: ProtestsApiClient;
constructor(baseUrl: string) {
this.leagues = new LeaguesApiClient(baseUrl);
this.races = new RacesApiClient(baseUrl);
this.drivers = new DriversApiClient(baseUrl);
this.teams = new TeamsApiClient(baseUrl);
this.sponsors = new SponsorsApiClient(baseUrl);
this.media = new MediaApiClient(baseUrl);
this.analytics = new AnalyticsApiClient(baseUrl);
this.auth = new AuthApiClient(baseUrl);
this.payments = new PaymentsApiClient(baseUrl);
this.dashboard = new DashboardApiClient(baseUrl);
this.penalties = new PenaltiesApiClient(baseUrl);
this.protests = new ProtestsApiClient(baseUrl);
const logger = new ConsoleLogger();
const errorReporter = new ConsoleErrorReporter();
this.leagues = new LeaguesApiClient(baseUrl, errorReporter, logger);
this.races = new RacesApiClient(baseUrl, errorReporter, logger);
this.drivers = new DriversApiClient(baseUrl, errorReporter, logger);
this.teams = new TeamsApiClient(baseUrl, errorReporter, logger);
this.sponsors = new SponsorsApiClient(baseUrl, errorReporter, logger);
this.media = new MediaApiClient(baseUrl, errorReporter, logger);
this.analytics = new AnalyticsApiClient(baseUrl, errorReporter, logger);
this.auth = new AuthApiClient(baseUrl, errorReporter, logger);
this.payments = new PaymentsApiClient(baseUrl, errorReporter, logger);
this.dashboard = new DashboardApiClient(baseUrl, errorReporter, logger);
this.penalties = new PenaltiesApiClient(baseUrl, errorReporter, logger);
this.protests = new ProtestsApiClient(baseUrl, errorReporter, logger);
}
}

View File

@@ -0,0 +1,24 @@
import { LeagueMembershipService } from './services/leagues/LeagueMembershipService';
/**
* Get membership for a driver in a league
*/
export function getMembership(leagueId: string, driverId: string) {
return LeagueMembershipService.getMembership(leagueId, driverId);
}
/**
* Get all members of a league
*/
export function getLeagueMembers(leagueId: string) {
return LeagueMembershipService.getLeagueMembers(leagueId);
}
/**
* Get primary league ID for a driver (first league they joined)
*/
export function getPrimaryLeagueIdForDriver(driverId: string): string | null {
const memberships = LeagueMembershipService.getAllMembershipsForDriver(driverId);
if (memberships.length === 0) return null;
return memberships[0].leagueId;
}

View File

@@ -0,0 +1,8 @@
// Re-export from utilities for backward compatibility
export { LeagueRoleUtility } from './utilities/LeagueRoleUtility';
export { LeagueMembershipUtility } from './utilities/LeagueMembershipUtility';
// Direct function export for convenience
export const isLeagueAdminOrHigherRole = (role: string): boolean => {
return role === 'owner' || role === 'admin' || role === 'steward';
};

View File

@@ -2,6 +2,7 @@ import { AuthApiClient } from '../../api/auth/AuthApiClient';
import { SessionViewModel } from '../../view-models/SessionViewModel';
import type { LoginParams } from '../../types/generated/LoginParams';
import type { SignupParams } from '../../types/generated/SignupParams';
import type { LoginWithIracingCallbackParams } from '../../types/generated/LoginWithIracingCallbackParams';
/**
* Auth Service
@@ -55,4 +56,16 @@ export class AuthService {
getIracingAuthUrl(returnTo?: string): string {
return this.apiClient.getIracingAuthUrl(returnTo);
}
/**
* Login with iRacing callback
*/
async loginWithIracingCallback(params: LoginWithIracingCallbackParams): Promise<SessionViewModel> {
try {
const dto = await this.apiClient.loginWithIracingCallback(params);
return new SessionViewModel(dto.user);
} catch (error) {
throw error;
}
}
}

View File

@@ -1,5 +1,5 @@
import type { DriversApiClient } from '../../api/drivers/DriversApiClient';
import { DriverRegistrationStatusViewModel } from '../../view-models';
import { DriverRegistrationStatusViewModel } from '../../view-models/DriverRegistrationStatusViewModel';
/**
* Driver Registration Service

View File

@@ -1,7 +1,5 @@
import { apiClient } from '@/lib/apiClient';
import type { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
import type { MembershipRole } from '@core/racing/domain/entities/MembershipRole';
import type { MembershipStatus } from '@core/racing/domain/entities/MembershipStatus';
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
export class LeagueMembershipService {
// In-memory cache for memberships (populated via API calls)
@@ -33,12 +31,12 @@ export class LeagueMembershipService {
static async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
try {
const result = await apiClient.leagues.getMemberships(leagueId);
const memberships: LeagueMembership[] = result.members.map(member => ({
const memberships: LeagueMembership[] = result.members.map((member: any) => ({
id: `${member.driverId}-${leagueId}`, // Generate ID since API doesn't provide it
leagueId,
driverId: member.driverId,
role: member.role as MembershipRole,
status: 'active' as MembershipStatus, // Assume active since API returns current members
role: member.role,
status: 'active', // Assume active since API returns current members
joinedAt: member.joinedAt,
}));
this.setLeagueMemberships(leagueId, memberships);
@@ -70,6 +68,20 @@ export class LeagueMembershipService {
return this.leagueMemberships.entries();
}
/**
* Get all memberships for a specific driver across all leagues.
*/
static getAllMembershipsForDriver(driverId: string): LeagueMembership[] {
const allMemberships: LeagueMembership[] = [];
for (const [leagueId, memberships] of this.leagueMemberships.entries()) {
const driverMembership = memberships.find(m => m.driverId === driverId);
if (driverMembership) {
allMemberships.push(driverMembership);
}
}
return allMemberships;
}
// Instance methods that delegate to static methods for consistency with service pattern
/**

View File

@@ -3,6 +3,7 @@ import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient";
import { SponsorsApiClient } from "@/lib/api/sponsors/SponsorsApiClient";
import { RacesApiClient } from "@/lib/api/races/RacesApiClient";
import { CreateLeagueInputDTO } from "@/lib/types/generated/CreateLeagueInputDTO";
import { CreateLeagueOutputDTO } from "@/lib/types/generated/CreateLeagueOutputDTO";
import { LeagueWithCapacityDTO } from "@/lib/types/generated/LeagueWithCapacityDTO";
import { CreateLeagueViewModel } from "@/lib/view-models/CreateLeagueViewModel";
import { LeagueMembershipsViewModel } from "@/lib/view-models/LeagueMembershipsViewModel";
@@ -11,14 +12,12 @@ import { LeagueStandingsViewModel } from "@/lib/view-models/LeagueStandingsViewM
import { LeagueStatsViewModel } from "@/lib/view-models/LeagueStatsViewModel";
import { LeagueSummaryViewModel } from "@/lib/view-models/LeagueSummaryViewModel";
import { RemoveMemberViewModel } from "@/lib/view-models/RemoveMemberViewModel";
import { LeagueDetailViewModel } from "@/lib/view-models/LeagueDetailViewModel";
import { LeaguePageDetailViewModel } from "@/lib/view-models/LeaguePageDetailViewModel";
import { LeagueDetailPageViewModel, SponsorInfo } from "@/lib/view-models/LeagueDetailPageViewModel";
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
import { SubmitBlocker, ThrottleBlocker } from "@/lib/blockers";
import { RaceDTO } from "@/lib/types/generated/RaceDTO";
import { LeagueScoringConfigDTO } from "@/lib/types/LeagueScoringConfigDTO";
import { LeagueStatsDTO } from "@/lib/types/generated/LeagueStatsDTO";
import { LeagueMembershipsDTO } from "@/lib/types/generated/LeagueMembershipsDTO";
/**
@@ -43,7 +42,17 @@ export class LeagueService {
*/
async getAllLeagues(): Promise<LeagueSummaryViewModel[]> {
const dto = await this.apiClient.getAllWithCapacity();
return dto.leagues.map((league: LeagueWithCapacityDTO) => new LeagueSummaryViewModel(league));
return dto.leagues.map((league: LeagueWithCapacityDTO) => ({
id: league.id,
name: league.name,
description: league.description ?? '',
ownerId: league.ownerId,
createdAt: '', // Not provided by API
maxDrivers: league.maxMembers,
usedDriverSlots: league.memberCount,
structureSummary: 'TBD',
timingSummary: 'TBD'
}));
}
/**
@@ -57,8 +66,8 @@ export class LeagueService {
const membershipsDto = await this.apiClient.getMemberships(leagueId);
// Resolve unique drivers that appear in standings
const driverIds = Array.from(new Set(dto.standings.map(entry => entry.driverId)));
const driverDtos = await Promise.all(driverIds.map(id => this.driversApiClient.getDriver(id)));
const driverIds: string[] = Array.from(new Set(dto.standings.map((entry: any) => entry.driverId)));
const driverDtos = await Promise.all(driverIds.map((id: string) => this.driversApiClient.getDriver(id)));
const drivers = driverDtos.filter((d): d is NonNullable<typeof d> => d !== null);
const dtoWithExtras = {
@@ -95,15 +104,17 @@ export class LeagueService {
}
/**
* Create a new league
*/
async createLeague(input: CreateLeagueInputDTO): Promise<void> {
if (!this.submitBlocker.canExecute() || !this.throttle.canExecute()) return;
* Create a new league
*/
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueOutputDTO> {
if (!this.submitBlocker.canExecute() || !this.throttle.canExecute()) {
throw new Error('Cannot execute at this time');
}
this.submitBlocker.block();
this.throttle.block();
try {
await this.apiClient.create(input);
return await this.apiClient.create(input);
} finally {
this.submitBlocker.release();
}
@@ -127,12 +138,12 @@ export class LeagueService {
/**
* Get league detail with owner, membership, and sponsor info
*/
async getLeagueDetail(leagueId: string, currentDriverId: string): Promise<LeagueDetailViewModel | null> {
async getLeagueDetail(leagueId: string, currentDriverId: string): Promise<LeaguePageDetailViewModel | null> {
// For now, assume league data comes from getAllWithCapacity or a new endpoint
// Since API may not have detailed league, we'll mock or assume
// In real implementation, add getLeagueDetail to API
const allLeagues = await this.apiClient.getAllWithCapacity();
const leagueDto = allLeagues.leagues.find(l => l.id === leagueId);
const leagueDto = allLeagues.leagues.find((l: any) => l.id === leagueId);
if (!leagueDto) return null;
// LeagueWithCapacityDTO already carries core fields; fall back to placeholder description/owner when not provided
@@ -149,7 +160,7 @@ export class LeagueService {
// Get membership
const membershipsDto = await this.apiClient.getMemberships(leagueId);
const membership = membershipsDto.members.find(m => m.driverId === currentDriverId);
const membership = membershipsDto.members.find((m: any) => m.driverId === currentDriverId);
const isAdmin = membership ? ['admin', 'owner'].includes(membership.role) : false;
// Get main sponsor
@@ -175,15 +186,31 @@ export class LeagueService {
console.warn('Failed to load main sponsor:', error);
}
return new LeagueDetailViewModel(
league.id,
league.name,
league.description,
league.ownerId,
ownerName,
mainSponsor,
isAdmin
);
return new LeaguePageDetailViewModel({
league: {
id: league.id,
name: league.name,
game: 'iRacing',
tier: 'standard',
season: 'Season 1',
description: league.description,
drivers: 0,
races: 0,
completedRaces: 0,
totalImpressions: 0,
avgViewsPerRace: 0,
engagement: 0,
rating: 0,
seasonStatus: 'active',
seasonDates: { start: new Date().toISOString(), end: new Date().toISOString() },
sponsorSlots: {
main: { available: true, price: 800, benefits: [] },
secondary: { available: 2, total: 2, price: 250, benefits: [] }
}
},
drivers: [],
races: []
});
}
/**
@@ -193,7 +220,7 @@ export class LeagueService {
try {
// Get league basic info
const allLeagues = await this.apiClient.getAllWithCapacity();
const league = allLeagues.leagues.find(l => l.id === leagueId);
const league = allLeagues.leagues.find((l: any) => l.id === leagueId);
if (!league) return null;
// Get owner
@@ -206,7 +233,7 @@ export class LeagueService {
const memberships = await this.apiClient.getMemberships(leagueId);
const driverIds = memberships.members.map(m => m.driverId);
const driverDtos = await Promise.all(driverIds.map(id => this.driversApiClient.getDriver(id)));
const drivers = driverDtos.filter((d): d is NonNullable<typeof d> => d !== null);
const drivers = driverDtos.filter((d: any): d is NonNullable<typeof d> => d !== null);
// Get all races for this league via the leagues API helper
const leagueRaces = await this.apiClient.getRaces(leagueId);
@@ -276,4 +303,12 @@ export class LeagueService {
return [];
}
}
/**
* Get league scoring presets
*/
async getScoringPresets(): Promise<any[]> {
const result = await this.apiClient.getScoringPresets();
return result.presets;
}
}

View File

@@ -54,4 +54,18 @@ export class MediaService {
getDriverAvatar(driverId: string): string {
return `/api/media/avatar/${driverId}`;
}
/**
* Get league cover URL
*/
getLeagueCover(leagueId: string): string {
return `/api/media/leagues/${leagueId}/cover`;
}
/**
* Get league logo URL
*/
getLeagueLogo(leagueId: string): string {
return `/api/media/leagues/${leagueId}/logo`;
}
}

View File

@@ -1,10 +1,8 @@
import type { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient';
import type { GetEntitySponsorshipPricingResultDto } from '../../api/sponsors/SponsorsApiClient';
import {
SponsorshipPricingViewModel,
SponsorSponsorshipsViewModel,
SponsorshipRequestViewModel
} from '../../view-models';
import { SponsorshipPricingViewModel } from '../../view-models/SponsorshipPricingViewModel';
import { SponsorSponsorshipsViewModel } from '../../view-models/SponsorSponsorshipsViewModel';
import { SponsorshipRequestViewModel } from '../../view-models/SponsorshipRequestViewModel';
import type { SponsorSponsorshipsDTO } from '../../types/generated';
/**

View File

@@ -0,0 +1,10 @@
export interface League {
id: string;
name: string;
description: string;
ownerId: string;
isPublic: boolean;
maxMembers: number;
createdAt: string;
updatedAt: string;
}

View File

@@ -0,0 +1,54 @@
export interface LeagueConfigFormModel {
leagueId?: string;
basics?: {
name: string;
description?: string;
visibility: 'public' | 'private' | 'unlisted';
gameId: string;
};
structure?: {
mode: 'solo' | 'fixedTeams';
maxDrivers?: number;
maxTeams?: number;
driversPerTeam?: number;
};
championships?: {
enableDriverChampionship: boolean;
enableTeamChampionship: boolean;
enableNationsChampionship: boolean;
enableTrophyChampionship: boolean;
};
scoring?: {
patternId?: string;
customScoringEnabled?: boolean;
};
dropPolicy?: {
strategy: 'none' | 'bestNResults' | 'dropWorstN';
n?: number;
};
timings?: {
practiceMinutes?: number;
qualifyingMinutes?: number;
sprintRaceMinutes?: number;
mainRaceMinutes?: number;
sessionCount?: number;
roundsPlanned?: number;
raceDayOfWeek?: number;
raceTimeUtc?: string;
weekdays?: string[];
recurrenceStrategy?: string;
timezoneId?: string;
seasonStartDate?: string;
};
stewarding?: {
decisionMode: 'owner_only' | 'admin_vote' | 'steward_panel';
requiredVotes?: number;
requireDefense: boolean;
defenseTimeLimit: number;
voteTimeLimit: number;
protestDeadlineHours: number;
stewardingClosesHours: number;
notifyAccusedOnProtest: boolean;
notifyOnVoteRequired: boolean;
};
}

View File

@@ -0,0 +1,7 @@
export interface LeagueMembership {
driverId: string;
leagueId: string;
role: 'owner' | 'admin' | 'steward' | 'member';
joinedAt: string;
status: 'active' | 'pending' | 'banned';
}

View File

@@ -0,0 +1,5 @@
export interface LeagueScoringConfigDTO {
patternId: string;
customScoringEnabled: boolean;
points: Record<string, number>;
}

View File

@@ -0,0 +1 @@
export type MembershipRole = 'owner' | 'admin' | 'steward' | 'member';

View File

@@ -0,0 +1 @@
export type Weekday = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun';

View File

@@ -0,0 +1,21 @@
export interface WizardErrors {
basics?: {
name?: string;
description?: string;
visibility?: string;
};
structure?: {
maxDrivers?: string;
maxTeams?: string;
driversPerTeam?: string;
};
timings?: {
qualifyingMinutes?: string;
mainRaceMinutes?: string;
roundsPlanned?: string;
};
scoring?: {
patternId?: string;
};
submit?: string;
}

View File

@@ -4,6 +4,14 @@
* Do not edit manually - regenerate using: npm run api:sync-types
*/
import type { DriverDTO } from './DriverDTO';
export interface LeagueStandingDTO {
driverId: string;
driver: DriverDTO;
points: number;
position: number;
wins: number;
podiums: number;
races: number;
}

View File

@@ -1,9 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
*/
export interface PaymentDTO {
id: string;
}

View File

@@ -1,5 +1,5 @@
import type { MembershipRole } from '@core/racing/domain/entities/MembershipRole';
type LeagueRole = MembershipRole;
import type { MembershipRoleDTO } from '@/lib/types/generated/MembershipRoleDTO';
type LeagueRole = MembershipRoleDTO['value'];
export class LeagueRoleUtility {
static isLeagueOwnerRole(role: LeagueRole): boolean {

View File

@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { AvailableLeaguesViewModel, AvailableLeagueViewModel } from './AvailableLeaguesViewModel';
describe('AvailableLeaguesViewModel', () => {
@@ -22,9 +22,9 @@ describe('AvailableLeaguesViewModel', () => {
expect(vm.leagues).toHaveLength(1);
expect(vm.leagues[0]).toBeInstanceOf(AvailableLeagueViewModel);
expect(vm.leagues[0].id).toBe(baseLeague.id);
expect(vm.leagues[0].name).toBe(baseLeague.name);
expect(vm.leagues[0].avgViewsPerRace).toBe(baseLeague.avgViewsPerRace);
expect(vm.leagues[0]?.id).toBe(baseLeague.id);
expect(vm.leagues[0]?.name).toBe(baseLeague.name);
expect(vm.leagues[0]?.avgViewsPerRace).toBe(baseLeague.avgViewsPerRace);
});
it('exposes formatted average views and CPM for main sponsor slot', () => {

View File

@@ -90,7 +90,7 @@ export interface DriverProfileExtendedProfileViewModel {
openToRequests: boolean;
}
export interface DriverProfileViewModel {
export interface DriverProfileViewModelData {
currentDriver: DriverProfileDriverSummaryViewModel | null;
stats: DriverProfileStatsViewModel | null;
finishDistribution: DriverProfileFinishDistributionViewModel | null;
@@ -106,7 +106,7 @@ export interface DriverProfileViewModel {
* Transforms API DTOs into UI-ready data structures.
*/
export class DriverProfileViewModel {
constructor(private readonly dto: DriverProfileViewModel) {}
constructor(private readonly dto: DriverProfileViewModelData) {}
get currentDriver(): DriverProfileDriverSummaryViewModel | null {
return this.dto.currentDriver;
@@ -135,7 +135,7 @@ export class DriverProfileViewModel {
/**
* Get the raw DTO for serialization or further processing
*/
toDTO(): DriverProfileViewModel {
toDTO(): DriverProfileViewModelData {
return this.dto;
}
}

View File

@@ -43,11 +43,11 @@ export class LeagueDetailPageViewModel {
settings: {
maxDrivers?: number;
};
socialLinks?: {
socialLinks: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
} | undefined;
// Owner info
owner: GetDriverOutputDTO | null;
@@ -103,13 +103,13 @@ export class LeagueDetailPageViewModel {
) {
this.id = league.id;
this.name = league.name;
this.description = league.description;
this.description = league.description ?? '';
this.ownerId = league.ownerId;
this.createdAt = league.createdAt;
this.createdAt = ''; // Not provided by API
this.settings = {
maxDrivers: league.maxDrivers,
maxDrivers: league.maxMembers,
};
this.socialLinks = league.socialLinks;
this.socialLinks = undefined;
this.owner = owner;
this.scoringConfig = scoringConfig;

View File

@@ -0,0 +1,24 @@
/**
* League Page Detail View Model
*
* View model for league page details.
*/
export class LeaguePageDetailViewModel {
id: string;
name: string;
description: string;
ownerId: string;
ownerName: string;
isAdmin: boolean;
mainSponsor: { name: string; logoUrl: string; websiteUrl: string } | null;
constructor(data: any) {
this.id = data.id;
this.name = data.name;
this.description = data.description;
this.ownerId = data.ownerId;
this.ownerName = data.ownerName;
this.isAdmin = data.isAdmin;
this.mainSponsor = data.mainSponsor;
}
}

View File

@@ -1,4 +1,4 @@
import type { LeagueScoringPresetDTO } from '@core/racing/application/ports/LeagueScoringPresetProvider';
import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';
/**
* View Model for league scoring presets

View File

@@ -1,4 +1,4 @@
import type { MembershipFeeDto } from '../types/generated';
import type { MembershipFeeDTO } from '../types/generated/MembershipFeeDTO';
export class MembershipFeeViewModel {
id: string;
@@ -10,7 +10,7 @@ export class MembershipFeeViewModel {
createdAt: Date;
updatedAt: Date;
constructor(dto: MembershipFeeDto) {
constructor(dto: MembershipFeeDTO) {
Object.assign(this, dto);
}

View File

@@ -1,4 +1,4 @@
import type { PaymentDto } from '../types/generated';
import type { PaymentDTO } from '../types/generated/PaymentDto';
export class PaymentViewModel {
id: string;

View File

@@ -13,7 +13,7 @@ export class StandingEntryViewModel {
private currentUserId: string;
private previousPosition?: number;
constructor(dto: LeagueStandingDTO & { position: number; points: number; wins?: number; podiums?: number; races?: number }, leaderPoints: number, nextPoints: number, currentUserId: string, previousPosition?: number) {
constructor(dto: LeagueStandingDTO, leaderPoints: number, nextPoints: number, currentUserId: string, previousPosition?: number) {
this.driverId = dto.driverId;
this.position = dto.position;
this.points = dto.points;