website cleanup

This commit is contained in:
2025-12-25 00:19:36 +01:00
parent d78854a4c6
commit 9486455b9e
82 changed files with 1223 additions and 363 deletions

View File

@@ -0,0 +1,18 @@
import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
/**
* AvatarGenerationViewModel
*
* View model for avatar generation process
*/
export class AvatarGenerationViewModel {
readonly success: boolean;
readonly avatarUrls: string[];
readonly errorMessage?: string;
constructor(dto: RequestAvatarGenerationOutputDTO) {
this.success = dto.success;
this.avatarUrls = dto.avatarUrls || [];
this.errorMessage = dto.errorMessage;
}
}

View File

@@ -7,13 +7,21 @@ import { CompleteOnboardingOutputDTO } from '../types/generated/CompleteOnboardi
export class CompleteOnboardingViewModel {
success: boolean;
driverId?: string;
errorMessage?: string;
constructor(dto: CompleteOnboardingOutputDTO) {
this.success = dto.success;
if (dto.driverId !== undefined) this.driverId = dto.driverId;
if (dto.errorMessage !== undefined) this.errorMessage = dto.errorMessage;
}
/** UI-specific: Whether onboarding was successful */
get isSuccessful(): boolean {
return this.success;
}
/** UI-specific: Whether there was an error */
get hasError(): boolean {
return !!this.errorMessage;
}
}

View File

