website refactor

This commit is contained in:
2026-01-12 01:01:49 +01:00
parent 5ca6023a5a
commit fefd8d1cd6
294 changed files with 4628 additions and 4991 deletions

View File

@@ -1,4 +1,4 @@
import { CompleteOnboardingOutputDTO } from '../types/generated/CompleteOnboardingOutputDTO';
import { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
/**
* Complete onboarding view model

View File

@@ -1,4 +1,4 @@
import { CreateLeagueOutputDTO } from '../types/generated/CreateLeagueOutputDTO';
import { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO';
/**
* View Model for Create League Result

View File

@@ -1,4 +1,4 @@
import type { DriverLeaderboardItemDTO } from '../types/generated/DriverLeaderboardItemDTO';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
export class DriverLeaderboardItemViewModel {
id: string;

View File

@@ -1,4 +1,4 @@
import { DriverLeaderboardItemDTO } from '../types/generated/DriverLeaderboardItemDTO';
import { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel';
export class DriverLeaderboardViewModel {

View File

@@ -1,4 +1,4 @@
import { DriverRegistrationStatusDTO } from '../types/generated/DriverRegistrationStatusDTO';
import { DriverRegistrationStatusDTO } from '@/lib/types/generated/DriverRegistrationStatusDTO';
export class DriverRegistrationStatusViewModel {
isRegistered!: boolean;

View File

@@ -1,4 +1,4 @@
import type { GetDriverOutputDTO } from '../types/generated/GetDriverOutputDTO';
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
/**
* View Model for driver summary with rating and rank

View File

@@ -1,10 +1,7 @@
import { LeagueWithCapacityAndScoringDTO } from '../types/generated/LeagueWithCapacityAndScoringDTO';
import { LeagueStatsDTO } from '../types/generated/LeagueStatsDTO';
import { LeagueMembershipsDTO } from '../types/generated/LeagueMembershipsDTO';
import { LeagueScheduleDTO } from '../types/generated/LeagueScheduleDTO';
import { LeagueStandingsDTO } from '../types/generated/LeagueStandingsDTO';
import { GetDriverOutputDTO } from '../types/generated/GetDriverOutputDTO';
import { RaceDTO } from '../types/generated/RaceDTO';
import { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
import { LeagueStatsDTO } from '@/lib/types/generated/LeagueStatsDTO';
import { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
import { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
import { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
import { RaceViewModel } from './RaceViewModel';
import { DriverViewModel } from './DriverViewModel';
@@ -34,6 +31,29 @@ export interface LeagueMembershipWithRole {
joinedAt: string;
}
// Helper interfaces for type narrowing
interface LeagueSettings {
maxDrivers?: number;
}
interface SocialLinks {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
}
interface LeagueStatsExtended {
averageSOF?: number;
averageRating?: number;
completedRaces?: number;
totalRaces?: number;
}
interface MembershipsContainer {
members?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>;
memberships?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>;
}
export class LeagueDetailPageViewModel {
// League basic info
id: string;
@@ -107,25 +127,35 @@ export class LeagueDetailPageViewModel {
this.description = league.description ?? '';
this.ownerId = league.ownerId;
this.createdAt = league.createdAt;
// Handle settings with proper type narrowing
const settings = league.settings as LeagueSettings | undefined;
const maxDrivers = settings?.maxDrivers;
this.settings = {
maxDrivers: league.settings?.maxDrivers ?? (league as any).maxDrivers,
maxDrivers: maxDrivers,
};
// Handle social links with proper type narrowing
const socialLinks = league.socialLinks as SocialLinks | undefined;
const discordUrl = socialLinks?.discordUrl;
const youtubeUrl = socialLinks?.youtubeUrl;
const websiteUrl = socialLinks?.websiteUrl;
this.socialLinks = {
discordUrl: league.socialLinks?.discordUrl ?? (league as any).socialLinks?.discordUrl,
youtubeUrl: league.socialLinks?.youtubeUrl ?? (league as any).socialLinks?.youtubeUrl,
websiteUrl: league.socialLinks?.websiteUrl ?? (league as any).socialLinks?.websiteUrl,
discordUrl,
youtubeUrl,
websiteUrl,
};
this.owner = owner;
this.scoringConfig = scoringConfig;
this.drivers = drivers;
const membershipDtos = ((memberships as any).members ?? (memberships as any).memberships ?? []) as Array<{
driverId: string;
role: string;
status?: 'active' | 'inactive';
joinedAt: string;
}>;
// Handle memberships with proper type narrowing
const membershipsContainer = memberships as MembershipsContainer;
const membershipDtos = membershipsContainer.members ??
membershipsContainer.memberships ??
[];
this.memberships = membershipDtos.map((m) => ({
driverId: m.driverId,
@@ -137,11 +167,15 @@ export class LeagueDetailPageViewModel {
this.allRaces = allRaces;
this.runningRaces = allRaces.filter(r => r.status === 'running');
const leagueStatsAny = leagueStats as any;
// Calculate SOF from available data with proper type narrowing
const statsExtended = leagueStats as LeagueStatsExtended;
const averageSOF = statsExtended.averageSOF ??
statsExtended.averageRating ?? undefined;
const completedRaces = statsExtended.completedRaces ??
statsExtended.totalRaces ?? undefined;
// Calculate SOF from available data
this.averageSOF = leagueStatsAny.averageSOF ?? leagueStats.averageRating ?? null;
this.completedRacesCount = leagueStatsAny.completedRaces ?? leagueStats.totalRaces ?? 0;
this.averageSOF = typeof averageSOF === 'number' ? averageSOF : null;
this.completedRacesCount = typeof completedRaces === 'number' ? completedRaces : 0;
this.sponsors = sponsors;
@@ -183,10 +217,14 @@ export class LeagueDetailPageViewModel {
const driverDto = this.drivers.find(d => d.id === driverId);
if (!driverDto) return null;
// Handle avatarUrl with proper type checking
const driverAny = driverDto as { avatarUrl?: unknown };
const avatarUrl = typeof driverAny.avatarUrl === 'string' ? driverAny.avatarUrl : null;
const driver = new DriverViewModel({
id: driverDto.id,
name: driverDto.name,
avatarUrl: (driverDto as any).avatarUrl ?? null,
avatarUrl: avatarUrl,
iracingId: driverDto.iracingId,
});

View File

@@ -1,4 +1,4 @@
import type { LeagueJoinRequestDTO } from '../types/generated/LeagueJoinRequestDTO';
import type { LeagueJoinRequestDTO } from '@/lib/types/generated/LeagueJoinRequestDTO';
/**
* League join request view model

View File

@@ -1,4 +1,4 @@
import { LeagueMemberDTO } from '../types/generated/LeagueMemberDTO';
import { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
import { DriverViewModel } from './DriverViewModel';
export class LeagueMemberViewModel {

View File

@@ -1,5 +1,5 @@
import { LeagueMemberViewModel } from './LeagueMemberViewModel';
import type { LeagueMemberDTO } from '../types/generated/LeagueMemberDTO';
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
/**
* View Model for League Memberships

View File

@@ -27,8 +27,8 @@ export class LeagueScoringChampionshipViewModel {
this.name = input.name;
this.type = input.type;
this.sessionTypes = input.sessionTypes;
this.pointsPreview = (input.pointsPreview as any) || [];
this.bonusSummary = (input as any).bonusSummary || [];
this.dropPolicyDescription = (input as any).dropPolicyDescription;
this.pointsPreview = input.pointsPreview ?? [];
this.bonusSummary = input.bonusSummary ?? [];
this.dropPolicyDescription = input.dropPolicyDescription;
}
}

View File

@@ -1,4 +1,5 @@
import { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
import type { LeagueScoringChampionshipDTO } from '@/lib/types/generated/LeagueScoringChampionshipDTO';
/**
* LeagueScoringConfigViewModel
@@ -9,21 +10,12 @@ 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;
}>;
readonly championships?: LeagueScoringChampionshipDTO[];
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;
this.scoringPresetName = dto.scoringPresetName;
this.dropPolicySummary = dto.dropPolicySummary;
this.championships = dto.championships;
}
}

View File

@@ -1,7 +1,7 @@
import { LeagueStandingDTO } from '../types/generated/LeagueStandingDTO';
import { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO';
import { StandingEntryViewModel } from './StandingEntryViewModel';
import { GetDriverOutputDTO } from '../types/generated/GetDriverOutputDTO';
import { LeagueMembership } from '../types/LeagueMembership';
import { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
import { LeagueMembership } from '@/lib/types/LeagueMembership';
export class LeagueStandingsViewModel {
standings: StandingEntryViewModel[];

View File

@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { LeagueWalletViewModel } from './LeagueWalletViewModel';
import { WalletTransactionViewModel } from './WalletTransactionViewModel';
import { WalletTransactionViewModel, FullTransactionDto } from './WalletTransactionViewModel';
const createTransaction = (overrides: Partial<WalletTransactionViewModel> = {}): WalletTransactionViewModel =>
const createTransaction = (overrides: Partial<FullTransactionDto> = {}): WalletTransactionViewModel =>
new WalletTransactionViewModel({
id: 'tx-1',
type: 'sponsorship',
@@ -13,7 +13,7 @@ const createTransaction = (overrides: Partial<WalletTransactionViewModel> = {}):
date: new Date('2024-01-01T00:00:00Z'),
status: 'completed',
reference: 'ref-1',
...(overrides as any),
...overrides,
});
describe('LeagueWalletViewModel', () => {
@@ -62,10 +62,10 @@ describe('LeagueWalletViewModel', () => {
});
it('filters transactions by type and supports all', () => {
const sponsorshipTx = createTransaction({ type: 'sponsorship' as any });
const membershipTx = createTransaction({ type: 'membership' as any, id: 'tx-2' });
const withdrawalTx = createTransaction({ type: 'withdrawal' as any, id: 'tx-3' });
const prizeTx = createTransaction({ type: 'prize' as any, id: 'tx-4' });
const sponsorshipTx = createTransaction({ type: 'sponsorship' });
const membershipTx = createTransaction({ type: 'membership', id: 'tx-2' });
const withdrawalTx = createTransaction({ type: 'withdrawal', id: 'tx-3' });
const prizeTx = createTransaction({ type: 'prize', id: 'tx-4' });
const vm = new LeagueWalletViewModel({
balance: 0,

View File

@@ -1,4 +1,4 @@
import type { GetMediaOutputDTO } from '../types/generated/GetMediaOutputDTO';
import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
/**
* Media View Model

View File

@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { MembershipFeeViewModel } from './MembershipFeeViewModel';
import type { MembershipFeeDto } from '../types/generated';
import type { MembershipFeeDTO } from '@/lib/types/generated';
const createMembershipFeeDto = (overrides: Partial<MembershipFeeDto> = {}): MembershipFeeDto => ({
const createMembershipFeeDto = (overrides: Partial<MembershipFeeDTO> = {}): MembershipFeeDTO => ({
id: 'fee-1',
leagueId: 'league-1',
seasonId: 'season-1',
@@ -38,7 +38,7 @@ describe('MembershipFeeViewModel', () => {
const seasonVm = new MembershipFeeViewModel(createMembershipFeeDto({ type: 'season' }));
const monthlyVm = new MembershipFeeViewModel(createMembershipFeeDto({ type: 'monthly' }));
const perRaceVm = new MembershipFeeViewModel(createMembershipFeeDto({ type: 'per_race' }));
const otherVm = new MembershipFeeViewModel(createMembershipFeeDto({ type: 'custom' as any }));
const otherVm = new MembershipFeeViewModel(createMembershipFeeDto({ type: 'custom' }));
expect(seasonVm.typeDisplay).toBe('Per Season');
expect(monthlyVm.typeDisplay).toBe('Monthly');

View File

@@ -1,4 +1,4 @@
import type { MembershipFeeDTO } from '../types/generated/MembershipFeeDTO';
import type { MembershipFeeDTO } from '@/lib/types/generated';
export class MembershipFeeViewModel {
id!: string;

View File

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

View File

@@ -1,4 +1,4 @@
import type { PrizeDTO } from '../types/generated/PrizeDTO';
import type { PrizeDTO } from '@/lib/types/generated/PrizeDTO';
export class PrizeViewModel {
id!: string;

View File

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

View File

@@ -1,5 +1,5 @@
import { ProtestDTO } from '../types/generated/ProtestDTO';
import { RaceProtestDTO } from '../types/generated/RaceProtestDTO';
import { ProtestDTO } from '@/lib/types/generated/ProtestDTO';
import { RaceProtestDTO } from '@/lib/types/generated/RaceProtestDTO';
/**
* Protest view model
@@ -22,15 +22,41 @@ export class ProtestViewModel {
constructor(dto: ProtestDTO | RaceProtestDTO) {
this.id = dto.id;
this.raceId = (dto as any).raceId || '';
// Type narrowing for raceId
if ('raceId' in dto) {
this.raceId = dto.raceId;
} else {
this.raceId = '';
}
this.protestingDriverId = dto.protestingDriverId;
this.accusedDriverId = dto.accusedDriverId;
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;
// Type narrowing for description
if ('description' in dto && typeof dto.description === 'string') {
this.description = dto.description;
} else {
this.description = '';
}
// Type narrowing for submittedAt and filedAt
if ('submittedAt' in dto && typeof dto.submittedAt === 'string') {
this.submittedAt = dto.submittedAt;
} else if ('filedAt' in dto && typeof dto.filedAt === 'string') {
this.submittedAt = dto.filedAt;
} else {
this.submittedAt = '';
}
if ('filedAt' in dto && typeof dto.filedAt === 'string') {
this.filedAt = dto.filedAt;
} else if ('submittedAt' in dto && typeof dto.submittedAt === 'string') {
this.filedAt = dto.submittedAt;
}
// Handle different DTO structures
if ('status' in dto) {
if ('status' in dto && typeof dto.status === 'string') {
this.status = dto.status;
} else {
this.status = 'pending';
@@ -38,14 +64,16 @@ export class ProtestViewModel {
// Handle incident data
if ('incident' in dto && dto.incident) {
const incident = dto.incident as { lap?: number; description?: string };
this.incident = {
lap: (dto.incident as any).lap,
description: (dto.incident as any).description
lap: typeof incident.lap === 'number' ? incident.lap : undefined,
description: typeof incident.description === 'string' ? incident.description : undefined
};
} else if ('lap' in dto || 'description' in dto) {
} else if (('lap' in dto && typeof (dto as { lap?: number }).lap === 'number') ||
('description' in dto && typeof (dto as { description?: string }).description === 'string')) {
this.incident = {
lap: (dto as any).lap,
description: (dto as any).description
lap: 'lap' in dto ? (dto as { lap?: number }).lap : undefined,
description: 'description' in dto ? (dto as { description?: string }).description : undefined
};
} else {
this.incident = null;

View File

@@ -1,4 +1,4 @@
import { RaceDetailEntryDTO } from '../types/generated/RaceDetailEntryDTO';
import { RaceDetailEntryDTO } from '@/lib/types/generated/RaceDetailEntryDTO';
export class RaceDetailEntryViewModel {
id: string;

View File

@@ -1,4 +1,4 @@
import { RaceDetailUserResultDTO } from '../types/generated/RaceDetailUserResultDTO';
import { RaceDetailUserResultDTO } from '@/lib/types/generated/RaceDetailUserResultDTO';
export class RaceDetailUserResultViewModel {
position!: number;

View File

@@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest';
import type { RaceDetailLeagueDTO } from '../types/generated/RaceDetailLeagueDTO';
import type { RaceDetailRaceDTO } from '../types/generated/RaceDetailRaceDTO';
import type { RaceDetailRegistrationDTO } from '../types/generated/RaceDetailRegistrationDTO';
import type { RaceDetailUserResultDTO } from '../types/generated/RaceDetailUserResultDTO';
import type { RaceDetailEntryDTO } from '../types/RaceDetailEntryDTO';
import type { RaceDetailLeagueDTO } from '@/lib/types/generated/RaceDetailLeagueDTO';
import type { RaceDetailRaceDTO } from '@/lib/types/generated/RaceDetailRaceDTO';
import type { RaceDetailRegistrationDTO } from '@/lib/types/generated/RaceDetailRegistrationDTO';
import type { RaceDetailUserResultDTO } from '@/lib/types/generated/RaceDetailUserResultDTO';
import type { RaceDetailEntryDTO } from '@/lib/types/RaceDetailEntryDTO';
import { RaceDetailViewModel } from './RaceDetailViewModel';
describe('RaceDetailViewModel', () => {
@@ -262,7 +262,7 @@ describe('RaceDetailViewModel', () => {
});
const cancelledVm = new RaceDetailViewModel({
race: createMockRace({ status: 'cancelled' as any }),
race: createMockRace({ status: 'cancelled' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),

View File

@@ -1,81 +0,0 @@
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/generated/RaceDetailEntryDTO';
import { RaceDetailEntryViewModel } from './RaceDetailEntryViewModel';
import { RaceDetailUserResultViewModel } from './RaceDetailUserResultViewModel';
export class RaceDetailViewModel {
race: RaceDetailRaceDTO | null;
league: RaceDetailLeagueDTO | null;
entryList: RaceDetailEntryViewModel[];
registration: RaceDetailRegistrationDTO;
userResult: RaceDetailUserResultViewModel | null;
error?: string;
constructor(dto: {
race: RaceDetailRaceDTO | null;
league: RaceDetailLeagueDTO | null;
entryList: RaceDetailEntryDTO[];
registration: RaceDetailRegistrationDTO;
userResult: RaceDetailUserResultDTO | null;
error?: string;
}, currentDriverId: string) {
this.race = dto.race;
this.league = dto.league;
this.entryList = dto.entryList.map(entry => new RaceDetailEntryViewModel(entry, currentDriverId));
this.registration = dto.registration;
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 as any).isUserRegistered ?? (this.registration as any).isRegistered ?? false;
}
/** UI-specific: Whether user can register */
get canRegister(): boolean {
return this.registration.canRegister;
}
/** UI-specific: Race status display */
get raceStatusDisplay(): string {
if (!this.race) return 'Unknown';
switch (this.race.status) {
case 'upcoming': return 'Upcoming';
case 'live': return 'Live';
case 'finished': return 'Finished';
default: return this.race.status;
}
}
/** UI-specific: Formatted scheduled time */
get formattedScheduledTime(): string {
return this.race ? new Date(this.race.scheduledAt).toLocaleString() : '';
}
/** UI-specific: Entry list count */
get entryCount(): number {
return this.entryList.length;
}
/** UI-specific: Whether race has results */
get hasResults(): boolean {
return this.userResult !== null;
}
/** UI-specific: Registration status message */
get registrationStatusMessage(): string {
if (this.isRegistered) return 'You are registered for this race';
if (this.canRegister) return 'You can register for this race';
return 'Registration not available';
}
/** UI-specific: Whether race can be re-opened */
get canReopenRace(): boolean {
if (!this.race) return false;
return this.race.status === 'completed' || this.race.status === 'cancelled';
}
}

