website cleanup

This commit is contained in:
2025-12-24 14:01:52 +01:00
parent a7aee42409
commit 9b683a59d3
65 changed files with 880 additions and 745 deletions

View File

@@ -1,5 +1,4 @@
import type { MembershipRole } from '@core/racing/domain/entities/MembershipRole';
type LeagueRole = MembershipRole;
type LeagueRole = 'owner' | 'admin' | 'steward' | 'member';
export interface LeagueRoleDisplayData {
text: string;

View File

@@ -18,6 +18,7 @@ import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
import { SubmitBlocker, ThrottleBlocker } from "@/lib/blockers";
import { RaceDTO } from "@/lib/types/generated/RaceDTO";
import { LeagueStatsDTO } from "@/lib/types/generated/LeagueStatsDTO";
import { LeagueScoringConfigDTO } from "@/lib/types/LeagueScoringConfigDTO";
/**

View File

@@ -1,12 +1,12 @@
import { MembershipFeeDto } from '@/lib/types/generated/MembershipFeeDto';
import type { MemberPaymentDto } from '@/lib/types/generated/MemberPaymentDto';
import { MembershipFeeDTO } from '@/lib/types/generated/MembershipFeeDTO';
import type { MemberPaymentDTO } from '@/lib/types/generated/MemberPaymentDTO';
import { MembershipFeeViewModel } from '@/lib/view-models/MembershipFeeViewModel';
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
// Response shape as returned by the membership-fees payments endpoint; mirrors the API contract until a generated type is introduced
export interface GetMembershipFeesOutputDto {
fee: MembershipFeeDto | null;
payments: MemberPaymentDto[];
fee: MembershipFeeDTO | null;
payments: MemberPaymentDTO[];
}
/**
@@ -23,7 +23,7 @@ export class MembershipFeeService {
/**
* Get membership fees by league ID with view model transformation
*/
async getMembershipFees(leagueId: string): Promise<{ fee: MembershipFeeViewModel | null; payments: MemberPaymentDto[] }> {
async getMembershipFees(leagueId: string): Promise<{ fee: MembershipFeeViewModel | null; payments: MemberPaymentDTO[] }> {
const dto: GetMembershipFeesOutputDto = await this.apiClient.getMembershipFees({ leagueId });
return {
fee: dto.fee ? new MembershipFeeViewModel(dto.fee) : null,

View File

@@ -3,8 +3,8 @@ import { PaymentViewModel } from '@/lib/view-models/PaymentViewModel';
import { PrizeViewModel } from '@/lib/view-models/PrizeViewModel';
import { WalletViewModel } from '@/lib/view-models/WalletViewModel';
import type { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
import type { PaymentDTO } from '../../types/generated/PaymentDto';
import type { PrizeDto } from '../../types/generated/PrizeDto';
import type { PaymentDTO } from '../../types/generated/PaymentDTO';
import type { PrizeDTO } from '../../types/generated/PrizeDTO';
// Local payment creation request matching the Payments API contract until a shared generated type is introduced
type CreatePaymentRequest = {
@@ -32,7 +32,7 @@ export class PaymentService {
* Get all payments with optional filters
*/
async getPayments(leagueId?: string, payerId?: string): Promise<PaymentViewModel[]> {
const query = leagueId || payerId ? { leagueId, payerId } : undefined;
const query = (leagueId || payerId) ? { ...(leagueId && { leagueId }), ...(payerId && { payerId }) } : undefined;
const dto = await this.apiClient.getPayments(query);
return dto.payments.map((payment: PaymentDTO) => new PaymentViewModel(payment));
}
@@ -62,7 +62,7 @@ export class PaymentService {
* Get membership fees for a league
*/
async getMembershipFees(leagueId: string, driverId?: string): Promise<MembershipFeeViewModel | null> {
const dto = await this.apiClient.getMembershipFees({ leagueId, driverId });
const dto = await this.apiClient.getMembershipFees({ leagueId, ...(driverId && { driverId }) });
return dto.fee ? new MembershipFeeViewModel(dto.fee) : null;
}
@@ -70,9 +70,9 @@ export class PaymentService {
* Get prizes with optional filters
*/
async getPrizes(leagueId?: string, seasonId?: string): Promise<PrizeViewModel[]> {
const query = leagueId || seasonId ? { leagueId, seasonId } : undefined;
const query = (leagueId || seasonId) ? { ...(leagueId && { leagueId }), ...(seasonId && { seasonId }) } : undefined;
const dto = await this.apiClient.getPrizes(query);
return dto.prizes.map((prize: PrizeDto) => new PrizeViewModel(prize));
return dto.prizes.map((prize: PrizeDTO) => new PrizeViewModel(prize));
}
/**

View File

@@ -22,7 +22,7 @@ export class RaceService {
driverId: string
): Promise<RaceDetailViewModel> {
const dto = await this.apiClient.getDetail(raceId, driverId);
return new RaceDetailViewModel(dto);
return new RaceDetailViewModel(dto, driverId);
}
/**

View File

@@ -45,6 +45,7 @@ export const siteConfig = {
// Note: All prices displayed are exclusive of VAT
euReverseChargeApplies: true,
nonEuVatExempt: true,
standardRate: 20,
notice: 'All prices shown are exclusive of VAT. Applicable taxes will be calculated at checkout.',
euBusinessNotice: 'EU businesses with a valid VAT ID may apply reverse charge.',
nonEuNotice: 'Non-EU businesses are not charged VAT.',

View File

@@ -1,5 +1,20 @@
export interface LeagueScoringChampionshipDTO {
id: string;
name: string;
type: string;
sessionTypes: string[];
pointsPreview: Array<{ sessionType: string; position: number; points: number }>;
bonusSummary: string[];
dropPolicyDescription: string;
}
export interface LeagueScoringConfigDTO {
patternId: string;
customScoringEnabled: boolean;
points: Record<string, number>;
leagueId: string;
seasonId: string;
gameId: string;
gameName: string;
scoringPresetId?: string;
scoringPresetName?: string;
dropPolicySummary: string;
championships: LeagueScoringChampionshipDTO[];
}

View File

@@ -0,0 +1,13 @@
/**
* 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 LeagueScoringChampionshipDTO {
id: string;
name: string;
type: string;
sessionTypes: string[];
pointsPreview: string;
}

View File

@@ -0,0 +1,12 @@
/**
* 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 LeagueScoringConfigDTO {
leagueId: string;
seasonId: string;
gameId: string;
gameName: string;
}

View File

@@ -4,14 +4,6 @@
* 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

@@ -0,0 +1,9 @@
/**
* 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 MembershipRoleDTO {
value: 'owner' | 'admin' | 'steward' | 'member';
}

View File

@@ -0,0 +1,9 @@
/**
* 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

@@ -7,6 +7,7 @@ import { GetDriverOutputDTO } from '../types/generated/GetDriverOutputDTO';
import { RaceDTO } from '../types/generated/RaceDTO';
import { LeagueScoringConfigDTO } from '../types/LeagueScoringConfigDTO';
import { RaceViewModel } from './RaceViewModel';
import { DriverViewModel } from './DriverViewModel';
// Sponsor info type
export interface SponsorInfo {
@@ -20,7 +21,7 @@ export interface SponsorInfo {
// Driver summary for management section
export interface DriverSummary {
driver: GetDriverOutputDTO;
driver: DriverViewModel;
rating: number | null;
rank: number | null;
}
@@ -117,7 +118,7 @@ export class LeagueDetailPageViewModel {
this.memberships = memberships.memberships.map(m => ({
driverId: m.driverId,
role: m.role,
status: m.status,
status: 'active',
joinedAt: m.joinedAt,
}));
@@ -164,8 +165,14 @@ export class LeagueDetailPageViewModel {
}
private buildDriverSummary(driverId: string): DriverSummary | null {
const driver = this.drivers.find(d => d.id === driverId);
if (!driver) return null;
const driverDto = this.drivers.find(d => d.id === driverId);
if (!driverDto) return null;
const driver = new DriverViewModel({
id: driverDto.id,
name: driverDto.name,
iracingId: driverDto.iracingId,
});
// Detailed rating and rank data are not wired from the analytics services yet;
// expose the driver identity only so the UI can still render role assignments.

View File

@@ -1,5 +1,5 @@
import type { LeagueConfigFormModel } from '@core/racing/application';
import type { LeagueScoringPresetDTO } from '@core/racing/application/ports/LeagueScoringPresetProvider';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';
import { LeagueScoringPresetsViewModel } from './LeagueScoringPresetsViewModel';
import { DriverSummaryViewModel } from './DriverSummaryViewModel';
@@ -23,6 +23,7 @@ export class LeagueSettingsViewModel {
id: string;
name: string;
ownerId: string;
createdAt: string;
};
config: LeagueConfigFormModel;
presets: LeagueScoringPresetDTO[];

View File

@@ -1,4 +1,4 @@
import type { PaymentDTO } from '../types/generated/PaymentDto';
import type { PaymentDTO } from '../types/generated/PaymentDTO';
export class PaymentViewModel {
id: string;
@@ -14,7 +14,7 @@ export class PaymentViewModel {
createdAt: Date;
completedAt?: Date;
constructor(dto: PaymentDto) {
constructor(dto: PaymentDTO) {
Object.assign(this, dto);
}

View File

@@ -0,0 +1,19 @@
import { RaceDetailEntryDTO } from '../types/generated/RaceDetailEntryDTO';
export class RaceDetailEntryViewModel {
id: string;
name: string;
country: string;
avatarUrl: string;
isCurrentUser: boolean;
rating: number | null;
constructor(dto: RaceDetailEntryDTO, currentDriverId: string, rating?: number) {
this.id = dto.id;
this.name = dto.name;
this.country = dto.country;
this.avatarUrl = dto.avatarUrl;
this.isCurrentUser = dto.id === currentDriverId;
this.rating = rating ?? null;
}
}

View File

@@ -0,0 +1,23 @@
import { RaceDetailUserResultDTO } from '../types/generated/RaceDetailUserResultDTO';
export class RaceDetailUserResultViewModel {
position: number;
startPosition: number;
incidents: number;
fastestLap: number;
positionChange: number;
ratingChange: number;
isPodium: boolean;
isClean: boolean;
constructor(dto: RaceDetailUserResultDTO) {
this.position = dto.position;
this.startPosition = dto.startPosition;
this.incidents = dto.incidents;
this.fastestLap = dto.fastestLap;
this.positionChange = dto.positionChange;
this.ratingChange = dto.ratingChange;
this.isPodium = dto.isPodium;
this.isClean = dto.isClean;
}
}

View File

@@ -41,11 +41,11 @@ describe('RaceDetailViewModel', () => {
entryList: entries,
registration,
userResult,
});
}, 'current-driver');
expect(viewModel.race).toBe(race);
expect(viewModel.league).toBe(league);
expect(viewModel.entryList).toBe(entries);
expect(viewModel.entryList).toHaveLength(0);
expect(viewModel.registration).toBe(registration);
expect(viewModel.userResult).toBe(userResult);
});

View File

@@ -2,14 +2,16 @@ import { RaceDetailLeagueDTO } from '../types/generated/RaceDetailLeagueDTO';
import { RaceDetailRaceDTO } from '../types/generated/RaceDetailRaceDTO';
import { RaceDetailRegistrationDTO } from '../types/generated/RaceDetailRegistrationDTO';
import { RaceDetailUserResultDTO } from '../types/generated/RaceDetailUserResultDTO';
import { RaceDetailEntryDTO } from '../types/RaceDetailEntryDTO';
import { RaceDetailEntryDTO } from '../types/generated/RaceDetailEntryDTO';
import { RaceDetailEntryViewModel } from './RaceDetailEntryViewModel';
import { RaceDetailUserResultViewModel } from './RaceDetailUserResultViewModel';
export class RaceDetailViewModel {
race: RaceDetailRaceDTO | null;
league: RaceDetailLeagueDTO | null;
entryList: RaceDetailEntryDTO[];
entryList: RaceDetailEntryViewModel[];
registration: RaceDetailRegistrationDTO;
userResult: RaceDetailUserResultDTO | null;
userResult: RaceDetailUserResultViewModel | null;
error?: string;
constructor(dto: {
@@ -19,18 +21,18 @@ export class RaceDetailViewModel {
registration: RaceDetailRegistrationDTO;
userResult: RaceDetailUserResultDTO | null;
error?: string;
}) {
}, currentDriverId: string) {
this.race = dto.race;
this.league = dto.league;
this.entryList = dto.entryList;
this.entryList = dto.entryList.map(entry => new RaceDetailEntryViewModel(entry, currentDriverId));
this.registration = dto.registration;
this.userResult = dto.userResult;
this.userResult = dto.userResult ? new RaceDetailUserResultViewModel(dto.userResult) : null;
this.error = dto.error;
}
/** UI-specific: Whether user is registered */
get isRegistered(): boolean {
return this.registration.isRegistered;
return this.registration.isUserRegistered;
}
/** UI-specific: Whether user can register */

View File

@@ -61,6 +61,11 @@ export class RaceResultViewModel {
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
}
/** Required by ResultsTable */
getPositionChange(): number {
return this.positionChange;
}
// Note: The generated DTO doesn't have id or raceId
// These will need to be added when the OpenAPI spec is updated
id: string = '';

View File

@@ -3,12 +3,11 @@ import { RaceWithSOFDTO } from '../types/generated/RaceWithSOFDTO';
export class RaceWithSOFViewModel {
id: string;
track: string;
strengthOfField: number | null;
constructor(dto: RaceWithSOFDTO) {
this.id = dto.id;
this.track = dto.track;
this.strengthOfField = (dto as any).strengthOfField ?? null;
}
// The view model currently exposes only basic race identity and track information.
// Additional strength-of-field or result details can be added here once the DTO carries them.
}

View File

@@ -21,6 +21,10 @@ export class SponsorshipDetailViewModel {
status: string = 'active';
amount: number = 0;
currency: string = 'USD';
type: string = 'league';
entityName: string = '';
price: number = 0;
impressions: number = 0;
/** UI-specific: Formatted amount */
get formattedAmount(): string {

View File

@@ -3,7 +3,7 @@ import type { TeamMemberDTO } from '@/lib/types/generated/GetTeamMembersOutputDT
export class TeamMemberViewModel {
driverId: string;
driverName: string;
role: 'owner' | 'manager' | 'member';
role: 'owner' | 'admin' | 'member';
joinedAt: string;
isActive: boolean;
avatarUrl: string;
@@ -26,7 +26,7 @@ export class TeamMemberViewModel {
get roleBadgeVariant(): string {
switch (this.role) {
case 'owner': return 'primary';
case 'manager': return 'secondary';
case 'admin': return 'secondary';
case 'member': return 'default';
default: return 'default';
}

View File

@@ -1,4 +1,4 @@
import { WalletDto } from '../types/generated/WalletDto';
import { WalletDTO } from '../types/generated/WalletDTO';
import { FullTransactionDto, WalletTransactionViewModel } from './WalletTransactionViewModel';
export class WalletViewModel {
@@ -11,7 +11,7 @@ export class WalletViewModel {
createdAt: string;
currency: string;
constructor(dto: WalletDto & { transactions?: FullTransactionDto[] }) {
constructor(dto: WalletDTO & { transactions?: any[] }) {
this.id = dto.id;
this.leagueId = dto.leagueId;
this.balance = dto.balance;