website refactor

This commit is contained in:
2026-01-17 22:55:03 +01:00
parent 64d9e7fd16
commit 69d4cce7f1
64 changed files with 1146 additions and 1014 deletions

View File

@@ -15,7 +15,9 @@ describe('LeagueDetailPageViewModel', () => {
description: 'Top tier competition',
ownerId: 'owner-1',
createdAt: '2025-01-01T00:00:00Z',
maxDrivers: 40,
settings: {
maxDrivers: 40,
},
socialLinks: {
discordUrl: 'https://discord.gg/example',
youtubeUrl: 'https://youtube.com/example',
@@ -43,7 +45,7 @@ describe('LeagueDetailPageViewModel', () => {
];
const memberships: LeagueMembershipsDTO = {
memberships: [
members: [
{
driverId: 'owner-1',
role: 'owner',
@@ -104,14 +106,14 @@ describe('LeagueDetailPageViewModel', () => {
expect(vm.name).toBe(league.name);
expect(vm.description).toBe(league.description);
expect(vm.ownerId).toBe(league.ownerId);
expect(vm.settings.maxDrivers).toBe(league.maxDrivers);
expect(vm.socialLinks?.discordUrl).toBe(league.socialLinks?.discordUrl);
expect(vm.settings.maxDrivers).toBe((league.settings as any).maxDrivers);
expect(vm.socialLinks?.discordUrl).toBe((league.socialLinks as any).discordUrl);
expect(vm.owner).toEqual(owner);
expect(vm.scoringConfig).toBeNull();
expect(vm.drivers).toHaveLength(drivers.length);
expect(vm.memberships).toHaveLength(memberships.memberships.length);
expect(vm.memberships).toHaveLength(memberships.members.length);
expect(vm.allRaces).toHaveLength(allRaces.length);
expect(vm.runningRaces.every(r => r.status === 'running')).toBe(true);
@@ -129,7 +131,7 @@ describe('LeagueDetailPageViewModel', () => {
sponsors,
);
const memberCount = memberships.memberships.length;
const memberCount = memberships.members.length;
const mainSponsorTaken = sponsors.some(s => s.tier === 'main');
const secondaryTaken = sponsors.filter(s => s.tier === 'secondary').length;
@@ -189,7 +191,7 @@ describe('LeagueDetailPageViewModel', () => {
expect(vmLow.sponsorInsights.tier).toBe('starter');
expect(vmHigh.sponsorInsights.trustScore).toBe(
Math.min(100, 60 + memberships.memberships.length + (leagueStats as any).completedRaces),
Math.min(100, 60 + memberships.members.length + (leagueStats as any).completedRaces),
);
});

View File

@@ -1,309 +0,0 @@
import { describe, expect, it } from 'vitest';
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', () => {
const createMockRace = (overrides?: Partial<RaceDetailRaceDTO>): RaceDetailRaceDTO => ({
id: 'race-123',
title: 'Test Race',
scheduledAt: '2023-12-31T20:00:00Z',
status: 'upcoming',
...overrides,
});
const createMockLeague = (): RaceDetailLeagueDTO => ({
id: 'league-123',
name: 'Test League',
});
const createMockRegistration = (
overrides?: Partial<RaceDetailRegistrationDTO>
): RaceDetailRegistrationDTO => ({
isRegistered: false,
canRegister: true,
...overrides,
});
it('should create instance with all properties', () => {
const race = createMockRace();
const league = createMockLeague();
const entries: RaceDetailEntryDTO[] = [];
const registration = createMockRegistration();
const userResult: RaceDetailUserResultDTO | null = null;
const viewModel = new RaceDetailViewModel({
race,
league,
entryList: entries,
registration,
userResult,
}, 'current-driver');
expect(viewModel.race).toBe(race);
expect(viewModel.league).toBe(league);
expect(viewModel.entryList).toHaveLength(0);
expect(viewModel.registration).toBe(registration);
expect(viewModel.userResult).toBe(userResult);
});
it('should handle null race and league', () => {
const viewModel = new RaceDetailViewModel({
race: null,
league: null,
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
expect(viewModel.race).toBeNull();
expect(viewModel.league).toBeNull();
});
it('should return correct isRegistered value', () => {
const registeredVm = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration({ isRegistered: true }),
userResult: null,
});
const notRegisteredVm = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration({ isRegistered: false }),
userResult: null,
});
expect(registeredVm.isRegistered).toBe(true);
expect(notRegisteredVm.isRegistered).toBe(false);
});
it('should return correct canRegister value', () => {
const canRegisterVm = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration({ canRegister: true }),
userResult: null,
});
const cannotRegisterVm = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration({ canRegister: false }),
userResult: null,
});
expect(canRegisterVm.canRegister).toBe(true);
expect(cannotRegisterVm.canRegister).toBe(false);
});
it('should format race status correctly', () => {
const upcomingVm = new RaceDetailViewModel({
race: createMockRace({ status: 'upcoming' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
const liveVm = new RaceDetailViewModel({
race: createMockRace({ status: 'live' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
const finishedVm = new RaceDetailViewModel({
race: createMockRace({ status: 'finished' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
expect(upcomingVm.raceStatusDisplay).toBe('Upcoming');
expect(liveVm.raceStatusDisplay).toBe('Live');
expect(finishedVm.raceStatusDisplay).toBe('Finished');
});
it('should return Unknown for status when race is null', () => {
const viewModel = new RaceDetailViewModel({
race: null,
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
expect(viewModel.raceStatusDisplay).toBe('Unknown');
});
it('should format scheduled time correctly', () => {
const viewModel = new RaceDetailViewModel({
race: createMockRace({ scheduledAt: '2023-12-31T20:00:00Z' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
const formatted = viewModel.formattedScheduledTime;
expect(formatted).toContain('2023');
expect(formatted).toContain('12/31');
});
it('should return empty string for formatted time when race is null', () => {
const viewModel = new RaceDetailViewModel({
race: null,
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
expect(viewModel.formattedScheduledTime).toBe('');
});
it('should return correct entry count', () => {
const entries: RaceDetailEntryDTO[] = [
{ driverId: 'driver-1', carId: 'car-1' },
{ driverId: 'driver-2', carId: 'car-2' },
{ driverId: 'driver-3', carId: 'car-3' },
] as RaceDetailEntryDTO[];
const viewModel = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: entries,
registration: createMockRegistration(),
userResult: null,
});
expect(viewModel.entryCount).toBe(3);
});
it('should return true for hasResults when userResult exists', () => {
const viewModel = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: { position: 1, lapTime: 90.5 } as RaceDetailUserResultDTO,
});
expect(viewModel.hasResults).toBe(true);
});
it('should return false for hasResults when userResult is null', () => {
const viewModel = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
expect(viewModel.hasResults).toBe(false);
});
it('should return correct registration status message when registered', () => {
const viewModel = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration({ isRegistered: true }),
userResult: null,
});
expect(viewModel.registrationStatusMessage).toBe('You are registered for this race');
});
it('should return correct registration status message when can register', () => {
const viewModel = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration({ isRegistered: false, canRegister: true }),
userResult: null,
});
expect(viewModel.registrationStatusMessage).toBe('You can register for this race');
});
it('should return correct registration status message when cannot register', () => {
const viewModel = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration({ isRegistered: false, canRegister: false }),
userResult: null,
});
expect(viewModel.registrationStatusMessage).toBe('Registration not available');
});
it('should expose canReopenRace for completed and cancelled statuses', () => {
const completedVm = new RaceDetailViewModel({
race: createMockRace({ status: 'completed' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
const cancelledVm = new RaceDetailViewModel({
race: createMockRace({ status: 'cancelled' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
const upcomingVm = new RaceDetailViewModel({
race: createMockRace({ status: 'upcoming' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
expect(completedVm.canReopenRace).toBe(true);
expect(cancelledVm.canReopenRace).toBe(true);
expect(upcomingVm.canReopenRace).toBe(false);
});
it('should handle error property', () => {
const viewModel = new RaceDetailViewModel({
race: createMockRace(),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
error: 'Failed to load race details',
});
expect(viewModel.error).toBe('Failed to load race details');
});
it('should handle custom race status', () => {
const viewModel = new RaceDetailViewModel({
race: createMockRace({ status: 'cancelled' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
expect(viewModel.raceStatusDisplay).toBe('cancelled');
});
});

View File

@@ -5,8 +5,7 @@ import { RaceResultViewModel } from './RaceResultViewModel';
export class RaceResultsDetailViewModel {
raceId: string;
track: string;
private currentUserId: string;
currentUserId: string;
constructor(dto: RaceResultsDetailDTO & { results?: RaceResultDTO[] }, currentUserId: string) {
this.raceId = dto.raceId;

View File

@@ -1,165 +0,0 @@
import { describe, it, expect } from 'vitest';
import { SponsorDashboardViewModel } from './SponsorDashboardViewModel';
import { SponsorshipViewModel } from './SponsorshipViewModel';
import { ActivityItemViewModel } from './ActivityItemViewModel';
import { RenewalAlertViewModel } from './RenewalAlertViewModel';
function makeSponsorship(overrides: Partial<any> = {}) {
return {
id: 's-1',
type: 'leagues',
entityId: 'league-1',
entityName: 'Pro League',
status: 'active',
startDate: '2025-01-01',
endDate: '2025-12-31',
price: 5_000,
impressions: 50_000,
...overrides,
};
}
describe('SponsorDashboardViewModel', () => {
const baseDto = {
sponsorId: 'sp-1',
sponsorName: 'Acme Corp',
metrics: { totalSpend: 10_000, totalImpressions: 100_000 },
sponsorships: {
leagues: [makeSponsorship()],
teams: [makeSponsorship({ id: 's-2', type: 'teams', price: 2_000, impressions: 20_000 })],
drivers: [],
races: [],
platform: [],
},
recentActivity: [
{ id: 'a-1', type: 'impression', timestamp: '2025-01-01T00:00:00Z', metadata: {} },
],
upcomingRenewals: [
{ id: 'r-1', sponsorshipId: 's-1', leagueName: 'Pro League', daysUntilRenewal: 10 },
],
} as any;
it('maps nested DTOs into view models', () => {
const vm = new SponsorDashboardViewModel(baseDto);
expect(vm.sponsorId).toBe(baseDto.sponsorId);
expect(vm.sponsorName).toBe(baseDto.sponsorName);
expect(vm.metrics).toBe(baseDto.metrics);
expect(vm.sponsorships.leagues[0]).toBeInstanceOf(SponsorshipViewModel);
expect(vm.sponsorships.teams[0]).toBeInstanceOf(SponsorshipViewModel);
expect(vm.recentActivity[0]).toBeInstanceOf(ActivityItemViewModel);
expect(vm.upcomingRenewals[0]).toBeInstanceOf(RenewalAlertViewModel);
});
it('computes total, active counts and investment from sponsorship buckets', () => {
const vm = new SponsorDashboardViewModel(baseDto);
const all = [
...baseDto.sponsorships.leagues,
...baseDto.sponsorships.teams,
];
expect(vm.totalSponsorships).toBe(all.length);
expect(vm.activeSponsorships).toBe(all.filter(s => s.status === 'active').length);
const expectedInvestment = all
.filter(s => s.status === 'active')
.reduce((sum, s) => sum + s.price, 0);
expect(vm.totalInvestment).toBe(expectedInvestment);
});
it('aggregates total impressions across all sponsorships', () => {
const vm = new SponsorDashboardViewModel(baseDto);
const all = [
...baseDto.sponsorships.leagues,
...baseDto.sponsorships.teams,
];
const expectedImpressions = all.reduce((sum, s) => sum + s.impressions, 0);
expect(vm.totalImpressions).toBe(expectedImpressions);
});
it('derives formatted investment, active percentage, status text and CPM', () => {
const vm = new SponsorDashboardViewModel(baseDto);
expect(vm.formattedTotalInvestment).toBe(`$${vm.totalInvestment.toLocaleString()}`);
const expectedActivePercentage = Math.round((vm.activeSponsorships / vm.totalSponsorships) * 100);
expect(vm.activePercentage).toBe(expectedActivePercentage);
expect(vm.hasSponsorships).toBe(true);
// statusText variants
const noActive = new SponsorDashboardViewModel({
...baseDto,
sponsorships: {
leagues: [makeSponsorship({ status: 'expired' })],
teams: [],
drivers: [],
races: [],
platform: [],
},
} as any);
expect(noActive.statusText).toBe('No active sponsorships');
const allActive = new SponsorDashboardViewModel({
...baseDto,
sponsorships: {
leagues: [makeSponsorship()],
teams: [],
drivers: [],
races: [],
platform: [],
},
} as any);
expect(allActive.statusText).toBe('All sponsorships active');
// cost per thousand views
expect(vm.costPerThousandViews).toBe(`$${(vm.totalInvestment / vm.totalImpressions * 1000).toFixed(2)}`);
const zeroImpressions = new SponsorDashboardViewModel({
...baseDto,
sponsorships: {
leagues: [makeSponsorship({ impressions: 0 })],
teams: [],
drivers: [],
races: [],
platform: [],
},
} as any);
expect(zeroImpressions.costPerThousandViews).toBe('$0.00');
});
it('exposes category data per sponsorship bucket', () => {
const vm = new SponsorDashboardViewModel(baseDto);
expect(vm.categoryData.leagues.count).toBe(baseDto.sponsorships.leagues.length);
expect(vm.categoryData.leagues.impressions).toBe(
baseDto.sponsorships.leagues.reduce((sum: number, s: any) => sum + s.impressions, 0),
);
expect(vm.categoryData.teams.count).toBe(baseDto.sponsorships.teams.length);
expect(vm.categoryData.teams.impressions).toBe(
baseDto.sponsorships.teams.reduce((sum: number, s: any) => sum + s.impressions, 0),
);
});
it('handles missing sponsorship buckets gracefully', () => {
const vm = new SponsorDashboardViewModel({
...baseDto,
sponsorships: undefined,
recentActivity: undefined,
upcomingRenewals: undefined,
} as any);
expect(vm.totalSponsorships).toBe(0);
expect(vm.activeSponsorships).toBe(0);
expect(vm.totalInvestment).toBe(0);
expect(vm.totalImpressions).toBe(0);
expect(vm.activePercentage).toBe(0);
expect(vm.hasSponsorships).toBe(false);
expect(vm.costPerThousandViews).toBe('$0.00');
expect(vm.recentActivity).toEqual([]);
expect(vm.upcomingRenewals).toEqual([]);
});
});

View File

@@ -0,0 +1,21 @@
import { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
/**
* Sponsor Dashboard View Model
*
* Represents dashboard data for a sponsor with UI-specific transformations.
*/
export class SponsorDashboardViewModel {
sponsorId: string;
sponsorName: string;
constructor(dto: SponsorDashboardDTO) {
this.sponsorId = dto.sponsorId;
this.sponsorName = dto.sponsorName;
}
/** UI-specific: Welcome message */
get welcomeMessage(): string {
return `Welcome back, ${this.sponsorName}!`;
}
}

View File

@@ -1,8 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('view-models index', () => {
it('should export view models', async () => {
const module = await import('./index');
expect(Object.keys(module).length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,97 @@
export * from "./ActivityItemViewModel";
export * from "./AdminUserViewModel";
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 "./DeleteMediaViewModel";
export * from "./DriverLeaderboardItemViewModel";
export * from "./DriverLeaderboardViewModel";
export * from "./DriverProfileDriverSummaryViewModel";
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 "./LeagueAdminRosterJoinRequestViewModel";
export * from "./LeagueAdminRosterMemberViewModel";
export * from "./LeagueAdminScheduleViewModel";
export * from "./LeagueAdminViewModel";
export * from "./LeagueCardViewModel";
export * from "./LeagueDetailPageViewModel";
export * 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 "./LeagueScoringSectionViewModel";
export * from "./LeagueSeasonSummaryViewModel";
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 "./OnboardingViewModel";
export * from "./PaymentViewModel";
export * from "./PrizeViewModel";
export * from "./ProfileOverviewViewModel";
export * from "./ProtestDetailViewModel";
export * from "./ProtestDriverViewModel";
export * from "./ProtestViewModel";
export * from "./RaceDetailEntryViewModel";
export * from "./RaceDetailsViewModel";
export * from "./RaceDetailUserResultViewModel";
export * from "./RaceListItemViewModel";
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 "./ScoringConfigurationViewModel";
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";