View File

@@ -1,4 +1,4 @@
import { RaceResultDTO } from '../types/generated/RaceResultDTO';
import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO';
export class RaceResultViewModel {
driverId!: string;

View File

@@ -1,6 +1,6 @@
import type { LeagueMembershipsViewModel } from '@/lib/view-models/LeagueMembershipsViewModel';
import type { RaceResultsDetailViewModel } from '@/lib/view-models/RaceResultsDetailViewModel';
import type { RaceWithSOFViewModel } from '@/lib/view-models/RaceWithSOFViewModel';
import type { LeagueMembershipsViewModel } from './LeagueMembershipsViewModel';
import type { RaceResultsDetailViewModel } from './RaceResultsDetailViewModel';
import type { RaceWithSOFViewModel } from './RaceWithSOFViewModel';
// TODO fucking violating our architecture, it should be a ViewModel
@@ -58,7 +58,7 @@ export class RaceResultsDataTransformer {
}
// Transform results
const results = resultsData.results.map((result: any) => ({
const results = resultsData.results.map((result) => ({
position: result.position,
driverId: result.driverId,
driverName: result.driverName,
@@ -74,9 +74,9 @@ export class RaceResultsDataTransformer {
}));
// Transform penalties
const penalties = resultsData.penalties.map((penalty: any) => ({
const penalties = resultsData.penalties.map((penalty) => ({
driverId: penalty.driverId,
driverName: resultsData.results.find((r: any) => r.driverId === penalty.driverId)?.driverName || 'Unknown',
driverName: resultsData.results.find((r) => r.driverId === penalty.driverId)?.driverName || 'Unknown',
type: penalty.type as 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points',
value: penalty.value || 0,
reason: 'Penalty applied', // Default since view model doesn't have reason
@@ -84,7 +84,7 @@ export class RaceResultsDataTransformer {
}));
// Transform memberships
const memberships = membershipsData?.memberships.map((membership: any) => ({
const memberships = membershipsData?.memberships.map((membership) => ({
driverId: membership.driverId,
role: membership.role || 'member',
}));

View File

@@ -1,5 +1,5 @@
import { RaceResultDTO } from '../types/generated/RaceResultDTO';
import { RaceResultsDetailDTO } from '../types/generated/RaceResultsDetailDTO';
import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO';
import { RaceResultsDetailDTO } from '@/lib/types/generated/RaceResultsDetailDTO';
import { RaceResultViewModel } from './RaceResultViewModel';
export class RaceResultsDetailViewModel {

View File

@@ -1,4 +1,4 @@
import type { RaceStatsDTO } from '../types/generated/RaceStatsDTO';
import type { RaceStatsDTO } from '@/lib/types/generated/RaceStatsDTO';
/**
* Race stats view model

View File

@@ -1,5 +1,5 @@
import { RaceDTO } from '../types/generated/RaceDTO';
import { RacesPageDataRaceDTO } from '../types/generated/RacesPageDataRaceDTO';
import { RaceDTO } from '@/lib/types/generated/RaceDTO';
import { RacesPageDataRaceDTO } from '@/lib/types/generated/RacesPageDataRaceDTO';
export class RaceViewModel {
constructor(
@@ -31,15 +31,15 @@ export class RaceViewModel {
}
get track(): string {
return (this.dto as any).track || '';
return 'track' in this.dto ? this.dto.track || '' : '';
}
get car(): string {
return (this.dto as any).car || '';
return 'car' in this.dto ? this.dto.car || '' : '';
}
get status(): string | undefined {
return this._status || (this.dto as any).status;
return this._status || ('status' in this.dto ? this.dto.status : undefined);
}
get registeredCount(): number | undefined {
@@ -47,7 +47,7 @@ export class RaceViewModel {
}
get strengthOfField(): number | undefined {
return this._strengthOfField || (this.dto as any).strengthOfField;
return this._strengthOfField || ('strengthOfField' in this.dto ? this.dto.strengthOfField : undefined);
}
/** UI-specific: Formatted date */

View File

@@ -1,4 +1,4 @@
import { RaceWithSOFDTO } from '../types/generated/RaceWithSOFDTO';
import { RaceWithSOFDTO } from '@/lib/types/generated/RaceWithSOFDTO';
export class RaceWithSOFViewModel {
id: string;
@@ -8,6 +8,6 @@ export class RaceWithSOFViewModel {
constructor(dto: RaceWithSOFDTO) {
this.id = dto.id;
this.track = dto.track;
this.strengthOfField = (dto as any).strengthOfField ?? null;
this.strengthOfField = 'strengthOfField' in dto ? dto.strengthOfField ?? null : null;
}
}

View File

@@ -1,6 +1,5 @@
import { RaceListItemViewModel } from './RaceListItemViewModel';
import type { RacesPageDataRaceDTO } from '../types/generated/RacesPageDataRaceDTO';
import type { RaceListItemDTO } from './RaceListItemViewModel';
import type { RacesPageDataRaceDTO } from '@/lib/types/generated/RacesPageDataRaceDTO';
// DTO matching the backend RacesPageDataDTO
interface RacesPageDTO {
@@ -16,23 +15,33 @@ export class RacesPageViewModel {
constructor(dto: RacesPageDTO) {
this.races = dto.races.map((r) => {
const status = (r as any).status as string | undefined;
const status = 'status' in r ? r.status : 'unknown';
const isUpcoming =
(r as any).isUpcoming ??
'isUpcoming' in r ? r.isUpcoming :
(status === 'upcoming' || status === 'scheduled');
const isLive =
(r as any).isLive ??
'isLive' in r ? r.isLive :
(status === 'live' || status === 'running');
const isPast =
(r as any).isPast ??
'isPast' in r ? r.isPast :
(status === 'completed' || status === 'finished' || status === 'cancelled');
const normalized: RaceListItemDTO = {
...(r as any),
strengthOfField: (r as any).strengthOfField ?? null,
// Build the RaceListItemDTO from the input with proper type checking
const scheduledAt = 'scheduledAt' in r ? r.scheduledAt :
('date' in r ? (r as { date?: string }).date : '');
const normalized = {
id: r.id,
track: 'track' in r ? r.track : '',
car: 'car' in r ? r.car : '',
scheduledAt: scheduledAt || '',
status: status,
leagueId: 'leagueId' in r ? r.leagueId : '',
leagueName: 'leagueName' in r ? r.leagueName : '',
strengthOfField: 'strengthOfField' in r ? (r as { strengthOfField?: number }).strengthOfField ?? null : null,
isUpcoming: Boolean(isUpcoming),
isLive: Boolean(isLive),
isPast: Boolean(isPast),
@@ -76,4 +85,4 @@ export class RacesPageViewModel {
get completedRaces(): RaceListItemViewModel[] {
return this.races.filter(r => r.status === 'completed');
}
}
}

View File

@@ -1,4 +1,4 @@
import type { RecordEngagementOutputDTO } from '../types/generated/RecordEngagementOutputDTO';
import type { RecordEngagementOutputDTO } from '@/lib/types/generated/RecordEngagementOutputDTO';
/**
* Record engagement output view model

View File

@@ -1,4 +1,4 @@
import type { RecordPageViewOutputDTO } from '../types/generated/RecordPageViewOutputDTO';
import type { RecordPageViewOutputDTO } from '@/lib/types/generated/RecordPageViewOutputDTO';
/**
* Record page view output view model

View File

@@ -1,4 +1,4 @@
import { RemoveLeagueMemberOutputDTO } from '../types/generated/RemoveLeagueMemberOutputDTO';
import { RemoveLeagueMemberOutputDTO } from '@/lib/types/generated/RemoveLeagueMemberOutputDTO';
/**
* View Model for Remove Member Result

View File

@@ -1,4 +1,4 @@
import { RequestAvatarGenerationOutputDTO } from '../types/generated/RequestAvatarGenerationOutputDTO';
import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
/**
* Request Avatar Generation View Model

View File

@@ -1,4 +1,4 @@
import { AuthenticatedUserDTO } from '../types/generated/AuthenticatedUserDTO';
import { AuthenticatedUserDTO } from '@/lib/types/generated/AuthenticatedUserDTO';
export class SessionViewModel {
userId: string;
@@ -6,31 +6,28 @@ export class SessionViewModel {
displayName: string;
avatarUrl?: string | null;
role?: string;
driverId?: string;
isAuthenticated: boolean = true;
constructor(dto: AuthenticatedUserDTO) {
this.userId = dto.userId;
this.email = dto.email;
this.displayName = dto.displayName;
const anyDto = dto as unknown as { primaryDriverId?: unknown; driverId?: unknown; avatarUrl?: unknown; role?: unknown };
if (typeof anyDto.primaryDriverId === 'string' && anyDto.primaryDriverId) {
this.driverId = anyDto.primaryDriverId;
} else if (typeof anyDto.driverId === 'string' && anyDto.driverId) {
this.driverId = anyDto.driverId;
// Use the optional fields from the DTO
if (dto.primaryDriverId) {
this.driverId = dto.primaryDriverId;
}
if (anyDto.avatarUrl !== undefined) {
this.avatarUrl = anyDto.avatarUrl as string | null;
if (dto.avatarUrl !== undefined) {
this.avatarUrl = dto.avatarUrl;
}
if (typeof anyDto.role === 'string' && anyDto.role) {
this.role = anyDto.role;
if (dto.role) {
this.role = dto.role;
}
}
// Note: The generated DTO doesn't have these fields
// These will need to be added when the OpenAPI spec is updated
driverId?: string;
isAuthenticated: boolean = true;
/**
* Compatibility accessor.
* Some legacy components expect `session.user.*`.
@@ -73,4 +70,4 @@ export class SessionViewModel {
get authStatusDisplay(): string {
return this.isAuthenticated ? 'Logged In' : 'Logged Out';
}
}
}

View File

@@ -1,138 +0,0 @@
import type { SponsorDashboardDTO } from '../types/generated/SponsorDashboardDTO';
import { SponsorshipViewModel } from './SponsorshipViewModel';
import { ActivityItemViewModel } from './ActivityItemViewModel';
import { RenewalAlertViewModel } from './RenewalAlertViewModel';
/**
* Sponsor Dashboard View Model
*
* View model for sponsor dashboard data with UI-specific transformations.
*/
export class SponsorDashboardViewModel {
sponsorId: string;
sponsorName: string;
metrics: any;
sponsorships: {
leagues: SponsorshipViewModel[];
teams: SponsorshipViewModel[];
drivers: SponsorshipViewModel[];
races: SponsorshipViewModel[];
platform: SponsorshipViewModel[];
};
recentActivity: ActivityItemViewModel[];
upcomingRenewals: RenewalAlertViewModel[];
constructor(dto: SponsorDashboardDTO) {
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: (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: any) => new ActivityItemViewModel(a));
this.upcomingRenewals = (dto.upcomingRenewals || []).map((r: any) => new RenewalAlertViewModel(r));
}
get totalSponsorships(): number {
return this.sponsorships.leagues.length +
this.sponsorships.teams.length +
this.sponsorships.drivers.length +
this.sponsorships.races.length +
this.sponsorships.platform.length;
}
get activeSponsorships(): number {
const all = [
...this.sponsorships.leagues,
...this.sponsorships.teams,
...this.sponsorships.drivers,
...this.sponsorships.races,
...this.sponsorships.platform,
];
return all.filter(s => s.status === 'active').length;
}
get totalInvestment(): number {
const all = [
...this.sponsorships.leagues,
...this.sponsorships.teams,
...this.sponsorships.drivers,
...this.sponsorships.races,
...this.sponsorships.platform,
];
return all.filter(s => s.status === 'active').reduce((sum, s) => sum + s.price, 0);
}
get totalImpressions(): number {
const all = [
...this.sponsorships.leagues,
...this.sponsorships.teams,
...this.sponsorships.drivers,
...this.sponsorships.races,
...this.sponsorships.platform,
];
return all.reduce((sum, s) => sum + s.impressions, 0);
}
/** UI-specific: Formatted total investment */
get formattedTotalInvestment(): string {
return `$${this.totalInvestment.toLocaleString()}`;
}
/** UI-specific: Active percentage */
get activePercentage(): number {
if (this.totalSponsorships === 0) return 0;
return Math.round((this.activeSponsorships / this.totalSponsorships) * 100);
}
/** UI-specific: Has sponsorships */
get hasSponsorships(): boolean {
return this.totalSponsorships > 0;
}
/** UI-specific: Status text */
get statusText(): string {
if (this.activeSponsorships === 0) return 'No active sponsorships';
if (this.activeSponsorships === this.totalSponsorships) return 'All sponsorships active';
return `${this.activeSponsorships} of ${this.totalSponsorships} active`;
}
/** UI-specific: Cost per 1K views */
get costPerThousandViews(): string {
if (this.totalImpressions === 0) return '$0.00';
return `$${(this.totalInvestment / this.totalImpressions * 1000).toFixed(2)}`;
}
/** UI-specific: Category data for charts */
get categoryData() {
return {
leagues: {
count: this.sponsorships.leagues.length,
impressions: this.sponsorships.leagues.reduce((sum, l) => sum + l.impressions, 0),
},
teams: {
count: this.sponsorships.teams.length,
impressions: this.sponsorships.teams.reduce((sum, t) => sum + t.impressions, 0),
},
drivers: {
count: this.sponsorships.drivers.length,
impressions: this.sponsorships.drivers.reduce((sum, d) => sum + d.impressions, 0),
},
races: {
count: this.sponsorships.races.length,
impressions: this.sponsorships.races.reduce((sum, r) => sum + r.impressions, 0),
},
platform: {
count: this.sponsorships.platform.length,
impressions: this.sponsorships.platform.reduce((sum, p) => sum + p.impressions, 0),
},
};
}
}

View File

@@ -1,4 +1,4 @@
import type { SponsorSponsorshipsDTO } from '../types/generated/SponsorSponsorshipsDTO';
import type { SponsorSponsorshipsDTO } from '@/lib/types/generated/SponsorSponsorshipsDTO';
import { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel';
/**

View File

@@ -1,4 +1,4 @@
import { SponsorshipDetailDTO } from '../types/generated/SponsorshipDetailDTO';
import { SponsorshipDetailDTO } from '@/lib/types/generated/SponsorshipDetailDTO';
export class SponsorshipDetailViewModel {
id: string;

View File

@@ -1,3 +1,27 @@
/**
* Interface for sponsorship data input
*/
export interface SponsorshipDataInput {
id: string;
type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';
entityId: string;
entityName: string;
tier?: 'main' | 'secondary';
status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';
applicationDate?: string | Date;
approvalDate?: string | Date;
rejectionReason?: string;
startDate: string | Date;
endDate: string | Date;
price: number;
impressions: number;
impressionsChange?: number;
engagement?: number;
details?: string;
entityOwner?: string;
applicationMessage?: string;
}
/**
* Sponsorship View Model
*
@@ -23,7 +47,7 @@ export class SponsorshipViewModel {
entityOwner?: string;
applicationMessage?: string;
constructor(data: any) {
constructor(data: SponsorshipDataInput) {
this.id = data.id;
this.type = data.type;
this.entityId = data.entityId;

View File

@@ -1,4 +1,4 @@
import { LeagueStandingDTO } from '../types/generated/LeagueStandingDTO';
import { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO';
export class StandingEntryViewModel {
driverId: string;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { WalletTransactionViewModel } from './WalletTransactionViewModel';
import { WalletTransactionViewModel, FullTransactionDto } from './WalletTransactionViewModel';
const createTx = (overrides: Partial<any> = {}): any => ({
const createTx = (overrides: Partial<FullTransactionDto> = {}): FullTransactionDto => ({
id: 'tx-1',
type: 'sponsorship',
description: 'Test',
@@ -44,7 +44,7 @@ describe('WalletTransactionViewModel', () => {
});
it('derives typeDisplay and formattedDate', () => {
const vm = new WalletTransactionViewModel(createTx({ type: 'membership' as any }));
const vm = new WalletTransactionViewModel(createTx({ type: 'membership' }));
expect(vm.typeDisplay).toBe('Membership');
expect(typeof vm.formattedDate).toBe('string');

View File

@@ -1,4 +1,4 @@
import { WalletDTO } from '../types/generated/WalletDTO';
import { WalletDTO } from '@/lib/types/generated/WalletDTO';
import { FullTransactionDto, WalletTransactionViewModel } from './WalletTransactionViewModel';
export class WalletViewModel {

View File

@@ -1,91 +1,100 @@
export * from './ActivityItemViewModel';
export * from './AnalyticsDashboardViewModel';
export * from './AnalyticsMetricsViewModel';
export * from './AvailableLeaguesViewModel';
export * from './AvatarGenerationViewModel';
export * from './AvatarViewModel';
export * from './BillingViewModel';
export * from './CompleteOnboardingViewModel';
export * from './CreateLeagueViewModel';
export * from './CreateTeamViewModel';
export * from './view-models/ActivityItemViewModel';
export * from './view-models/AnalyticsDashboardViewModel';
export * from './view-models/AnalyticsMetricsViewModel';
export * from './view-models/AvailableLeaguesViewModel';
export * from './view-models/AvatarGenerationViewModel';
export * from './view-models/AvatarViewModel';
export * from './view-models/BillingViewModel';
export * from './view-models/CompleteOnboardingViewModel';
export * from './view-models/CreateLeagueViewModel';
export * from './view-models/CreateTeamViewModel';
export {
DashboardOverviewViewModel,
} from './DashboardOverviewViewModel';
export * from './DeleteMediaViewModel';
export * from './DriverLeaderboardItemViewModel';
export * from './DriverLeaderboardViewModel';
export * from './DriverProfileViewModel';
export * from './DriverRegistrationStatusViewModel';
export * from './DriverSummaryViewModel';
export * from './DriverTeamViewModel';
export * from './DriverViewModel';
export * from './EmailSignupViewModel';
export * from './HomeDiscoveryViewModel';
export * from './ImportRaceResultsSummaryViewModel';
export * from './LeagueAdminViewModel';
export * from './LeagueCardViewModel';
export * from './LeagueDetailPageViewModel';
export { LeagueDetailViewModel, LeagueViewModel } from './LeagueDetailViewModel';
export * from './LeagueJoinRequestViewModel';
export * from './LeagueMembershipsViewModel';
export * from './LeagueMemberViewModel';
export * from './LeaguePageDetailViewModel';
export * from './LeagueScheduleViewModel';
export * from './LeagueScoringChampionshipViewModel';
export * from './LeagueScoringConfigViewModel';
export * from './LeagueScoringPresetsViewModel';
export * from './LeagueScoringPresetViewModel';
export * from './LeagueSettingsViewModel';
export * from './LeagueStandingsViewModel';
export * from './LeagueStatsViewModel';
export * from './LeagueStewardingViewModel';
export * from './LeagueSummaryViewModel';
export * from './LeagueWalletViewModel';
export * from './MediaViewModel';
export * from './MembershipFeeViewModel';
export * from './PaymentViewModel';
export * from './PrizeViewModel';
export * from './ProfileOverviewViewModel';
export * from './ProtestDriverViewModel';
export * from './ProtestViewModel';
export * from './RaceDetailEntryViewModel';
export * from './RaceDetailUserResultViewModel';
export * from './RaceDetailViewModel';
export * from './RaceListItemViewModel';
export * from './RaceResultsDataTransformer';
export * from './RaceResultsDetailViewModel';
export * from './RaceResultViewModel';
export * from './RacesPageViewModel';
export * from './RaceStatsViewModel';
export * from './RaceStewardingViewModel';
export * from './RaceViewModel';
export * from './RaceWithSOFViewModel';
export * from './RecordEngagementInputViewModel';
export * from './RecordEngagementOutputViewModel';
export * from './RecordPageViewInputViewModel';
export * from './RecordPageViewOutputViewModel';
export * from './RemoveMemberViewModel';
export * from './RenewalAlertViewModel';
export * from './RequestAvatarGenerationViewModel';
export * from './SessionViewModel';
export * from './SponsorDashboardViewModel';
export * from './SponsorSettingsViewModel';
export * from './SponsorshipDetailViewModel';
export * from './SponsorshipPricingViewModel';
export * from './SponsorshipRequestViewModel';
export * from './SponsorshipViewModel';
export * from './SponsorSponsorshipsViewModel';
export * from './SponsorViewModel';
export * from './StandingEntryViewModel';
export * from './TeamCardViewModel';
export * from './TeamDetailsViewModel';
export * from './TeamJoinRequestViewModel';
export * from './TeamMemberViewModel';
export * from './TeamSummaryViewModel';
export * from './UpcomingRaceCardViewModel';
export * from './UpdateAvatarViewModel';
export * from './UpdateTeamViewModel';
export * from './UploadMediaViewModel';
export * from './UserProfileViewModel';
export * from './WalletTransactionViewModel';
export * from './WalletViewModel';
} from './view-models/DashboardOverviewViewModel';
export * from './view-models/DashboardOverviewViewModelData';
export * from './view-models/DeleteMediaViewModel';
export * from './view-models/DriverLeaderboardItemViewModel';
export * from './view-models/DriverLeaderboardViewModel';
export * from './view-models/DriverProfileViewModel';
export * from './view-models/DriverRegistrationStatusViewModel';
export * from './view-models/DriverSummaryViewModel';
export * from './view-models/DriverTeamViewModel';
export * from './view-models/DriverViewModel';
export * from './view-models/EmailSignupViewModel';
export * from './view-models/HomeDiscoveryViewModel';
export * from './view-models/ImportRaceResultsSummaryViewModel';
export * from './view-models/LeagueAdminViewModel';
export * from './view-models/LeagueCardViewModel';
export * from './view-models/LeagueDetailPageViewModel';
export { LeagueDetailViewModel, LeagueViewModel } from './view-models/LeagueDetailViewModel';
export * from './view-models/LeagueJoinRequestViewModel';
export * from './view-models/LeagueMembershipsViewModel';
export * from './view-models/LeagueMemberViewModel';
export * from './view-models/LeaguePageDetailViewModel';
export * from './view-models/LeagueScheduleViewModel';
export * from './view-models/LeagueScoringChampionshipViewModel';
export * from './view-models/LeagueScoringConfigViewModel';
export * from './view-models/LeagueScoringPresetsViewModel';
export * from './view-models/LeagueScoringPresetViewModel';
export * from './view-models/LeagueScoringSectionViewModel';
export * from './view-models/LeagueSettingsViewModel';
export * from './view-models/LeagueStandingsViewModel';
export * from './view-models/LeagueStatsViewModel';
export * from './view-models/LeagueStewardingViewModel';
export * from './view-models/LeagueSummaryViewModel';
export * from './view-models/LeagueWalletViewModel';
export * from './view-models/MediaViewModel';
export * from './view-models/MembershipFeeViewModel';
export * from './view-models/PaymentViewModel';
export * from './view-models/PrizeViewModel';
export * from './view-models/ProfileOverviewViewModel';
export * from './view-models/ProtestDriverViewModel';
export * from './view-models/ProtestViewModel';
export * from './view-models/RaceDetailEntryViewModel';
export * from './view-models/RaceDetailUserResultViewModel';
export * from './view-models/RaceDetailViewModel';
export * from './view-models/RaceListItemViewModel';
export * from './view-models/RaceResultsDataTransformer';
export * from './view-models/RaceResultsDetailViewModel';
export * from './view-models/RaceResultViewModel';
export * from './view-models/RacesPageViewModel';
export * from './view-models/RaceStatsViewModel';
export * from './view-models/RaceStewardingViewModel';
export * from './view-models/RaceViewModel';
export * from './view-models/RaceWithSOFViewModel';
export * from './view-models/RecordEngagementInputViewModel';
export * from './view-models/RecordEngagementOutputViewModel';
export * from './view-models/RecordPageViewInputViewModel';
export * from './view-models/RecordPageViewOutputViewModel';
export * from './view-models/RemoveMemberViewModel';
export * from './view-models/RenewalAlertViewModel';
export * from './view-models/RequestAvatarGenerationViewModel';
export * from './view-models/ScoringConfigurationViewModel';
export * from './view-models/SessionViewModel';
export * from './view-models/SponsorDashboardViewModel';
export * from './view-models/SponsorSettingsViewModel';
export * from './view-models/SponsorshipDetailViewModel';
export * from './view-models/SponsorshipPricingViewModel';
export * from './view-models/SponsorshipRequestViewModel';
export * from './view-models/SponsorshipViewModel';
export * from './view-models/SponsorSponsorshipsViewModel';
export * from './view-models/SponsorViewModel';
export * from './view-models/StandingEntryViewModel';
export * from './view-models/TeamCardViewModel';
export * from './view-models/TeamDetailsViewModel';
export * from './view-models/TeamJoinRequestViewModel';
export * from './view-models/TeamMemberViewModel';
export * from './view-models/TeamSummaryViewModel';
export * from './view-models/UpcomingRaceCardViewModel';
export * from './view-models/UpdateAvatarViewModel';
export * from './view-models/UpdateTeamViewModel';
export * from './view-models/UploadMediaViewModel';
export * from './view-models/UserProfileViewModel';
export * from './view-models/WalletTransactionViewModel';
export * from './view-models/WalletViewModel';
export * from './presenters/DashboardPresenter';
export * from './presenters/ProfileLeaguesPresenter';
export * from './presenters/TeamDetailPresenter';
export * from './presenters/TeamsPresenter';
export * from './presenters/AdminViewModelPresenter';