@@ -1,9 +1,9 @@
import { DriverRegistrationStatusDTO } from '../types/generated/DriverRegistrationStatusDTO';
export class DriverRegistrationStatusViewModel {
isRegistered: boolean;
raceId: string;
driverId: string;
isRegistered!: boolean;
raceId!: string;
driverId!: string;
constructor(dto: DriverRegistrationStatusDTO) {
Object.assign(this, dto);

View File

@@ -10,6 +10,9 @@ export class DriverViewModel {
avatarUrl?: string;
iracingId?: string;
rating?: number;
country?: string;
bio?: string;
joinedAt?: string;
constructor(dto: {
id: string;
@@ -17,12 +20,18 @@ export class DriverViewModel {
avatarUrl?: string;
iracingId?: string;
rating?: number;
country?: string;
bio?: string;
joinedAt?: string;
}) {
this.id = dto.id;
this.name = dto.name;
if (dto.avatarUrl !== undefined) this.avatarUrl = dto.avatarUrl;
if (dto.iracingId !== undefined) this.iracingId = dto.iracingId;
if (dto.rating !== undefined) this.rating = dto.rating;
if (dto.country !== undefined) this.country = dto.country;
if (dto.bio !== undefined) this.bio = dto.bio;
if (dto.joinedAt !== undefined) this.joinedAt = dto.joinedAt;
}
/** UI-specific: Whether driver has an iRacing ID */

View File

@@ -0,0 +1,16 @@
/**
* EmailSignupViewModel
*
* View model for email signup responses
*/
export class EmailSignupViewModel {
readonly email: string;
readonly message: string;
readonly status: 'success' | 'error' | 'info';
constructor(email: string, message: string, status: 'success' | 'error' | 'info') {
this.email = email;
this.message = message;
this.status = status;
}
}

View File

@@ -106,18 +106,22 @@ export class LeagueDetailPageViewModel {
this.name = league.name;
this.description = league.description ?? '';
this.ownerId = league.ownerId;
this.createdAt = ''; // Not provided by API
this.createdAt = league.createdAt;
this.settings = {
maxDrivers: league.maxMembers,
maxDrivers: league.settings?.maxDrivers,
};
this.socialLinks = {
discordUrl: league.discordUrl,
youtubeUrl: league.youtubeUrl,
websiteUrl: league.websiteUrl,
};
this.socialLinks = undefined;
this.owner = owner;
this.scoringConfig = scoringConfig;
this.drivers = drivers;
this.memberships = memberships.memberships.map(m => ({
this.memberships = memberships.members.map(m => ({
driverId: m.driverId,
role: m.role,
role: m.role as 'owner' | 'admin' | 'steward' | 'member',
status: 'active',
joinedAt: m.joinedAt,
}));
@@ -125,8 +129,9 @@ export class LeagueDetailPageViewModel {
this.allRaces = allRaces;
this.runningRaces = allRaces.filter(r => r.status === 'running');
this.averageSOF = leagueStats.averageSOF ?? null;
this.completedRacesCount = leagueStats.completedRaces ?? 0;
// Calculate SOF from available data
this.averageSOF = leagueStats.averageRating ?? null;
this.completedRacesCount = leagueStats.totalRaces ?? 0;
this.sponsors = sponsors;

View File

@@ -0,0 +1,26 @@
import { LeagueScoringChampionshipDTO } from '@/lib/types/generated/LeagueScoringChampionshipDTO';
/**
* LeagueScoringChampionshipViewModel
*
* View model for league scoring championship
*/
export class LeagueScoringChampionshipViewModel {
readonly id: string;
readonly name: string;
readonly type: string;
readonly sessionTypes: string[];
readonly pointsPreview: Array<{ sessionType: string; position: number; points: number }>;
readonly bonusSummary: string[];
readonly dropPolicyDescription?: string;
constructor(dto: LeagueScoringChampionshipDTO) {
this.id = dto.id;
this.name = dto.name;
this.type = dto.type;
this.sessionTypes = dto.sessionTypes;
this.pointsPreview = (dto.pointsPreview as any) || [];
this.bonusSummary = (dto as any).bonusSummary || [];
this.dropPolicyDescription = (dto as any).dropPolicyDescription;
}
}

View File

@@ -0,0 +1,29 @@
import { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
/**
* LeagueScoringConfigViewModel
*
* View model for league scoring configuration
*/
export class LeagueScoringConfigViewModel {
readonly gameName: string;
readonly scoringPresetName?: string;
readonly dropPolicySummary?: string;
readonly championships?: Array<{
id: string;
name: string;
type: 'driver' | 'team' | 'nations' | 'trophy' | string;
sessionTypes: string[];
pointsPreview: Array<{ sessionType: string; position: number; points: number }>;
bonusSummary: string[];
dropPolicyDescription?: string;
}>;
constructor(dto: LeagueScoringConfigDTO) {
this.gameName = dto.gameName;
// These would be mapped from extended properties if available
this.scoringPresetName = (dto as any).scoringPresetName;
this.dropPolicySummary = (dto as any).dropPolicySummary;
this.championships = (dto as any).championships;
}
}

View File

@@ -0,0 +1,20 @@
import { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';
/**
* LeagueScoringPresetViewModel
*
* View model for league scoring preset configuration
*/
export class LeagueScoringPresetViewModel {
readonly id: string;
readonly name: string;
readonly sessionSummary: string;
readonly bonusSummary?: string;
constructor(dto: LeagueScoringPresetDTO) {
this.id = dto.id;
this.name = dto.name;
this.sessionSummary = dto.sessionSummary;
this.bonusSummary = dto.bonusSummary;
}
}

View File

@@ -1,4 +1,4 @@
import type { GetMediaOutputDTO } from '../types/generated';
import type { GetMediaOutputDTO } from '../types/generated/GetMediaOutputDTO';
/**
* Media View Model
@@ -30,4 +30,4 @@ export class MediaViewModel {
const mb = kb / 1024;
return `${mb.toFixed(2)} MB`;
}
}
}

View File

@@ -1,14 +1,14 @@
import type { MembershipFeeDTO } from '../types/generated/MembershipFeeDTO';
export class MembershipFeeViewModel {
id: string;
leagueId: string;
id!: string;
leagueId!: string;
seasonId?: string;
type: string;
amount: number;
enabled: boolean;
createdAt: Date;
updatedAt: Date;
type!: string;
amount!: number;
enabled!: boolean;
createdAt!: Date;
updatedAt!: Date;
constructor(dto: MembershipFeeDTO) {
Object.assign(this, dto);

View File

@@ -1,17 +1,17 @@
import type { PaymentDTO } from '../types/generated/PaymentDTO';
export class PaymentViewModel {
id: string;
type: string;
amount: number;
platformFee: number;
netAmount: number;
payerId: string;
payerType: string;
leagueId: string;
id!: string;
type!: string;
amount!: number;
platformFee!: number;
netAmount!: number;
payerId!: string;
payerType!: string;
leagueId!: string;
seasonId?: string;
status: string;
createdAt: Date;
status!: string;
createdAt!: Date;
completedAt?: Date;
constructor(dto: PaymentDTO) {

View File

@@ -1,21 +1,32 @@
import type { PrizeDto } from '../types/generated';
import type { PrizeDTO } from '../types/generated/PrizeDTO';
export class PrizeViewModel {
id: string;
leagueId: string;
seasonId: string;
position: number;
name: string;
amount: number;
type: string;
id!: string;
leagueId!: string;
seasonId!: string;
position!: number;
name!: string;
amount!: number;
type!: string;
description?: string;
awarded: boolean;
awarded!: boolean;
awardedTo?: string;
awardedAt?: Date;
createdAt: Date;
createdAt!: Date;
constructor(dto: PrizeDto) {
Object.assign(this, dto);
constructor(dto: PrizeDTO) {
this.id = dto.id;
this.leagueId = dto.leagueId;
this.seasonId = dto.seasonId;
this.position = dto.position;
this.name = dto.name;
this.amount = dto.amount;
this.type = dto.type;
this.description = dto.description;
this.awarded = dto.awarded;
this.awardedTo = dto.awardedTo;
this.awardedAt = dto.awardedAt ? new Date(dto.awardedAt) : undefined;
this.createdAt = new Date(dto.createdAt);
}
/** UI-specific: Formatted amount */
@@ -67,4 +78,4 @@ export class PrizeViewModel {
get formattedCreatedAt(): string {
return this.createdAt.toLocaleString();
}
}
}

View File

@@ -1,4 +1,4 @@
import { DriverSummaryDTO } from '../types/generated/LeagueAdminProtestsDTO';
import { DriverSummaryDTO } from '../types/generated/DriverSummaryDTO';
export class ProtestDriverViewModel {
constructor(private readonly dto: DriverSummaryDTO) {}

View File

@@ -1,4 +1,5 @@
import { ProtestDTO } from '../types/generated/ProtestDTO';
import { RaceProtestDTO } from '../types/generated/RaceProtestDTO';
/**
* Protest view model
@@ -11,22 +12,49 @@ export class ProtestViewModel {
accusedDriverId: string;
description: string;
submittedAt: string;
filedAt?: string;
status: string;
reviewedAt?: string;
decisionNotes?: string;
incident?: { lap?: number } | null;
incident?: { lap?: number; description?: string } | null;
proofVideoUrl?: string | null;
comment?: string | null;
constructor(dto: ProtestDTO) {
constructor(dto: ProtestDTO | RaceProtestDTO) {
this.id = dto.id;
this.raceId = dto.raceId;
this.raceId = (dto as any).raceId || '';
this.protestingDriverId = dto.protestingDriverId;
this.accusedDriverId = dto.accusedDriverId;
this.description = dto.description;
this.submittedAt = dto.submittedAt;
this.description = (dto as any).description || dto.description;
this.submittedAt = (dto as any).submittedAt || (dto as any).filedAt || '';
this.filedAt = (dto as any).filedAt || (dto as any).submittedAt;
// Handle different DTO structures
if ('status' in dto) {
this.status = dto.status;
} else {
this.status = 'pending';
}
// Handle incident data
if ('incident' in dto && dto.incident) {
this.incident = {
lap: (dto.incident as any).lap,
description: (dto.incident as any).description
};
} else if ('lap' in dto || 'description' in dto) {
this.incident = {
lap: (dto as any).lap,
description: (dto as any).description
};
} else {
this.incident = null;
}
// Status and decision metadata are not part of the protest DTO in this build; they default to a pending, unreviewed protest
this.status = 'pending';
if (!('status' in dto)) {
this.status = 'pending';
}
this.reviewedAt = undefined;
this.decisionNotes = undefined;
}

View File

@@ -1,14 +1,14 @@
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;
position!: number;
startPosition!: number;
incidents!: number;
fastestLap!: number;
positionChange!: number;
isPodium!: boolean;
isClean!: boolean;
ratingChange!: number;
constructor(dto: RaceDetailUserResultDTO) {
this.position = dto.position;
@@ -16,8 +16,49 @@ export class RaceDetailUserResultViewModel {
this.incidents = dto.incidents;
this.fastestLap = dto.fastestLap;
this.positionChange = dto.positionChange;
this.ratingChange = dto.ratingChange;
this.isPodium = dto.isPodium;
this.isClean = dto.isClean;
this.ratingChange = dto.ratingChange ?? 0;
}
/** UI-specific: Display for position change */
get positionChangeDisplay(): string {
if (this.positionChange > 0) return `+${this.positionChange}`;
if (this.positionChange < 0) return `${this.positionChange}`;
return '0';
}
/** UI-specific: Color for position change */
get positionChangeColor(): string {
if (this.positionChange > 0) return 'green';
if (this.positionChange < 0) return 'red';
return 'gray';
}
/** UI-specific: Whether this is the winner */
get isWinner(): boolean {
return this.position === 1;
}
/** UI-specific: Rating change display */
get ratingChangeDisplay(): string {
if (this.ratingChange > 0) return `+${this.ratingChange}`;
return `${this.ratingChange}`;
}
/** UI-specific: Rating change color */
get ratingChangeColor(): string {
if (this.ratingChange > 0) return 'green';
if (this.ratingChange < 0) return 'red';
return 'gray';
}
/** UI-specific: Formatted lap time */
get lapTimeFormatted(): string {
if (this.fastestLap <= 0) return '--:--.---';
const minutes = Math.floor(this.fastestLap / 60);
const seconds = Math.floor(this.fastestLap % 60);
const milliseconds = Math.floor((this.fastestLap % 1) * 1000);
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
}
}

View File

@@ -1,16 +1,16 @@
import { RaceResultDTO } from '../types/generated/RaceResultDTO';
export class RaceResultViewModel {
driverId: string;
driverName: string;
avatarUrl: string;
position: number;
startPosition: number;
incidents: number;
fastestLap: number;
positionChange: number;
isPodium: boolean;
isClean: boolean;
driverId!: string;
driverName!: string;
avatarUrl!: string;
position!: number;
startPosition!: number;
incidents!: number;
fastestLap!: number;
positionChange!: number;
isPodium!: boolean;
isClean!: boolean;
constructor(dto: RaceResultDTO) {
Object.assign(this, dto);

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { RaceStatsViewModel } from './RaceStatsViewModel';
import type { RaceStatsDTO } from '../types/generated';
import type { RaceStatsDTO } from '../types/generated/RaceStatsDTO';
const createDto = (overrides: Partial<RaceStatsDTO> = {}): RaceStatsDTO => ({
totalRaces: 1234,

View File

@@ -1,4 +1,4 @@
import { RaceStatsDTO } from '../types/generated';
import type { RaceStatsDTO } from '../types/generated/RaceStatsDTO';
/**
* Race stats view model
@@ -15,4 +15,4 @@ export class RaceStatsViewModel {
get formattedTotalRaces(): string {
return this.totalRaces.toLocaleString();
}
}
}

View File

@@ -1,22 +1,45 @@
import { RaceDTO } from '../types/generated/RaceDTO';
import { RacesPageDataRaceDTO } from '../types/generated/RacesPageDataRaceDTO';
export class RaceViewModel {
constructor(private readonly dto: RaceDTO, private readonly _status?: string, private readonly _registeredCount?: number, private readonly _strengthOfField?: number) {}
constructor(
private readonly dto: RaceDTO | RacesPageDataRaceDTO,
private readonly _status?: string,
private readonly _registeredCount?: number,
private readonly _strengthOfField?: number
) {}
get id(): string {
return this.dto.id;
}
get name(): string {
return this.dto.name;
if ('name' in this.dto) {
return this.dto.name;
}
return '';
}
get date(): string {
return this.dto.date;
if ('date' in this.dto) {
return this.dto.date;
}
if ('scheduledAt' in this.dto) {
return this.dto.scheduledAt;
}
return '';
}
get track(): string {
return (this.dto as any).track || '';
}
get car(): string {
return (this.dto as any).car || '';
}
get status(): string | undefined {
return this._status;
return this._status || (this.dto as any).status;
}
get registeredCount(): number | undefined {
@@ -24,7 +47,7 @@ export class RaceViewModel {
}
get strengthOfField(): number | undefined {
return this._strengthOfField;
return this._strengthOfField || (this.dto as any).strengthOfField;
}
/** UI-specific: Formatted date */

View File

@@ -1,4 +1,4 @@
import { RecordEngagementOutputDTO } from '../types/generated';
import type { RecordEngagementOutputDTO } from '../types/generated/RecordEngagementOutputDTO';
/**
* Record engagement output view model
@@ -27,4 +27,4 @@ export class RecordEngagementOutputViewModel {
get isHighEngagement(): boolean {
return this.engagementWeight > 1.0;
}
}
}

View File

@@ -1,4 +1,4 @@
import { RecordPageViewOutputDTO } from '../types/generated';
import type { RecordPageViewOutputDTO } from '../types/generated/RecordPageViewOutputDTO';
/**
* Record page view output view model
@@ -15,4 +15,4 @@ export class RecordPageViewOutputViewModel {
get displayPageViewId(): string {
return `Page View: ${this.pageViewId}`;
}
}
}

View File

@@ -1,9 +1,4 @@
// Note: No generated DTO available for RequestAvatarGeneration yet
interface RequestAvatarGenerationDTO {
success: boolean;
avatarUrl?: string;
error?: string;
}
import { RequestAvatarGenerationOutputDTO } from '../types/generated/RequestAvatarGenerationOutputDTO';
/**
* Request Avatar Generation View Model
@@ -12,13 +7,15 @@ interface RequestAvatarGenerationDTO {
*/
export class RequestAvatarGenerationViewModel {
success: boolean;
avatarUrl?: string;
error?: string;
requestId?: string;
avatarUrls?: string[];
errorMessage?: string;
constructor(dto: RequestAvatarGenerationDTO) {
constructor(dto: RequestAvatarGenerationOutputDTO) {
this.success = dto.success;
if (dto.avatarUrl !== undefined) this.avatarUrl = dto.avatarUrl;
if (dto.error !== undefined) this.error = dto.error;
if (dto.requestId !== undefined) this.requestId = dto.requestId;
if (dto.avatarUrls !== undefined) this.avatarUrls = dto.avatarUrls;
if (dto.errorMessage !== undefined) this.errorMessage = dto.errorMessage;
}
/** UI-specific: Whether generation was successful */
@@ -28,6 +25,11 @@ export class RequestAvatarGenerationViewModel {
/** UI-specific: Whether there was an error */
get hasError(): boolean {
return !!this.error;
return !!this.errorMessage;
}
/** UI-specific: Get first avatar URL */
get firstAvatarUrl(): string | undefined {
return this.avatarUrls?.[0];
}
}

View File

@@ -26,15 +26,18 @@ export class SponsorDashboardViewModel {
this.sponsorId = dto.sponsorId;
this.sponsorName = dto.sponsorName;
this.metrics = dto.metrics;
// Cast sponsorships to proper type
const sponsorships = dto.sponsorships as any;
this.sponsorships = {
leagues: (dto.sponsorships?.leagues || []).map(s => new SponsorshipViewModel(s)),
teams: (dto.sponsorships?.teams || []).map(s => new SponsorshipViewModel(s)),
drivers: (dto.sponsorships?.drivers || []).map(s => new SponsorshipViewModel(s)),
races: (dto.sponsorships?.races || []).map(s => new SponsorshipViewModel(s)),
platform: (dto.sponsorships?.platform || []).map(s => new SponsorshipViewModel(s)),
leagues: (sponsorships?.leagues || []).map((s: any) => new SponsorshipViewModel(s)),
teams: (sponsorships?.teams || []).map((s: any) => new SponsorshipViewModel(s)),
drivers: (sponsorships?.drivers || []).map((s: any) => new SponsorshipViewModel(s)),
races: (sponsorships?.races || []).map((s: any) => new SponsorshipViewModel(s)),
platform: (sponsorships?.platform || []).map((s: any) => new SponsorshipViewModel(s)),
};
this.recentActivity = (dto.recentActivity || []).map(a => new ActivityItemViewModel(a));
this.upcomingRenewals = (dto.upcomingRenewals || []).map(r => new RenewalAlertViewModel(r));
this.recentActivity = (dto.recentActivity || []).map((a: any) => new ActivityItemViewModel(a));
this.upcomingRenewals = (dto.upcomingRenewals || []).map((r: any) => new RenewalAlertViewModel(r));
}
get totalSponsorships(): number {

View File

@@ -1,12 +1,12 @@
import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO';
export class TeamDetailsViewModel {
id: string;
name: string;
tag: string;
id!: string;
name!: string;
tag!: string;
description?: string;
ownerId: string;
leagues: string[];
ownerId!: string;
leagues!: string[];
createdAt?: string;
specialization?: string;
region?: string;
@@ -23,10 +23,15 @@ export class TeamDetailsViewModel {
this.ownerId = dto.team.ownerId;
this.leagues = dto.team.leagues;
this.createdAt = dto.team.createdAt;
this.specialization = dto.team.specialization;
this.region = dto.team.region;
this.languages = dto.team.languages;
this.membership = dto.membership;
// These properties don't exist in the current TeamDTO but may be added later
this.specialization = undefined;
this.region = undefined;
this.languages = undefined;
this.membership = dto.membership ? {
role: dto.membership.role,
joinedAt: dto.membership.joinedAt,
isActive: dto.membership.isActive
} : null;
this._canManage = dto.canManage;
this.currentUserId = currentUserId;
}

View File

@@ -1,3 +1,16 @@
// Export the DTO type that WalletTransactionViewModel expects
export type FullTransactionDto = {
id: string;
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
description: string;
amount: number;
fee: number;
netAmount: number;
date: Date;
status: 'completed' | 'pending' | 'failed';
reference?: string;
};
export class WalletTransactionViewModel {
id: string;
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
@@ -9,17 +22,7 @@ export class WalletTransactionViewModel {
status: 'completed' | 'pending' | 'failed';
reference?: string;
constructor(dto: {
id: string;
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
description: string;
amount: number;
fee: number;
netAmount: number;
date: Date;
status: 'completed' | 'pending' | 'failed';
reference?: string;
}) {
constructor(dto: FullTransactionDto) {
this.id = dto.id;
this.type = dto.type;
this.description = dto.description;