diff --git a/apps/website/lib/builders/view-data/AdminDashboardViewDataBuilder.ts b/apps/website/lib/builders/view-data/AdminDashboardViewDataBuilder.ts index f7edb8106..086593244 100644 --- a/apps/website/lib/builders/view-data/AdminDashboardViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/AdminDashboardViewDataBuilder.ts @@ -1,3 +1,4 @@ +import { ViewData } from '@/lib/contracts/view-data/ViewData'; import type { DashboardStats } from '@/lib/types/admin'; import type { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData'; @@ -7,7 +8,14 @@ import type { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewD * Transforms DashboardStats API DTO into AdminDashboardViewData for server-side rendering. * Deterministic; side-effect free; no HTTP calls. */ -export class AdminDashboardViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class AdminDashboardViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return AdminDashboardViewDataBuilder.build(input); + } + + static build( static build(apiDto: DashboardStats): AdminDashboardViewData { return { stats: { diff --git a/apps/website/lib/builders/view-data/AdminUsersViewDataBuilder.ts b/apps/website/lib/builders/view-data/AdminUsersViewDataBuilder.ts index 55cded0e5..2effc8dba 100644 --- a/apps/website/lib/builders/view-data/AdminUsersViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/AdminUsersViewDataBuilder.ts @@ -1,27 +1,19 @@ +import { ViewData } from '@/lib/contracts/view-data/ViewData'; import type { UserListResponse } from '@/lib/types/admin'; import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData'; -/** - * AdminUsersViewDataBuilder - * - * Server-side builder that transforms API DTO - * into ViewData for the AdminUsersTemplate. - * - * Deterministic, side-effect free. - */ -export class AdminUsersViewDataBuilder { - static build(apiDto: UserListResponse): AdminUsersViewData { - const users = apiDto.users.map(user => ({ - id: user.id, - email: user.email, - displayName: user.displayName, - roles: user.roles, - status: user.status, - isSystemAdmin: user.isSystemAdmin, - createdAt: typeof user.createdAt === 'string' ? user.createdAt : (user.createdAt as unknown as Date).toISOString(), - updatedAt: typeof user.updatedAt === 'string' ? user.updatedAt : (user.updatedAt as unknown as Date).toISOString(), - lastLoginAt: user.lastLoginAt ? (typeof user.lastLoginAt === 'string' ? user.lastLoginAt : (user.lastLoginAt as unknown as Date).toISOString()) : undefined, - primaryDriverId: user.primaryDriverId, +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class AdminUsersViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return AdminUsersViewDataBuilder.build(input); + } + + static build( + public static build(apiDto: UserListResponse): AdminUsersViewData { + const users = apiDto.users.map(u => ({ + ...u, + joinedAt: new Date(u.joinedAt), })); return { @@ -35,4 +27,4 @@ export class AdminUsersViewDataBuilder { adminCount: users.filter(u => u.isSystemAdmin).length, }; } -} \ No newline at end of file +} diff --git a/apps/website/lib/builders/view-data/AnalyticsDashboardViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/AnalyticsDashboardViewDataBuilder.test.ts new file mode 100644 index 000000000..24c9df43f --- /dev/null +++ b/apps/website/lib/builders/view-data/AnalyticsDashboardViewDataBuilder.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect } from 'vitest'; +import { AnalyticsDashboardViewDataBuilder } from './AnalyticsDashboardViewDataBuilder'; +import { AnalyticsDashboardInputViewData } from '@/lib/view-data/AnalyticsDashboardInputViewData'; + +describe('AnalyticsDashboardViewDataBuilder', () => { + it('builds ViewData from AnalyticsDashboardInputViewData', () => { + const inputViewData: AnalyticsDashboardInputViewData = { + totalUsers: 100, + activeUsers: 40, + totalRaces: 10, + totalLeagues: 5, + }; + + const viewData = AnalyticsDashboardViewDataBuilder.build(inputViewData); + + expect(viewData.metrics.totalUsers).toBe(100); + expect(viewData.metrics.activeUsers).toBe(40); + expect(viewData.metrics.totalRaces).toBe(10); + expect(viewData.metrics.totalLeagues).toBe(5); + expect(viewData.metrics.userEngagementRate).toBeCloseTo(40); + expect(viewData.metrics.formattedEngagementRate).toBe('40.0%'); + expect(viewData.metrics.activityLevel).toBe('Low'); + }); + + it('computes engagement rate and formatted engagement rate', () => { + const inputViewData: AnalyticsDashboardInputViewData = { + totalUsers: 200, + activeUsers: 50, + totalRaces: 0, + totalLeagues: 0, + }; + + const viewData = AnalyticsDashboardViewDataBuilder.build(inputViewData); + + expect(viewData.metrics.userEngagementRate).toBeCloseTo(25); + expect(viewData.metrics.formattedEngagementRate).toBe('25.0%'); + }); + + it('handles zero users safely', () => { + const inputViewData: AnalyticsDashboardInputViewData = { + totalUsers: 0, + activeUsers: 0, + totalRaces: 0, + totalLeagues: 0, + }; + + const viewData = AnalyticsDashboardViewDataBuilder.build(inputViewData); + + expect(viewData.metrics.userEngagementRate).toBe(0); + expect(viewData.metrics.formattedEngagementRate).toBe('0.0%'); + expect(viewData.metrics.activityLevel).toBe('Low'); + }); + + it('derives activity level buckets from engagement rate', () => { + const low = AnalyticsDashboardViewDataBuilder.build({ + totalUsers: 100, + activeUsers: 30, + totalRaces: 0, + totalLeagues: 0, + }); + const medium = AnalyticsDashboardViewDataBuilder.build({ + totalUsers: 100, + activeUsers: 50, + totalRaces: 0, + totalLeagues: 0, + }); + const high = AnalyticsDashboardViewDataBuilder.build({ + totalUsers: 100, + activeUsers: 90, + totalRaces: 0, + totalLeagues: 0, + }); + + expect(low.metrics.activityLevel).toBe('Low'); + expect(medium.metrics.activityLevel).toBe('Medium'); + expect(high.metrics.activityLevel).toBe('High'); + }); +}); diff --git a/apps/website/lib/builders/view-data/AnalyticsDashboardViewDataBuilder.ts b/apps/website/lib/builders/view-data/AnalyticsDashboardViewDataBuilder.ts new file mode 100644 index 000000000..9803cb661 --- /dev/null +++ b/apps/website/lib/builders/view-data/AnalyticsDashboardViewDataBuilder.ts @@ -0,0 +1,34 @@ +import { AnalyticsDashboardInputViewData } from '@/lib/view-data/AnalyticsDashboardInputViewData'; +import { AnalyticsDashboardViewData } from '@/lib/view-data/AnalyticsDashboardViewData'; + +/** + * AnalyticsDashboardViewDataBuilder + * + * Transforms AnalyticsDashboardInputViewData into AnalyticsDashboardViewData for server-side rendering. + * Deterministic; side-effect free; no HTTP calls. + */ +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class AnalyticsDashboardViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return AnalyticsDashboardViewDataBuilder.build(input); + } + + static build(viewData: AnalyticsDashboardInputViewData): AnalyticsDashboardViewData { + const userEngagementRate = viewData.totalUsers > 0 ? (viewData.activeUsers / viewData.totalUsers) * 100 : 0; + const formattedEngagementRate = `${userEngagementRate.toFixed(1)}%`; + const activityLevel = userEngagementRate > 70 ? 'High' : userEngagementRate > 40 ? 'Medium' : 'Low'; + + return { + metrics: { + totalUsers: viewData.totalUsers, + activeUsers: viewData.activeUsers, + totalRaces: viewData.totalRaces, + totalLeagues: viewData.totalLeagues, + userEngagementRate, + formattedEngagementRate, + activityLevel, + }, + }; + } +} diff --git a/apps/website/lib/builders/view-data/AvatarViewDataBuilder.ts b/apps/website/lib/builders/view-data/AvatarViewDataBuilder.ts index 2841bc51c..8d41eea0b 100644 --- a/apps/website/lib/builders/view-data/AvatarViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/AvatarViewDataBuilder.ts @@ -8,7 +8,14 @@ import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import { AvatarViewData } from '@/lib/view-data/AvatarViewData'; -export class AvatarViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class AvatarViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return AvatarViewDataBuilder.build(input); + } + + static build( static build(apiDto: MediaBinaryDTO): AvatarViewData { return { buffer: Buffer.from(apiDto.buffer).toString('base64'), diff --git a/apps/website/lib/builders/view-data/CategoryIconViewDataBuilder.ts b/apps/website/lib/builders/view-data/CategoryIconViewDataBuilder.ts index 4fcdd4068..2a0ab7590 100644 --- a/apps/website/lib/builders/view-data/CategoryIconViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/CategoryIconViewDataBuilder.ts @@ -8,7 +8,14 @@ import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import { CategoryIconViewData } from '@/lib/view-data/CategoryIconViewData'; -export class CategoryIconViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class CategoryIconViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return CategoryIconViewDataBuilder.build(input); + } + + static build( static build(apiDto: MediaBinaryDTO): CategoryIconViewData { return { buffer: Buffer.from(apiDto.buffer).toString('base64'), diff --git a/apps/website/lib/builders/view-data/CompleteOnboardingViewData.ts b/apps/website/lib/builders/view-data/CompleteOnboardingViewData.ts index 77529732e..e000f1fd6 100644 --- a/apps/website/lib/builders/view-data/CompleteOnboardingViewData.ts +++ b/apps/website/lib/builders/view-data/CompleteOnboardingViewData.ts @@ -1,4 +1,6 @@ -export interface CompleteOnboardingViewData { +import { ViewData } from "@/lib/contracts/view-data/ViewData"; + +export interface CompleteOnboardingViewData extends ViewData { success: boolean; driverId?: string; errorMessage?: string; diff --git a/apps/website/lib/builders/view-data/CompleteOnboardingViewDataBuilder.ts b/apps/website/lib/builders/view-data/CompleteOnboardingViewDataBuilder.ts index 3c8903fc3..3c4d1d140 100644 --- a/apps/website/lib/builders/view-data/CompleteOnboardingViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/CompleteOnboardingViewDataBuilder.ts @@ -6,8 +6,16 @@ import { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO'; import { CompleteOnboardingViewData } from './CompleteOnboardingViewData'; +import { ViewData } from '@/lib/contracts/view-data/ViewData'; -export class CompleteOnboardingViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class CompleteOnboardingViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return CompleteOnboardingViewDataBuilder.build(input); + } + + static build( /** * Transform DTO into ViewData * diff --git a/apps/website/lib/builders/view-data/DashboardViewDataBuilder.ts b/apps/website/lib/builders/view-data/DashboardViewDataBuilder.ts index 1bd761442..4af70b49a 100644 --- a/apps/website/lib/builders/view-data/DashboardViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/DashboardViewDataBuilder.ts @@ -6,6 +6,8 @@ import { DashboardRankDisplay } from '@/lib/display-objects/DashboardRankDisplay import { DashboardConsistencyDisplay } from '@/lib/display-objects/DashboardConsistencyDisplay'; import { DashboardCountDisplay } from '@/lib/display-objects/DashboardCountDisplay'; import { DashboardLeaguePositionDisplay } from '@/lib/display-objects/DashboardLeaguePositionDisplay'; +import { ViewData } from '@/lib/contracts/view-data/ViewData'; +import { number } from 'zod'; /** * DashboardViewDataBuilder @@ -13,7 +15,14 @@ import { DashboardLeaguePositionDisplay } from '@/lib/display-objects/DashboardL * Transforms DashboardOverviewDTO (API DTO) into DashboardViewData for server-side rendering. * Deterministic; side-effect free; no HTTP calls. */ -export class DashboardViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class DashboardViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return DashboardViewDataBuilder.build(input); + } + + static build( static build(apiDto: DashboardOverviewDTO): DashboardViewData { return { currentDriver: { diff --git a/apps/website/lib/builders/view-data/DeleteMediaViewData.ts b/apps/website/lib/builders/view-data/DeleteMediaViewData.ts new file mode 100644 index 000000000..4740a5c16 --- /dev/null +++ b/apps/website/lib/builders/view-data/DeleteMediaViewData.ts @@ -0,0 +1,6 @@ +import { ViewData } from "@/lib/contracts/view-data/ViewData"; + +export interface DeleteMediaViewData extends ViewData { + success: boolean; + error?: string; +} diff --git a/apps/website/lib/builders/view-data/DeleteMediaViewDataBuilder.test.ts b/apps/website/lib/builders/view-data/DeleteMediaViewDataBuilder.test.ts new file mode 100644 index 000000000..12ab457ba --- /dev/null +++ b/apps/website/lib/builders/view-data/DeleteMediaViewDataBuilder.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect } from 'vitest'; +import { DeleteMediaViewDataBuilder } from './DeleteMediaViewDataBuilder'; +import type { DeleteMediaOutputDTO } from '@/lib/types/generated/DeleteMediaOutputDTO'; + +describe('DeleteMediaViewDataBuilder', () => { + describe('happy paths', () => { + it('should transform successful deletion DTO to ViewData correctly', () => { + const apiDto: DeleteMediaOutputDTO = { + success: true, + }; + + const result = DeleteMediaViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + success: true, + error: undefined, + }); + }); + + it('should handle deletion with error message', () => { + const apiDto: DeleteMediaOutputDTO = { + success: false, + error: 'Failed to delete media', + }; + + const result = DeleteMediaViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + success: false, + error: 'Failed to delete media', + }); + }); + + it('should handle deletion with only success field', () => { + const apiDto: DeleteMediaOutputDTO = { + success: true, + }; + + const result = DeleteMediaViewDataBuilder.build(apiDto); + + expect(result).toEqual({ + success: true, + error: undefined, + }); + }); + }); + + describe('data transformation', () => { + it('should preserve all DTO fields in the output', () => { + const apiDto: DeleteMediaOutputDTO = { + success: false, + error: 'Something went wrong', + }; + + const result = DeleteMediaViewDataBuilder.build(apiDto); + + expect(result.success).toBe(apiDto.success); + expect(result.error).toBe(apiDto.error); + }); + + it('should not modify the input DTO', () => { + const apiDto: DeleteMediaOutputDTO = { + success: false, + error: 'Error', + }; + + const originalDto = { ...apiDto }; + DeleteMediaViewDataBuilder.build(apiDto); + + expect(apiDto).toEqual(originalDto); + }); + }); + + describe('edge cases', () => { + it('should handle false success value', () => { + const apiDto: DeleteMediaOutputDTO = { + success: false, + error: 'Error occurred', + }; + + const result = DeleteMediaViewDataBuilder.build(apiDto); + + expect(result.success).toBe(false); + expect(result.error).toBe('Error occurred'); + }); + + it('should handle empty string error message', () => { + const apiDto: DeleteMediaOutputDTO = { + success: false, + error: '', + }; + + const result = DeleteMediaViewDataBuilder.build(apiDto); + + expect(result.success).toBe(false); + expect(result.error).toBe(''); + }); + + it('should handle very long error message', () => { + const longError = 'Error: ' + 'a'.repeat(1000); + const apiDto: DeleteMediaOutputDTO = { + success: false, + error: longError, + }; + + const result = DeleteMediaViewDataBuilder.build(apiDto); + + expect(result.error).toBe(longError); + }); + + it('should handle special characters in error message', () => { + const apiDto: DeleteMediaOutputDTO = { + success: false, + error: 'Error: "Failed to delete media" (code: 500)', + }; + + const result = DeleteMediaViewDataBuilder.build(apiDto); + + expect(result.error).toBe('Error: "Failed to delete media" (code: 500)'); + }); + }); +}); diff --git a/apps/website/lib/builders/view-data/DeleteMediaViewDataBuilder.ts b/apps/website/lib/builders/view-data/DeleteMediaViewDataBuilder.ts new file mode 100644 index 000000000..14024d6f2 --- /dev/null +++ b/apps/website/lib/builders/view-data/DeleteMediaViewDataBuilder.ts @@ -0,0 +1,30 @@ +/** + * DeleteMedia ViewData Builder + * + * Transforms media deletion result into ViewData for templates. + */ + +import { DeleteMediaOutputDTO } from '@/lib/types/generated/DeleteMediaOutputDTO'; +import { DeleteMediaViewData } from './DeleteMediaViewData'; +import { ViewData } from '@/lib/contracts/view-data/ViewData'; + +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class DeleteMediaViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return DeleteMediaViewDataBuilder.build(input); + } + + /** + * Transform DTO into ViewData + * + * @param apiDto - The API DTO to transform + * @returns ViewData for templates + */ + static build(apiDto: DeleteMediaOutputDTO): DeleteMediaViewData { + return { + success: apiDto.success, + error: apiDto.error, + }; + } +} diff --git a/apps/website/lib/builders/view-data/DriverProfileViewDataBuilder.ts b/apps/website/lib/builders/view-data/DriverProfileViewDataBuilder.ts index 10df12ba3..a0c49f474 100644 --- a/apps/website/lib/builders/view-data/DriverProfileViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/DriverProfileViewDataBuilder.ts @@ -1,5 +1,5 @@ import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO'; -import type { DriverProfileViewData } from '@/lib/types/view-data/DriverProfileViewData'; +import type { DriverProfileViewData } from '@/lib/view-data/DriverProfileViewData'; import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; @@ -12,7 +12,14 @@ import { PercentDisplay } from '@/lib/display-objects/PercentDisplay'; * Transforms GetDriverProfileOutputDTO into ViewData for the driver profile page. * Deterministic, side-effect free, no HTTP calls. */ -export class DriverProfileViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class DriverProfileViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return DriverProfileViewDataBuilder.build(input); + } + + static build( static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData { return { currentDriver: apiDto.currentDriver ? { diff --git a/apps/website/lib/builders/view-data/DriverRankingsViewDataBuilder.ts b/apps/website/lib/builders/view-data/DriverRankingsViewDataBuilder.ts index 8d7b1f83b..9e4d043a4 100644 --- a/apps/website/lib/builders/view-data/DriverRankingsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/DriverRankingsViewDataBuilder.ts @@ -1,9 +1,15 @@ import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewData'; -import { WinRateDisplay } from '@/lib/display-objects/WinRateDisplay'; -import { MedalDisplay } from '@/lib/display-objects/MedalDisplay'; +import { ViewData } from '@/lib/contracts/view-data/ViewData'; -export class DriverRankingsViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class DriverRankingsViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return DriverRankingsViewDataBuilder.build(input); + } + + static build( static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData { if (!apiDto || apiDto.length === 0) { return { diff --git a/apps/website/lib/builders/view-data/DriversViewDataBuilder.ts b/apps/website/lib/builders/view-data/DriversViewDataBuilder.ts index 3ae21eb9f..950867963 100644 --- a/apps/website/lib/builders/view-data/DriversViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/DriversViewDataBuilder.ts @@ -1,9 +1,16 @@ import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO'; -import type { DriversViewData } from '@/lib/types/view-data/DriversViewData'; +import type { DriversViewData } from '@/lib/view-data/DriversViewData'; import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; -export class DriversViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class DriversViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return DriversViewDataBuilder.build(input); + } + + static build( static build(dto: DriversLeaderboardDTO): DriversViewData { return { drivers: dto.drivers.map(driver => ({ diff --git a/apps/website/lib/builders/view-data/ForgotPasswordViewDataBuilder.ts b/apps/website/lib/builders/view-data/ForgotPasswordViewDataBuilder.ts index 66af517f2..7ecf5a260 100644 --- a/apps/website/lib/builders/view-data/ForgotPasswordViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/ForgotPasswordViewDataBuilder.ts @@ -6,9 +6,18 @@ */ import { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO'; -import { ForgotPasswordViewData } from './types/ForgotPasswordViewData'; +import { ForgotPasswordViewData } from '../../view-data/ForgotPasswordViewData'; +import { ViewData } from '@/lib/contracts/view-data/ViewData'; +import { error } from 'console'; -export class ForgotPasswordViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class ForgotPasswordViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return ForgotPasswordViewDataBuilder.build(input); + } + + static build( static build(apiDto: ForgotPasswordPageDTO): ForgotPasswordViewData { return { returnTo: apiDto.returnTo, diff --git a/apps/website/lib/builders/view-data/GenerateAvatarsViewData.ts b/apps/website/lib/builders/view-data/GenerateAvatarsViewData.ts index 3522cb969..b739d9f16 100644 --- a/apps/website/lib/builders/view-data/GenerateAvatarsViewData.ts +++ b/apps/website/lib/builders/view-data/GenerateAvatarsViewData.ts @@ -1,4 +1,6 @@ -export interface GenerateAvatarsViewData { +import { ViewData } from "@/lib/contracts/view-data/ViewData"; + +export interface GenerateAvatarsViewData extends ViewData { success: boolean; avatarUrls: string[]; errorMessage?: string; diff --git a/apps/website/lib/builders/view-data/GenerateAvatarsViewDataBuilder.ts b/apps/website/lib/builders/view-data/GenerateAvatarsViewDataBuilder.ts index ff9ef7b25..03cc18720 100644 --- a/apps/website/lib/builders/view-data/GenerateAvatarsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/GenerateAvatarsViewDataBuilder.ts @@ -7,8 +7,16 @@ import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO'; import { GenerateAvatarsViewData } from './GenerateAvatarsViewData'; +import { ViewData } from '@/lib/contracts/view-data/ViewData'; -export class GenerateAvatarsViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class GenerateAvatarsViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return GenerateAvatarsViewDataBuilder.build(input); + } + + static build( /** * Transform DTO into ViewData * diff --git a/apps/website/lib/builders/view-data/HealthViewDataBuilder.ts b/apps/website/lib/builders/view-data/HealthViewDataBuilder.ts index 127f57859..4db051f48 100644 --- a/apps/website/lib/builders/view-data/HealthViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/HealthViewDataBuilder.ts @@ -37,7 +37,14 @@ export interface HealthDTO { }>; } -export class HealthViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class HealthViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return HealthViewDataBuilder.build(input); + } + + static build( static build(dto: HealthDTO): HealthViewData { const now = new Date(); const lastUpdated = dto.timestamp || now.toISOString(); diff --git a/apps/website/lib/builders/view-data/HomeViewDataBuilder.ts b/apps/website/lib/builders/view-data/HomeViewDataBuilder.ts index 241f360f7..3f574553c 100644 --- a/apps/website/lib/builders/view-data/HomeViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/HomeViewDataBuilder.ts @@ -1,12 +1,20 @@ import type { HomeViewData } from '@/templates/HomeTemplate'; import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO'; +import { ViewData } from '@/lib/contracts/view-data/ViewData'; /** * HomeViewDataBuilder * * Transforms HomeDataDTO to HomeViewData. */ -export class HomeViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class HomeViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return HomeViewDataBuilder.build(input); + } + + static build( /** * Build HomeViewData from HomeDataDTO * diff --git a/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.ts index 371bf49e7..54f358976 100644 --- a/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.ts @@ -1,8 +1,16 @@ +import { ViewData } from '@/lib/contracts/view-data/ViewData'; import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData'; -export class LeaderboardsViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeaderboardsViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeaderboardsViewDataBuilder.build(input); + } + + static build( static build( apiDto: { drivers: { drivers: DriverLeaderboardItemDTO[] }; teams: GetTeamsLeaderboardOutputDTO } ): LeaderboardsViewData { diff --git a/apps/website/lib/builders/view-data/LeagueCoverViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueCoverViewDataBuilder.ts index 1ea99f4e3..6e5fc5177 100644 --- a/apps/website/lib/builders/view-data/LeagueCoverViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeagueCoverViewDataBuilder.ts @@ -8,7 +8,14 @@ import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import { LeagueCoverViewData } from '@/lib/view-data/LeagueCoverViewData'; -export class LeagueCoverViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeagueCoverViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeagueCoverViewDataBuilder.build(input); + } + + static build( static build(apiDto: MediaBinaryDTO): LeagueCoverViewData { return { buffer: Buffer.from(apiDto.buffer).toString('base64'), diff --git a/apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder.ts index 488fa4630..4fd312007 100644 --- a/apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder.ts @@ -11,7 +11,14 @@ import type { LeagueDetailViewData, LeagueInfoData, LiveRaceData, DriverSummaryD * Transforms API DTOs into LeagueDetailViewData for server-side rendering. * Deterministic; side-effect free; no HTTP calls. */ -export class LeagueDetailViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeagueDetailViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeagueDetailViewDataBuilder.build(input); + } + + static build( static build(input: { league: LeagueWithCapacityAndScoringDTO; owner: GetDriverOutputDTO | null; diff --git a/apps/website/lib/builders/view-data/LeagueLogoViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueLogoViewDataBuilder.ts index 6614d713c..acbb727f9 100644 --- a/apps/website/lib/builders/view-data/LeagueLogoViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeagueLogoViewDataBuilder.ts @@ -8,7 +8,14 @@ import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import { LeagueLogoViewData } from '@/lib/view-data/LeagueLogoViewData'; -export class LeagueLogoViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeagueLogoViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeagueLogoViewDataBuilder.build(input); + } + + static build( static build(apiDto: MediaBinaryDTO): LeagueLogoViewData { return { buffer: Buffer.from(apiDto.buffer).toString('base64'), diff --git a/apps/website/lib/builders/view-data/LeagueRosterAdminViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueRosterAdminViewDataBuilder.ts index fdcb8df78..9f53a3aa3 100644 --- a/apps/website/lib/builders/view-data/LeagueRosterAdminViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeagueRosterAdminViewDataBuilder.ts @@ -9,7 +9,14 @@ import { DateDisplay } from '@/lib/display-objects/DateDisplay'; * Transforms API DTOs into LeagueRosterAdminViewData for server-side rendering. * Deterministic; side-effect free; no HTTP calls. */ -export class LeagueRosterAdminViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeagueRosterAdminViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeagueRosterAdminViewDataBuilder.build(input); + } + + static build( static build(input: { leagueId: string; members: LeagueRosterMemberDTO[]; diff --git a/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.ts index 0bed5a548..2565fa0d5 100644 --- a/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.ts @@ -1,7 +1,14 @@ import { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData'; import { LeagueScheduleApiDto } from '@/lib/types/tbd/LeagueScheduleApiDto'; -export class LeagueScheduleViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeagueScheduleViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeagueScheduleViewDataBuilder.build(input); + } + + static build( static build(apiDto: LeagueScheduleApiDto, currentDriverId?: string, isAdmin: boolean = false): LeagueScheduleViewData { const now = new Date(); diff --git a/apps/website/lib/builders/view-data/LeagueSettingsViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueSettingsViewDataBuilder.ts index 384ddebf2..ca829c123 100644 --- a/apps/website/lib/builders/view-data/LeagueSettingsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeagueSettingsViewDataBuilder.ts @@ -1,7 +1,14 @@ -import { LeagueSettingsViewData } from '@/lib/view-data/leagues/LeagueSettingsViewData'; import { LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto'; +import { LeagueSettingsViewData } from '@/lib/view-data/LeagueSettingsViewData'; -export class LeagueSettingsViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeagueSettingsViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeagueSettingsViewDataBuilder.build(input); + } + + static build( static build(apiDto: LeagueSettingsApiDto): LeagueSettingsViewData { return { leagueId: apiDto.leagueId, diff --git a/apps/website/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder.ts index 11fb272c3..eb4672f16 100644 --- a/apps/website/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder.ts @@ -1,9 +1,16 @@ -import { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData'; -import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto'; import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { StatusDisplay } from '@/lib/display-objects/StatusDisplay'; +import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto'; +import { LeagueSponsorshipsViewData } from '@/lib/view-data/LeagueSponsorshipsViewData'; -export class LeagueSponsorshipsViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeagueSponsorshipsViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeagueSponsorshipsViewDataBuilder.build(input); + } + + static build( static build(apiDto: LeagueSponsorshipsApiDto): LeagueSponsorshipsViewData { return { leagueId: apiDto.leagueId, diff --git a/apps/website/lib/builders/view-data/LeagueStandingsViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueStandingsViewDataBuilder.ts index 72fe81921..f35714e6f 100644 --- a/apps/website/lib/builders/view-data/LeagueStandingsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeagueStandingsViewDataBuilder.ts @@ -16,7 +16,14 @@ interface LeagueMembershipsApiDto { * Transforms API DTOs into LeagueStandingsViewData for server-side rendering. * Deterministic; side-effect free; no HTTP calls. */ -export class LeagueStandingsViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeagueStandingsViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeagueStandingsViewDataBuilder.build(input); + } + + static build( static build( standingsDto: LeagueStandingsApiDto, membershipsDto: LeagueMembershipsApiDto, diff --git a/apps/website/lib/builders/view-data/LeagueWalletViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueWalletViewDataBuilder.ts index e800a7f30..d025c014c 100644 --- a/apps/website/lib/builders/view-data/LeagueWalletViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeagueWalletViewDataBuilder.ts @@ -1,9 +1,16 @@ -import { LeagueWalletViewData, LeagueWalletTransactionViewData } from '@/lib/view-data/leagues/LeagueWalletViewData'; -import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto'; import { CurrencyDisplay } from '@/lib/display-objects/CurrencyDisplay'; import { DateDisplay } from '@/lib/display-objects/DateDisplay'; +import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto'; +import { LeagueWalletTransactionViewData, LeagueWalletViewData } from '@/lib/view-data/LeagueWalletViewData'; -export class LeagueWalletViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeagueWalletViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeagueWalletViewDataBuilder.build(input); + } + + static build( static build(apiDto: LeagueWalletApiDto): LeagueWalletViewData { const transactions: LeagueWalletTransactionViewData[] = apiDto.transactions.map(t => ({ ...t, diff --git a/apps/website/lib/builders/view-data/LeaguesViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeaguesViewDataBuilder.ts index 358c73201..8346b76ca 100644 --- a/apps/website/lib/builders/view-data/LeaguesViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeaguesViewDataBuilder.ts @@ -7,7 +7,14 @@ import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData'; * Transforms AllLeaguesWithCapacityAndScoringDTO (API DTO) into LeaguesViewData for server-side rendering. * Deterministic; side-effect free; no HTTP calls. */ -export class LeaguesViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LeaguesViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LeaguesViewDataBuilder.build(input); + } + + static build( static build(apiDto: AllLeaguesWithCapacityAndScoringDTO): LeaguesViewData { return { leagues: apiDto.leagues.map((league) => ({ diff --git a/apps/website/lib/builders/view-data/LoginViewDataBuilder.ts b/apps/website/lib/builders/view-data/LoginViewDataBuilder.ts index 0d9c44314..89e6695e7 100644 --- a/apps/website/lib/builders/view-data/LoginViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LoginViewDataBuilder.ts @@ -6,9 +6,18 @@ */ import { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO'; -import { LoginViewData } from './types/LoginViewData'; +import { LoginViewData } from '../../view-data/LoginViewData'; +import { ViewData } from '@/lib/contracts/view-data/ViewData'; +import { error } from 'console'; -export class LoginViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class LoginViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return LoginViewDataBuilder.build(input); + } + + static build( static build(apiDto: LoginPageDTO): LoginViewData { return { returnTo: apiDto.returnTo, diff --git a/apps/website/lib/builders/view-data/OnboardingPageViewDataBuilder.ts b/apps/website/lib/builders/view-data/OnboardingPageViewDataBuilder.ts index 79bfabcfc..51c17630e 100644 --- a/apps/website/lib/builders/view-data/OnboardingPageViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/OnboardingPageViewDataBuilder.ts @@ -6,7 +6,14 @@ import { OnboardingPageViewData } from '@/lib/view-data/OnboardingPageViewData'; -export class OnboardingPageViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class OnboardingPageViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return OnboardingPageViewDataBuilder.build(input); + } + + static build( /** * Transform driver data into ViewData * diff --git a/apps/website/lib/builders/view-data/OnboardingViewDataBuilder.ts b/apps/website/lib/builders/view-data/OnboardingViewDataBuilder.ts index de2595327..d8d004b41 100644 --- a/apps/website/lib/builders/view-data/OnboardingViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/OnboardingViewDataBuilder.ts @@ -9,7 +9,14 @@ import { Result } from '@/lib/contracts/Result'; import { PresentationError } from '@/lib/contracts/page-queries/PresentationError'; import { OnboardingPageViewData } from '@/lib/view-data/OnboardingPageViewData'; -export class OnboardingViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class OnboardingViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return OnboardingViewDataBuilder.build(input); + } + + static build( static build(apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError>): Result { if (apiDto.isErr()) { return Result.err(apiDto.getError()); diff --git a/apps/website/lib/builders/view-data/ProfileLeaguesViewDataBuilder.ts b/apps/website/lib/builders/view-data/ProfileLeaguesViewDataBuilder.ts index fe075a5a6..59e167dd1 100644 --- a/apps/website/lib/builders/view-data/ProfileLeaguesViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/ProfileLeaguesViewDataBuilder.ts @@ -19,7 +19,14 @@ interface ProfileLeaguesPageDto { * ViewData Builder for Profile Leagues page * Transforms Page DTO to ViewData for templates */ -export class ProfileLeaguesViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class ProfileLeaguesViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return ProfileLeaguesViewDataBuilder.build(input); + } + + static build( static build(apiDto: ProfileLeaguesPageDto): ProfileLeaguesViewData { return { ownedLeagues: apiDto.ownedLeagues.map((league: { leagueId: string; name: string; description: string; membershipRole: 'owner' | 'admin' | 'steward' | 'member'; }) => ({ diff --git a/apps/website/lib/builders/view-data/ProfileViewDataBuilder.ts b/apps/website/lib/builders/view-data/ProfileViewDataBuilder.ts index f5a6f974d..fb4a42a22 100644 --- a/apps/website/lib/builders/view-data/ProfileViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/ProfileViewDataBuilder.ts @@ -8,7 +8,14 @@ import { PercentDisplay } from '@/lib/display-objects/PercentDisplay'; import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; -export class ProfileViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class ProfileViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return ProfileViewDataBuilder.build(input); + } + + static build( static build(apiDto: GetDriverProfileOutputDTO): ProfileViewData { const driver = apiDto.currentDriver; diff --git a/apps/website/lib/builders/view-data/ProtestDetailViewDataBuilder.ts b/apps/website/lib/builders/view-data/ProtestDetailViewDataBuilder.ts index f60095480..9a2fdc703 100644 --- a/apps/website/lib/builders/view-data/ProtestDetailViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/ProtestDetailViewDataBuilder.ts @@ -1,4 +1,4 @@ -import { ProtestDetailViewData } from '@/lib/view-data/leagues/ProtestDetailViewData'; +import { ProtestDetailViewData } from '@/lib/view-data/ProtestDetailViewData'; interface ProtestDetailApiDto { id: string; @@ -29,7 +29,14 @@ interface ProtestDetailApiDto { }>; } -export class ProtestDetailViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class ProtestDetailViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return ProtestDetailViewDataBuilder.build(input); + } + + static build( static build(apiDto: ProtestDetailApiDto): ProtestDetailViewData { return { protestId: apiDto.id, diff --git a/apps/website/lib/builders/view-data/RaceDetailViewDataBuilder.ts b/apps/website/lib/builders/view-data/RaceDetailViewDataBuilder.ts index 72d546289..bd34577c6 100644 --- a/apps/website/lib/builders/view-data/RaceDetailViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/RaceDetailViewDataBuilder.ts @@ -1,4 +1,4 @@ -import { RaceDetailViewData, RaceDetailRace, RaceDetailLeague, RaceDetailEntry, RaceDetailRegistration, RaceDetailUserResult } from '@/lib/view-data/races/RaceDetailViewData'; +import { RaceDetailEntry, RaceDetailLeague, RaceDetailRace, RaceDetailRegistration, RaceDetailUserResult, RaceDetailViewData } from '@/lib/view-data/RaceDetailViewData'; /** * Race Detail View Data Builder @@ -6,7 +6,14 @@ import { RaceDetailViewData, RaceDetailRace, RaceDetailLeague, RaceDetailEntry, * Transforms API DTO into ViewData for the race detail template. * Deterministic, side-effect free. */ -export class RaceDetailViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class RaceDetailViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return RaceDetailViewDataBuilder.build(input); + } + + static build( static build(apiDto: any): RaceDetailViewData { if (!apiDto || !apiDto.race) { return { diff --git a/apps/website/lib/builders/view-data/RaceResultsViewDataBuilder.ts b/apps/website/lib/builders/view-data/RaceResultsViewDataBuilder.ts index c74a2159d..9a1d75efc 100644 --- a/apps/website/lib/builders/view-data/RaceResultsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/RaceResultsViewDataBuilder.ts @@ -1,4 +1,4 @@ -import { RaceResultsViewData, RaceResultsResult, RaceResultsPenalty } from '@/lib/view-data/races/RaceResultsViewData'; +import { RaceResultsPenalty, RaceResultsResult, RaceResultsViewData } from '@/lib/view-data/RaceResultsViewData'; /** * Race Results View Data Builder @@ -6,7 +6,14 @@ import { RaceResultsViewData, RaceResultsResult, RaceResultsPenalty } from '@/li * Transforms API DTO into ViewData for the race results template. * Deterministic, side-effect free. */ -export class RaceResultsViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class RaceResultsViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return RaceResultsViewDataBuilder.build(input); + } + + static build( static build(apiDto: unknown): RaceResultsViewData { if (!apiDto) { return { diff --git a/apps/website/lib/builders/view-data/RaceStewardingViewDataBuilder.ts b/apps/website/lib/builders/view-data/RaceStewardingViewDataBuilder.ts index 0e8381a53..e00edb7b2 100644 --- a/apps/website/lib/builders/view-data/RaceStewardingViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/RaceStewardingViewDataBuilder.ts @@ -1,4 +1,4 @@ -import { RaceStewardingViewData, Protest, Penalty, Driver } from '@/lib/view-data/races/RaceStewardingViewData'; +import { Driver, Penalty, Protest, RaceStewardingViewData } from '@/lib/view-data/RaceStewardingViewData'; /** * Race Stewarding View Data Builder @@ -6,7 +6,14 @@ import { RaceStewardingViewData, Protest, Penalty, Driver } from '@/lib/view-dat * Transforms API DTO into ViewData for the race stewarding template. * Deterministic, side-effect free. */ -export class RaceStewardingViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class RaceStewardingViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return RaceStewardingViewDataBuilder.build(input); + } + + static build( static build(apiDto: unknown): RaceStewardingViewData { if (!apiDto) { return { diff --git a/apps/website/lib/builders/view-data/RacesViewDataBuilder.ts b/apps/website/lib/builders/view-data/RacesViewDataBuilder.ts index e78f81f58..aead0ed1c 100644 --- a/apps/website/lib/builders/view-data/RacesViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/RacesViewDataBuilder.ts @@ -4,7 +4,14 @@ import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { RaceStatusDisplay } from '@/lib/display-objects/RaceStatusDisplay'; import { RelativeTimeDisplay } from '@/lib/display-objects/RelativeTimeDisplay'; -export class RacesViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class RacesViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return RacesViewDataBuilder.build(input); + } + + static build( static build(apiDto: RacesPageDataDTO): RacesViewData { const now = new Date(); const races = apiDto.races.map((race): RaceViewData => { diff --git a/apps/website/lib/builders/view-data/ResetPasswordViewDataBuilder.ts b/apps/website/lib/builders/view-data/ResetPasswordViewDataBuilder.ts index e9ac70067..bf0ee0974 100644 --- a/apps/website/lib/builders/view-data/ResetPasswordViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/ResetPasswordViewDataBuilder.ts @@ -6,9 +6,18 @@ */ import { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO'; -import { ResetPasswordViewData } from './types/ResetPasswordViewData'; +import { ResetPasswordViewData } from '../../view-data/ResetPasswordViewData'; +import { ViewData } from '@/lib/contracts/view-data/ViewData'; +import { error } from 'console'; -export class ResetPasswordViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class ResetPasswordViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return ResetPasswordViewDataBuilder.build(input); + } + + static build( static build(apiDto: ResetPasswordPageDTO): ResetPasswordViewData { return { token: apiDto.token, diff --git a/apps/website/lib/builders/view-data/RulebookViewDataBuilder.ts b/apps/website/lib/builders/view-data/RulebookViewDataBuilder.ts index a9a94ab18..16f72afb9 100644 --- a/apps/website/lib/builders/view-data/RulebookViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/RulebookViewDataBuilder.ts @@ -1,7 +1,14 @@ -import { RulebookViewData } from '@/lib/view-data/leagues/RulebookViewData'; import { RulebookApiDto } from '@/lib/types/tbd/RulebookApiDto'; +import { RulebookViewData } from '@/lib/view-data/RulebookViewData'; -export class RulebookViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class RulebookViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return RulebookViewDataBuilder.build(input); + } + + static build( static build(apiDto: RulebookApiDto): RulebookViewData { const primaryChampionship = apiDto.scoringConfig.championships.find(c => c.type === 'driver') ?? apiDto.scoringConfig.championships[0]; const positionPoints: { position: number; points: number }[] = primaryChampionship?.pointsPreview diff --git a/apps/website/lib/builders/view-data/SignupViewDataBuilder.ts b/apps/website/lib/builders/view-data/SignupViewDataBuilder.ts index 1414c0a27..c9348dcf8 100644 --- a/apps/website/lib/builders/view-data/SignupViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/SignupViewDataBuilder.ts @@ -6,9 +6,14 @@ */ import { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO'; -import { SignupViewData } from './types/SignupViewData'; +import { SignupViewData } from '../../view-data/SignupViewData'; +import { ViewDataBuilder } from '../../contracts/builders/ViewDataBuilder'; + +export class SignupViewDataBuilder implements ViewDataBuilder { + build(apiDto: SignupPageDTO): SignupViewData { + return SignupViewDataBuilder.build(apiDto); + } -export class SignupViewDataBuilder { static build(apiDto: SignupPageDTO): SignupViewData { return { returnTo: apiDto.returnTo, diff --git a/apps/website/lib/builders/view-data/SponsorDashboardViewDataBuilder.ts b/apps/website/lib/builders/view-data/SponsorDashboardViewDataBuilder.ts index 4ca46ac60..d98adad6a 100644 --- a/apps/website/lib/builders/view-data/SponsorDashboardViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/SponsorDashboardViewDataBuilder.ts @@ -9,7 +9,14 @@ import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; * Transforms SponsorDashboardDTO into ViewData for templates. * Deterministic and side-effect free. */ -export class SponsorDashboardViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class SponsorDashboardViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return SponsorDashboardViewDataBuilder.build(input); + } + + static build( static build(apiDto: SponsorDashboardDTO): SponsorDashboardViewData { const totalInvestmentValue = apiDto.investment.activeSponsorships * 1000; diff --git a/apps/website/lib/builders/view-data/SponsorLogoViewDataBuilder.ts b/apps/website/lib/builders/view-data/SponsorLogoViewDataBuilder.ts index 0b594a521..159760b3d 100644 --- a/apps/website/lib/builders/view-data/SponsorLogoViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/SponsorLogoViewDataBuilder.ts @@ -8,7 +8,14 @@ import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import { SponsorLogoViewData } from '@/lib/view-data/SponsorLogoViewData'; -export class SponsorLogoViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class SponsorLogoViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return SponsorLogoViewDataBuilder.build(input); + } + + static build( static build(apiDto: MediaBinaryDTO): SponsorLogoViewData { return { buffer: Buffer.from(apiDto.buffer).toString('base64'), diff --git a/apps/website/lib/builders/view-data/SponsorshipRequestsPageViewDataBuilder.ts b/apps/website/lib/builders/view-data/SponsorshipRequestsPageViewDataBuilder.ts index b644a5ff9..52a4bbb2d 100644 --- a/apps/website/lib/builders/view-data/SponsorshipRequestsPageViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/SponsorshipRequestsPageViewDataBuilder.ts @@ -5,7 +5,14 @@ import type { SponsorshipRequestsViewData } from '@/lib/view-data/SponsorshipReq * ViewData Builder for Sponsorship Requests page * Transforms API DTO to ViewData for templates */ -export class SponsorshipRequestsPageViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class SponsorshipRequestsPageViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return SponsorshipRequestsPageViewDataBuilder.build(input); + } + + static build( static build(apiDto: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData { return { sections: [{ diff --git a/apps/website/lib/builders/view-data/SponsorshipRequestsViewDataBuilder.ts b/apps/website/lib/builders/view-data/SponsorshipRequestsViewDataBuilder.ts index 468a4a76e..131c3baa6 100644 --- a/apps/website/lib/builders/view-data/SponsorshipRequestsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/SponsorshipRequestsViewDataBuilder.ts @@ -1,7 +1,14 @@ import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO'; import type { SponsorshipRequestsViewData } from '@/lib/view-data/SponsorshipRequestsViewData'; -export class SponsorshipRequestsViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class SponsorshipRequestsViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return SponsorshipRequestsViewDataBuilder.build(input); + } + + static build( static build(apiDto: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData { return { sections: [ diff --git a/apps/website/lib/builders/view-data/StewardingViewDataBuilder.ts b/apps/website/lib/builders/view-data/StewardingViewDataBuilder.ts index 50300b181..2941f760f 100644 --- a/apps/website/lib/builders/view-data/StewardingViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/StewardingViewDataBuilder.ts @@ -1,8 +1,15 @@ import { StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto'; -import { StewardingViewData } from '@/lib/view-data/leagues/StewardingViewData'; +import { StewardingViewData } from '@/lib/view-data/StewardingViewData'; -export class StewardingViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class StewardingViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return StewardingViewDataBuilder.build(input); + } + + static build( static build(apiDto: StewardingApiDto): StewardingViewData { return { leagueId: apiDto.leagueId, diff --git a/apps/website/lib/builders/view-data/TeamDetailViewDataBuilder.ts b/apps/website/lib/builders/view-data/TeamDetailViewDataBuilder.ts index 3655de1c9..a4da40e6b 100644 --- a/apps/website/lib/builders/view-data/TeamDetailViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/TeamDetailViewDataBuilder.ts @@ -9,7 +9,14 @@ import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; * TeamDetailViewDataBuilder - Transforms TeamDetailPageDto into ViewData * Deterministic; side-effect free; no HTTP calls */ -export class TeamDetailViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class TeamDetailViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return TeamDetailViewDataBuilder.build(input); + } + + static build( static build(apiDto: TeamDetailPageDto): TeamDetailViewData { const team: TeamDetailData = { id: apiDto.team.id, diff --git a/apps/website/lib/builders/view-data/TeamLogoViewDataBuilder.ts b/apps/website/lib/builders/view-data/TeamLogoViewDataBuilder.ts index 596bc9c71..90f00419e 100644 --- a/apps/website/lib/builders/view-data/TeamLogoViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/TeamLogoViewDataBuilder.ts @@ -8,7 +8,14 @@ import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import { TeamLogoViewData } from '@/lib/view-data/TeamLogoViewData'; -export class TeamLogoViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class TeamLogoViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return TeamLogoViewDataBuilder.build(input); + } + + static build( static build(apiDto: MediaBinaryDTO): TeamLogoViewData { return { buffer: Buffer.from(apiDto.buffer).toString('base64'), diff --git a/apps/website/lib/builders/view-data/TeamRankingsViewDataBuilder.ts b/apps/website/lib/builders/view-data/TeamRankingsViewDataBuilder.ts index 0c57cfae9..080d95d8f 100644 --- a/apps/website/lib/builders/view-data/TeamRankingsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/TeamRankingsViewDataBuilder.ts @@ -1,21 +1,18 @@ +import { ViewData } from '@/lib/contracts/view-data/ViewData'; import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData'; -export class TeamRankingsViewDataBuilder { - static build(apiDto: GetTeamsLeaderboardOutputDTO): TeamRankingsViewData { - const allTeams = apiDto.teams.map((team, index) => ({ - id: team.id, - name: team.name, - tag: team.tag, - memberCount: team.memberCount, - category: undefined, - totalWins: team.totalWins || 0, - logoUrl: team.logoUrl || '', - position: index + 1, - isRecruiting: team.isRecruiting, - performanceLevel: team.performanceLevel || 'N/A', - rating: team.rating || 0, - totalRaces: team.totalRaces || 0, +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class TeamRankingsViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return TeamRankingsViewDataBuilder.build(input); + } + + static build( + public static build(apiDto: GetTeamsLeaderboardOutputDTO): TeamRankingsViewData { + const allTeams = apiDto.teams.map(t => ({ + ...t, })); return { diff --git a/apps/website/lib/builders/view-data/TeamsViewDataBuilder.ts b/apps/website/lib/builders/view-data/TeamsViewDataBuilder.ts index e38d42240..496244a27 100644 --- a/apps/website/lib/builders/view-data/TeamsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/TeamsViewDataBuilder.ts @@ -8,7 +8,14 @@ import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; * TeamsViewDataBuilder - Transforms TeamsPageDto into ViewData for TeamsTemplate * Deterministic; side-effect free; no HTTP calls */ -export class TeamsViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class TeamsViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return TeamsViewDataBuilder.build(input); + } + + static build( static build(apiDto: TeamsPageDto): TeamsViewData { const teams: TeamSummaryData[] = apiDto.teams.map((team: TeamListItemDTO): TeamSummaryData => ({ teamId: team.id, diff --git a/apps/website/lib/builders/view-data/TrackImageViewDataBuilder.ts b/apps/website/lib/builders/view-data/TrackImageViewDataBuilder.ts index 648515fa8..64852a5b5 100644 --- a/apps/website/lib/builders/view-data/TrackImageViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/TrackImageViewDataBuilder.ts @@ -8,7 +8,14 @@ import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import { TrackImageViewData } from '@/lib/view-data/TrackImageViewData'; -export class TrackImageViewDataBuilder { +import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; + +export class TrackImageViewDataBuilder implements ViewDataBuilder { + build(input: any): any { + return TrackImageViewDataBuilder.build(input); + } + + static build( static build(apiDto: MediaBinaryDTO): TrackImageViewData { return { buffer: Buffer.from(apiDto.buffer).toString('base64'), diff --git a/apps/website/lib/builders/view-data/types/ForgotPasswordViewData.ts b/apps/website/lib/builders/view-data/types/ForgotPasswordViewData.ts deleted file mode 100644 index 9cd8414c6..000000000 --- a/apps/website/lib/builders/view-data/types/ForgotPasswordViewData.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Forgot Password View Data - * - * ViewData for the forgot password template. - */ - -export interface ForgotPasswordViewData { - returnTo: string; - showSuccess: boolean; - successMessage?: string; - magicLink?: string; - formState: any; // Will be managed by client component - isSubmitting: boolean; - submitError?: string; -} \ No newline at end of file diff --git a/apps/website/lib/builders/view-data/types/LoginViewData.ts b/apps/website/lib/builders/view-data/types/LoginViewData.ts deleted file mode 100644 index 0127b0401..000000000 --- a/apps/website/lib/builders/view-data/types/LoginViewData.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Login View Data - * - * ViewData for the login template. - */ - -import { FormState } from './FormState'; - -export interface LoginViewData { - returnTo: string; - hasInsufficientPermissions: boolean; - showPassword: boolean; - showErrorDetails: boolean; - formState: FormState; - isSubmitting: boolean; - submitError?: string; -} \ No newline at end of file diff --git a/apps/website/lib/builders/view-data/types/ResetPasswordViewData.ts b/apps/website/lib/builders/view-data/types/ResetPasswordViewData.ts deleted file mode 100644 index 66ad83f55..000000000 --- a/apps/website/lib/builders/view-data/types/ResetPasswordViewData.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Reset Password View Data - * - * ViewData for the reset password template. - */ - -export interface ResetPasswordViewData { - token: string; - returnTo: string; - showSuccess: boolean; - successMessage?: string; - formState: any; // Will be managed by client component - isSubmitting: boolean; - submitError?: string; -} \ No newline at end of file diff --git a/apps/website/lib/builders/view-data/types/SignupViewData.ts b/apps/website/lib/builders/view-data/types/SignupViewData.ts deleted file mode 100644 index 80297e48e..000000000 --- a/apps/website/lib/builders/view-data/types/SignupViewData.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Signup View Data - * - * ViewData for the signup template. - */ - -export interface SignupViewData { - returnTo: string; - formState: any; // Will be managed by client component - isSubmitting: boolean; - submitError?: string; -} \ No newline at end of file diff --git a/apps/website/lib/builders/view-models/DriverProfileViewModelBuilder.ts b/apps/website/lib/builders/view-models/DriverProfileViewModelBuilder.ts index 69f0c4c38..097de910d 100644 --- a/apps/website/lib/builders/view-models/DriverProfileViewModelBuilder.ts +++ b/apps/website/lib/builders/view-models/DriverProfileViewModelBuilder.ts @@ -21,7 +21,14 @@ import type { * Transforms GetDriverProfileOutputDTO into DriverProfileViewModel. * Deterministic, side-effect free, no HTTP calls. */ -export class DriverProfileViewModelBuilder { +import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder"; + +export class DriverProfileViewModelBuilder implements ViewModelBuilder { + build(input: any): any { + return DriverProfileViewModelBuilder.build(input); + } + + static build( /** * Build ViewModel from API DTO * diff --git a/apps/website/lib/builders/view-models/DriversViewModelBuilder.ts b/apps/website/lib/builders/view-models/DriversViewModelBuilder.ts index c31dca47d..d789aafac 100644 --- a/apps/website/lib/builders/view-models/DriversViewModelBuilder.ts +++ b/apps/website/lib/builders/view-models/DriversViewModelBuilder.ts @@ -7,7 +7,14 @@ import { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardV * Transforms DriversLeaderboardDTO into DriverLeaderboardViewModel. * Deterministic, side-effect free, no HTTP calls. */ -export class DriversViewModelBuilder { +import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder"; + +export class DriversViewModelBuilder implements ViewModelBuilder { + build(input: any): any { + return DriversViewModelBuilder.build(input); + } + + static build( static build(apiDto: DriversLeaderboardDTO): DriverLeaderboardViewModel { return new DriverLeaderboardViewModel({ drivers: apiDto.drivers, diff --git a/apps/website/lib/builders/view-models/ForgotPasswordViewModelBuilder.ts b/apps/website/lib/builders/view-models/ForgotPasswordViewModelBuilder.ts index ba5fc08d7..2b998b7db 100644 --- a/apps/website/lib/builders/view-models/ForgotPasswordViewModelBuilder.ts +++ b/apps/website/lib/builders/view-models/ForgotPasswordViewModelBuilder.ts @@ -5,10 +5,16 @@ * Deterministic, side-effect free, no business logic. */ -import { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData'; -import { ForgotPasswordViewModel, ForgotPasswordFormState } from '@/lib/view-models/auth/ForgotPasswordViewModel'; +import { ForgotPasswordViewData } from '@/lib/view-data/ForgotPasswordViewData'; +import { ForgotPasswordFormState, ForgotPasswordViewModel } from '@/lib/view-models/auth/ForgotPasswordViewModel'; + +import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder"; + +export class ForgotPasswordViewModelBuilder implements ViewModelBuilder { + build(input: any): any { + return ForgotPasswordViewModelBuilder.build(input); + } -export class ForgotPasswordViewModelBuilder { static build(viewData: ForgotPasswordViewData): ForgotPasswordViewModel { const formState: ForgotPasswordFormState = { fields: { diff --git a/apps/website/lib/builders/view-models/LeagueSummaryViewModelBuilder.ts b/apps/website/lib/builders/view-models/LeagueSummaryViewModelBuilder.ts index 00f51666c..537a6f45b 100644 --- a/apps/website/lib/builders/view-models/LeagueSummaryViewModelBuilder.ts +++ b/apps/website/lib/builders/view-models/LeagueSummaryViewModelBuilder.ts @@ -1,7 +1,13 @@ import { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel'; import { LeaguesViewData } from '@/lib/view-data/LeaguesViewData'; -export class LeagueSummaryViewModelBuilder { +import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder"; + +export class LeagueSummaryViewModelBuilder implements ViewModelBuilder { + build(input: any): any { + return LeagueSummaryViewModelBuilder.build(input); + } + static build(league: LeaguesViewData['leagues'][number]): LeagueSummaryViewModel { return { id: league.id, diff --git a/apps/website/lib/builders/view-models/LoginViewModelBuilder.ts b/apps/website/lib/builders/view-models/LoginViewModelBuilder.ts index 3d60478cb..26db5272f 100644 --- a/apps/website/lib/builders/view-models/LoginViewModelBuilder.ts +++ b/apps/website/lib/builders/view-models/LoginViewModelBuilder.ts @@ -5,10 +5,16 @@ * Deterministic, side-effect free, no business logic. */ -import { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData'; -import { LoginViewModel, LoginFormState, LoginUIState } from '@/lib/view-models/auth/LoginViewModel'; +import { LoginViewData } from '@/lib/view-data/LoginViewData'; +import { LoginFormState, LoginUIState, LoginViewModel } from '@/lib/view-models/auth/LoginViewModel'; + +import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder"; + +export class LoginViewModelBuilder implements ViewModelBuilder { + build(input: any): any { + return LoginViewModelBuilder.build(input); + } -export class LoginViewModelBuilder { static build(viewData: LoginViewData): LoginViewModel { const formState: LoginFormState = { fields: { diff --git a/apps/website/lib/builders/view-models/OnboardingViewModelBuilder.ts b/apps/website/lib/builders/view-models/OnboardingViewModelBuilder.ts index 84901fae4..0afdf1c9b 100644 --- a/apps/website/lib/builders/view-models/OnboardingViewModelBuilder.ts +++ b/apps/website/lib/builders/view-models/OnboardingViewModelBuilder.ts @@ -9,7 +9,14 @@ import { Result } from '@/lib/contracts/Result'; import { DomainError } from '@/lib/contracts/services/Service'; import { OnboardingViewModel } from '@/lib/view-models/OnboardingViewModel'; -export class OnboardingViewModelBuilder { +import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder"; + +export class OnboardingViewModelBuilder implements ViewModelBuilder { + build(input: any): any { + return OnboardingViewModelBuilder.build(input); + } + + static build( static build(apiDto: { isAlreadyOnboarded: boolean }): Result { try { return Result.ok({ diff --git a/apps/website/lib/builders/view-models/ResetPasswordViewModelBuilder.ts b/apps/website/lib/builders/view-models/ResetPasswordViewModelBuilder.ts index 2d13085a5..2a1f64ccc 100644 --- a/apps/website/lib/builders/view-models/ResetPasswordViewModelBuilder.ts +++ b/apps/website/lib/builders/view-models/ResetPasswordViewModelBuilder.ts @@ -5,10 +5,17 @@ * Deterministic, side-effect free, no business logic. */ -import { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData'; -import { ResetPasswordViewModel, ResetPasswordFormState, ResetPasswordUIState } from '@/lib/view-models/auth/ResetPasswordViewModel'; +import { ResetPasswordViewData } from '@/lib/view-data/ResetPasswordViewData'; +import { ResetPasswordFormState, ResetPasswordUIState, ResetPasswordViewModel } from '@/lib/view-models/auth/ResetPasswordViewModel'; -export class ResetPasswordViewModelBuilder { +import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder"; + +export class ResetPasswordViewModelBuilder implements ViewModelBuilder { + build(input: any): any { + return ResetPasswordViewModelBuilder.build(input); + } + + static build( static build(viewData: ResetPasswordViewData): ResetPasswordViewModel { const formState: ResetPasswordFormState = { fields: { diff --git a/apps/website/lib/builders/view-models/SignupViewModelBuilder.ts b/apps/website/lib/builders/view-models/SignupViewModelBuilder.ts index 9f397a740..966e85999 100644 --- a/apps/website/lib/builders/view-models/SignupViewModelBuilder.ts +++ b/apps/website/lib/builders/view-models/SignupViewModelBuilder.ts @@ -5,10 +5,17 @@ * Deterministic, side-effect free, no business logic. */ -import { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData'; -import { SignupViewModel, SignupFormState, SignupUIState } from '@/lib/view-models/auth/SignupViewModel'; +import { SignupViewData } from '@/lib/view-data/SignupViewData'; +import { SignupFormState, SignupUIState, SignupViewModel } from '@/lib/view-models/auth/SignupViewModel'; -export class SignupViewModelBuilder { +import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder"; + +export class SignupViewModelBuilder implements ViewModelBuilder { + build(input: any): any { + return SignupViewModelBuilder.build(input); + } + + static build( static build(viewData: SignupViewData): SignupViewModel { const formState: SignupFormState = { fields: { diff --git a/apps/website/lib/display-objects/AvatarDisplay.ts b/apps/website/lib/display-objects/AvatarDisplay.ts index c915bb998..64ec4c7b8 100644 --- a/apps/website/lib/display-objects/AvatarDisplay.ts +++ b/apps/website/lib/display-objects/AvatarDisplay.ts @@ -19,9 +19,10 @@ export class AvatarDisplay { /** * Determines if avatar data is valid for display. + * Accepts base64-encoded string buffer. */ - static hasValidData(buffer: ArrayBuffer, contentType: string): boolean { - return buffer.byteLength > 0 && contentType.length > 0; + static hasValidData(buffer: string, contentType: string): boolean { + return buffer.length > 0 && contentType.length > 0; } /** diff --git a/apps/website/lib/display-objects/LeagueCreationStatusDisplay.ts b/apps/website/lib/display-objects/LeagueCreationStatusDisplay.ts new file mode 100644 index 000000000..c587f36d4 --- /dev/null +++ b/apps/website/lib/display-objects/LeagueCreationStatusDisplay.ts @@ -0,0 +1,14 @@ +/** + * LeagueCreationStatusDisplay + * + * Deterministic mapping of league creation status to display messages. + */ + +export class LeagueCreationStatusDisplay { + /** + * Maps league creation success status to display message. + */ + static statusMessage(success: boolean): string { + return success ? 'League created successfully!' : 'Failed to create league.'; + } +} diff --git a/apps/website/lib/display-objects/RatingTrendDisplay.ts b/apps/website/lib/display-objects/RatingTrendDisplay.ts new file mode 100644 index 000000000..54b0614a2 --- /dev/null +++ b/apps/website/lib/display-objects/RatingTrendDisplay.ts @@ -0,0 +1,15 @@ +export class RatingTrendDisplay { + static getTrend(currentRating: number, previousRating: number | undefined): 'up' | 'down' | 'same' { + if (!previousRating) return 'same'; + if (currentRating > previousRating) return 'up'; + if (currentRating < previousRating) return 'down'; + return 'same'; + } + + static getChangeIndicator(currentRating: number, previousRating: number | undefined): string { + const change = previousRating ? currentRating - previousRating : 0; + if (change > 0) return `+${change}`; + if (change < 0) return `${change}`; + return '0'; + } +} diff --git a/apps/website/lib/display-objects/SkillLevelIconDisplay.ts b/apps/website/lib/display-objects/SkillLevelIconDisplay.ts new file mode 100644 index 000000000..7311b213b --- /dev/null +++ b/apps/website/lib/display-objects/SkillLevelIconDisplay.ts @@ -0,0 +1,11 @@ +export class SkillLevelIconDisplay { + static getIcon(skillLevel: string): string { + const icons: Record = { + beginner: '🥉', + intermediate: '🥈', + advanced: '🥇', + expert: '👑', + }; + return icons[skillLevel] || '🏁'; + } +} diff --git a/apps/website/lib/display-objects/TeamCreationStatusDisplay.ts b/apps/website/lib/display-objects/TeamCreationStatusDisplay.ts new file mode 100644 index 000000000..13638e188 --- /dev/null +++ b/apps/website/lib/display-objects/TeamCreationStatusDisplay.ts @@ -0,0 +1,14 @@ +/** + * TeamCreationStatusDisplay + * + * Deterministic mapping of team creation status to display messages. + */ + +export class TeamCreationStatusDisplay { + /** + * Maps team creation success status to display message. + */ + static statusMessage(success: boolean): string { + return success ? 'Team created successfully!' : 'Failed to create team.'; + } +} diff --git a/apps/website/lib/types/tbd/AvailableLeaguesDTO.ts b/apps/website/lib/types/tbd/AvailableLeaguesDTO.ts index 00c79cafc..055f34924 100644 --- a/apps/website/lib/types/tbd/AvailableLeaguesDTO.ts +++ b/apps/website/lib/types/tbd/AvailableLeaguesDTO.ts @@ -5,19 +5,21 @@ export interface AvailableLeaguesDTO { export interface AvailableLeagueDTO { id: string; name: string; + game: string; description: string; drivers: number; + avgViewsPerRace: number; mainSponsorSlot: { available: boolean; price: number; }; secondarySlots: { available: number; + total: number; price: number; }; - cpm: number; - season: { - startDate: string; - endDate: string; - }; + rating: number; + tier: 'premium' | 'standard' | 'starter'; + nextRace?: string; + seasonStatus: 'active' | 'upcoming' | 'completed'; } \ No newline at end of file diff --git a/apps/website/lib/view-data/AnalyticsDashboardInputViewData.ts b/apps/website/lib/view-data/AnalyticsDashboardInputViewData.ts new file mode 100644 index 000000000..870016ad0 --- /dev/null +++ b/apps/website/lib/view-data/AnalyticsDashboardInputViewData.ts @@ -0,0 +1,8 @@ +import { ViewData } from "../contracts/view-data/ViewData"; + +export interface AnalyticsDashboardInputViewData extends ViewData { + totalUsers: number; + activeUsers: number; + totalRaces: number; + totalLeagues: number; +} diff --git a/apps/website/lib/view-data/AnalyticsMetricsViewData.ts b/apps/website/lib/view-data/AnalyticsMetricsViewData.ts new file mode 100644 index 000000000..dbdcecfd5 --- /dev/null +++ b/apps/website/lib/view-data/AnalyticsMetricsViewData.ts @@ -0,0 +1,8 @@ +import { ViewData } from "../contracts/view-data/ViewData"; + +export interface AnalyticsMetricsViewData extends ViewData { + pageViews: number; + uniqueVisitors: number; + averageSessionDuration: number; + bounceRate: number; +} diff --git a/apps/website/lib/view-data/BillingViewData.ts b/apps/website/lib/view-data/BillingViewData.ts new file mode 100644 index 000000000..0eec76d6f --- /dev/null +++ b/apps/website/lib/view-data/BillingViewData.ts @@ -0,0 +1,47 @@ +import { ViewData } from "../contracts/view-data/ViewData"; + + +export interface BillingViewData extends ViewData { + paymentMethods: Array<{ + id: string; + type: 'card' | 'bank' | 'sepa'; + last4: string; + brand?: string; + isDefault: boolean; + expiryMonth?: number; + expiryYear?: number; + bankName?: string; + displayLabel: string; + expiryDisplay: string | null; + }>; + invoices: Array<{ + id: string; + invoiceNumber: string; + date: string; + dueDate: string; + amount: number; + vatAmount: number; + totalAmount: number; + status: 'paid' | 'pending' | 'overdue' | 'failed'; + description: string; + sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform'; + pdfUrl: string; + formattedTotalAmount: string; + formattedVatAmount: string; + formattedDate: string; + isOverdue: boolean; + }>; + stats: { + totalSpent: number; + pendingAmount: number; + nextPaymentDate: string; + nextPaymentAmount: number; + activeSponsorships: number; + averageMonthlySpend: number; + formattedTotalSpent: string; + formattedPendingAmount: string; + formattedNextPaymentAmount: string; + formattedAverageMonthlySpend: string; + formattedNextPaymentDate: string; + }; +} diff --git a/apps/website/lib/view-data/CreateTeamViewData.ts b/apps/website/lib/view-data/CreateTeamViewData.ts new file mode 100644 index 000000000..1c472ffe1 --- /dev/null +++ b/apps/website/lib/view-data/CreateTeamViewData.ts @@ -0,0 +1,14 @@ +import { ViewData } from '../contracts/view-data/ViewData'; + +/** + * CreateTeamViewData + * + * ViewData for the create team result page. + * Contains only raw serializable data, no methods or computed properties + */ + +export interface CreateTeamViewData extends ViewData { + teamId: string; + success: boolean; + successMessage: string; +} diff --git a/apps/website/lib/view-data/DashboardStatsViewData.ts b/apps/website/lib/view-data/DashboardStatsViewData.ts new file mode 100644 index 000000000..5bf126c42 --- /dev/null +++ b/apps/website/lib/view-data/DashboardStatsViewData.ts @@ -0,0 +1,38 @@ +import { ViewData } from '@/lib/contracts/view-data/ViewData'; + +/** + * DashboardStatsViewData + * + * ViewData for DashboardStatsViewModel. + * Template-ready data structure with only primitives. + */ + +export interface DashboardStatsViewData extends ViewData { + totalUsers: number; + activeUsers: number; + suspendedUsers: number; + deletedUsers: number; + systemAdmins: number; + recentLogins: number; + newUsersToday: number; + userGrowth: { + label: string; + value: number; + color: string; + }[]; + roleDistribution: { + label: string; + value: number; + color: string; + }[]; + statusDistribution: { + active: number; + suspended: number; + deleted: number; + }; + activityTimeline: { + date: string; + newUsers: number; + logins: number; + }[]; +} diff --git a/apps/website/lib/view-models/ActivityItemViewModel.test.ts b/apps/website/lib/view-models/ActivityItemViewModel.test.ts index f186a1335..d2dbecdb9 100644 --- a/apps/website/lib/view-models/ActivityItemViewModel.test.ts +++ b/apps/website/lib/view-models/ActivityItemViewModel.test.ts @@ -1,17 +1,18 @@ import { describe, it, expect } from 'vitest'; import { ActivityItemViewModel } from './ActivityItemViewModel'; +import { ActivityItemViewData } from '../view-data/ActivityItemViewData'; describe('ActivityItemViewModel', () => { - it('maps basic properties from input data', () => { - const data = { + it('maps basic properties from ActivityItemViewData', () => { + const viewData: ActivityItemViewData = { id: 'activity-1', - type: 'race' as const, + type: 'race', message: 'Test activity', time: '2025-01-01T12:00:00Z', impressions: 1234, }; - const viewModel = new ActivityItemViewModel(data); + const viewModel = new ActivityItemViewModel(viewData); expect(viewModel.id).toBe('activity-1'); expect(viewModel.type).toBe('race'); @@ -40,7 +41,7 @@ describe('ActivityItemViewModel', () => { type: 'unknown', message: '', time: '', - } as any); + }); expect(unknown.typeColor).toBe('bg-gray-500'); }); @@ -77,4 +78,19 @@ describe('ActivityItemViewModel', () => { expect(noImpressions.formattedImpressions).toBeNull(); expect(zeroImpressions.formattedImpressions).toBeNull(); }); + + it('handles optional impressions field', () => { + const withoutImpressions: ActivityItemViewData = { + id: 'activity-5', + type: 'platform', + message: 'Platform activity', + time: '2025-01-01T12:00:00Z', + }; + + const viewModel = new ActivityItemViewModel(withoutImpressions); + + expect(viewModel.impressions).toBeUndefined(); + expect(viewModel.formattedImpressions).toBeNull(); + }); + }); diff --git a/apps/website/lib/view-models/ActivityItemViewModel.ts b/apps/website/lib/view-models/ActivityItemViewModel.ts index 22ab385b2..52678f4f0 100644 --- a/apps/website/lib/view-models/ActivityItemViewModel.ts +++ b/apps/website/lib/view-models/ActivityItemViewModel.ts @@ -2,24 +2,30 @@ * Activity Item View Model * * View model for recent activity items. + * + * Accepts ActivityItemViewData as input and produces UI-ready data. */ -export class ActivityItemViewModel { - id: string; - type: 'race' | 'league' | 'team' | 'driver' | 'platform'; - message: string; - time: string; - impressions?: number; +import { ActivityItemViewData } from "../view-data/ActivityItemViewData"; +import { ViewModel } from "../contracts/view-models/ViewModel"; - constructor(data: any) { - this.id = data.id; - this.type = data.type; - this.message = data.message; - this.time = data.time; - this.impressions = data.impressions; +export class ActivityItemViewModel extends ViewModel { + readonly id: string; + readonly type: string; + readonly message: string; + readonly time: string; + readonly impressions?: number; + + constructor(viewData: ActivityItemViewData) { + super(); + this.id = viewData.id; + this.type = viewData.type; + this.message = viewData.message; + this.time = viewData.time; + this.impressions = viewData.impressions; } get typeColor(): string { - const colors = { + const colors: Record = { race: 'bg-warning-amber', league: 'bg-primary-blue', team: 'bg-purple-400', diff --git a/apps/website/lib/view-models/AdminUserViewModel.test.ts b/apps/website/lib/view-models/AdminUserViewModel.test.ts index 348f54e6d..17a80197d 100644 --- a/apps/website/lib/view-models/AdminUserViewModel.test.ts +++ b/apps/website/lib/view-models/AdminUserViewModel.test.ts @@ -1,24 +1,25 @@ import { describe, it, expect } from 'vitest'; import { AdminUserViewModel, DashboardStatsViewModel, UserListViewModel } from './AdminUserViewModel'; -import type { UserDto, DashboardStats } from '@/lib/api/admin/AdminApiClient'; +import type { AdminUserViewData } from '@/lib/view-data/AdminUserViewData'; +import type { DashboardStatsViewData } from '@/lib/view-data/DashboardStatsViewData'; describe('AdminUserViewModel', () => { - const createBaseDto = (): UserDto => ({ + const createBaseViewData = (): AdminUserViewData => ({ id: 'user-123', email: 'test@example.com', displayName: 'Test User', roles: ['user'], status: 'active', isSystemAdmin: false, - createdAt: new Date('2024-01-01T00:00:00Z'), - updatedAt: new Date('2024-01-02T00:00:00Z'), - lastLoginAt: new Date('2024-01-15T10:30:00Z'), + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-02T00:00:00Z', + lastLoginAt: '2024-01-15T10:30:00Z', primaryDriverId: 'driver-456', }); - it('maps core fields from DTO', () => { - const dto = createBaseDto(); - const vm = new AdminUserViewModel(dto); + it('maps core fields from ViewData', () => { + const viewData = createBaseViewData(); + const vm = new AdminUserViewModel(viewData); expect(vm.id).toBe('user-123'); expect(vm.email).toBe('test@example.com'); @@ -30,8 +31,8 @@ describe('AdminUserViewModel', () => { }); it('converts dates to Date objects', () => { - const dto = createBaseDto(); - const vm = new AdminUserViewModel(dto); + const viewData = createBaseViewData(); + const vm = new AdminUserViewModel(viewData); expect(vm.createdAt).toBeInstanceOf(Date); expect(vm.updatedAt).toBeInstanceOf(Date); @@ -40,19 +41,19 @@ describe('AdminUserViewModel', () => { }); it('handles missing lastLoginAt', () => { - const dto = createBaseDto(); - delete dto.lastLoginAt; - const vm = new AdminUserViewModel(dto); + const viewData = createBaseViewData(); + delete viewData.lastLoginAt; + const vm = new AdminUserViewModel(viewData); expect(vm.lastLoginAt).toBeUndefined(); expect(vm.lastLoginFormatted).toBe('Never'); }); it('formats role badges correctly', () => { - const owner = new AdminUserViewModel({ ...createBaseDto(), roles: ['owner'] }); - const admin = new AdminUserViewModel({ ...createBaseDto(), roles: ['admin'] }); - const user = new AdminUserViewModel({ ...createBaseDto(), roles: ['user'] }); - const custom = new AdminUserViewModel({ ...createBaseDto(), roles: ['custom-role'] }); + const owner = new AdminUserViewModel({ ...createBaseViewData(), roles: ['owner'] }); + const admin = new AdminUserViewModel({ ...createBaseViewData(), roles: ['admin'] }); + const user = new AdminUserViewModel({ ...createBaseViewData(), roles: ['user'] }); + const custom = new AdminUserViewModel({ ...createBaseViewData(), roles: ['custom-role'] }); expect(owner.roleBadges).toEqual(['Owner']); expect(admin.roleBadges).toEqual(['Admin']); @@ -61,51 +62,36 @@ describe('AdminUserViewModel', () => { }); it('derives status badge correctly', () => { - const active = new AdminUserViewModel({ ...createBaseDto(), status: 'active' }); - const suspended = new AdminUserViewModel({ ...createBaseDto(), status: 'suspended' }); - const deleted = new AdminUserViewModel({ ...createBaseDto(), status: 'deleted' }); + const active = new AdminUserViewModel({ ...createBaseViewData(), status: 'active' }); + const suspended = new AdminUserViewModel({ ...createBaseViewData(), status: 'suspended' }); + const deleted = new AdminUserViewModel({ ...createBaseViewData(), status: 'deleted' }); - expect(active.statusBadge).toEqual({ label: 'Active', variant: 'performance-green' }); - expect(suspended.statusBadge).toEqual({ label: 'Suspended', variant: 'yellow-500' }); - expect(deleted.statusBadge).toEqual({ label: 'Deleted', variant: 'racing-red' }); + expect(active.statusBadgeLabel).toBe('Active'); + expect(active.statusBadgeVariant).toBe('performance-green'); + expect(suspended.statusBadgeLabel).toBe('Suspended'); + expect(suspended.statusBadgeVariant).toBe('yellow-500'); + expect(deleted.statusBadgeLabel).toBe('Deleted'); + expect(deleted.statusBadgeVariant).toBe('racing-red'); }); it('formats dates for display', () => { - const dto = createBaseDto(); - const vm = new AdminUserViewModel(dto); + const viewData = createBaseViewData(); + const vm = new AdminUserViewModel(viewData); - expect(vm.lastLoginFormatted).toBe('1/15/2024'); - expect(vm.createdAtFormatted).toBe('1/1/2024'); - }); - - it('derives action permissions correctly', () => { - const active = new AdminUserViewModel({ ...createBaseDto(), status: 'active' }); - const suspended = new AdminUserViewModel({ ...createBaseDto(), status: 'suspended' }); - const deleted = new AdminUserViewModel({ ...createBaseDto(), status: 'deleted' }); - - expect(active.canSuspend).toBe(true); - expect(active.canActivate).toBe(false); - expect(active.canDelete).toBe(true); - - expect(suspended.canSuspend).toBe(false); - expect(suspended.canActivate).toBe(true); - expect(suspended.canDelete).toBe(true); - - expect(deleted.canSuspend).toBe(false); - expect(deleted.canActivate).toBe(false); - expect(deleted.canDelete).toBe(false); + expect(vm.lastLoginFormatted).toBe('Jan 15, 2024'); + expect(vm.createdAtFormatted).toBe('Jan 1, 2024'); }); it('handles multiple roles', () => { - const dto = { ...createBaseDto(), roles: ['owner', 'admin'] }; - const vm = new AdminUserViewModel(dto); + const viewData = { ...createBaseViewData(), roles: ['owner', 'admin'] }; + const vm = new AdminUserViewModel(viewData); expect(vm.roleBadges).toEqual(['Owner', 'Admin']); }); }); describe('DashboardStatsViewModel', () => { - const createBaseData = (): DashboardStats => ({ + const createBaseData = (): DashboardStatsViewData => ({ totalUsers: 100, activeUsers: 70, suspendedUsers: 10, @@ -165,21 +151,24 @@ describe('DashboardStatsViewModel', () => { totalUsers: 100, recentLogins: 10, // 10% engagement }); - expect(lowEngagement.activityLevel).toBe('low'); + expect(lowEngagement.activityLevelLabel).toBe('Low'); + expect(lowEngagement.activityLevelValue).toBe('low'); const mediumEngagement = new DashboardStatsViewModel({ ...createBaseData(), totalUsers: 100, recentLogins: 35, // 35% engagement }); - expect(mediumEngagement.activityLevel).toBe('medium'); + expect(mediumEngagement.activityLevelLabel).toBe('Medium'); + expect(mediumEngagement.activityLevelValue).toBe('medium'); const highEngagement = new DashboardStatsViewModel({ ...createBaseData(), totalUsers: 100, recentLogins: 60, // 60% engagement }); - expect(highEngagement.activityLevel).toBe('high'); + expect(highEngagement.activityLevelLabel).toBe('High'); + expect(highEngagement.activityLevelValue).toBe('high'); }); it('handles zero users safely', () => { @@ -194,7 +183,8 @@ describe('DashboardStatsViewModel', () => { expect(vm.activeRate).toBe(0); expect(vm.activeRateFormatted).toBe('0%'); expect(vm.adminRatio).toBe('1:1'); - expect(vm.activityLevel).toBe('low'); + expect(vm.activityLevelLabel).toBe('Low'); + expect(vm.activityLevelValue).toBe('low'); }); it('preserves arrays from input', () => { @@ -208,21 +198,21 @@ describe('DashboardStatsViewModel', () => { }); describe('UserListViewModel', () => { - const createDto = (overrides: Partial = {}): UserDto => ({ + const createViewData = (overrides: Partial = {}): AdminUserViewData => ({ id: 'user-1', email: 'test@example.com', displayName: 'Test User', roles: ['user'], status: 'active', isSystemAdmin: false, - createdAt: new Date('2024-01-01'), - updatedAt: new Date('2024-01-02'), + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-02T00:00:00Z', ...overrides, }); - it('wraps user DTOs in AdminUserViewModel instances', () => { + it('wraps user ViewData in AdminUserViewModel instances', () => { const data = { - users: [createDto({ id: 'user-1' }), createDto({ id: 'user-2' })], + users: [createViewData({ id: 'user-1' }), createViewData({ id: 'user-2' })], total: 2, page: 1, limit: 10, @@ -239,7 +229,7 @@ describe('UserListViewModel', () => { it('exposes pagination metadata', () => { const data = { - users: [createDto()], + users: [createViewData()], total: 50, page: 2, limit: 10, @@ -256,7 +246,7 @@ describe('UserListViewModel', () => { it('derives hasUsers correctly', () => { const withUsers = new UserListViewModel({ - users: [createDto()], + users: [createViewData()], total: 1, page: 1, limit: 10, @@ -277,7 +267,7 @@ describe('UserListViewModel', () => { it('derives showPagination correctly', () => { const withPagination = new UserListViewModel({ - users: [createDto()], + users: [createViewData()], total: 20, page: 1, limit: 10, @@ -285,7 +275,7 @@ describe('UserListViewModel', () => { }); const withoutPagination = new UserListViewModel({ - users: [createDto()], + users: [createViewData()], total: 5, page: 1, limit: 10, @@ -298,7 +288,7 @@ describe('UserListViewModel', () => { it('calculates start and end indices correctly', () => { const vm = new UserListViewModel({ - users: [createDto(), createDto(), createDto()], + users: [createViewData(), createViewData(), createViewData()], total: 50, page: 2, limit: 10, diff --git a/apps/website/lib/view-models/AdminUserViewModel.ts b/apps/website/lib/view-models/AdminUserViewModel.ts index 9ab86a9ef..a0c9a8b3a 100644 --- a/apps/website/lib/view-models/AdminUserViewModel.ts +++ b/apps/website/lib/view-models/AdminUserViewModel.ts @@ -1,12 +1,18 @@ -import type { UserDto } from '@/lib/types/admin'; +import type { AdminUserViewData } from '@/lib/view-data/AdminUserViewData'; +import type { DashboardStatsViewData } from '@/lib/view-data/DashboardStatsViewData'; +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { UserStatusDisplay } from "../display-objects/UserStatusDisplay"; +import { UserRoleDisplay } from "../display-objects/UserRoleDisplay"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import { ActivityLevelDisplay } from "../display-objects/ActivityLevelDisplay"; /** * AdminUserViewModel - * + * * View Model for admin user management. * Transforms API DTO into UI-ready state with formatting and derived fields. */ -export class AdminUserViewModel { +export class AdminUserViewModel extends ViewModel { id: string; email: string; displayName: string; @@ -18,73 +24,48 @@ export class AdminUserViewModel { lastLoginAt?: Date; primaryDriverId?: string; - // UI-specific derived fields + // UI-specific derived fields (primitive outputs only) readonly roleBadges: string[]; - readonly statusBadge: { label: string; variant: string }; + readonly statusBadgeLabel: string; + readonly statusBadgeVariant: string; readonly lastLoginFormatted: string; readonly createdAtFormatted: string; - readonly canSuspend: boolean; - readonly canActivate: boolean; - readonly canDelete: boolean; - constructor(dto: UserDto) { - this.id = dto.id; - this.email = dto.email; - this.displayName = dto.displayName; - this.roles = dto.roles; - this.status = dto.status; - this.isSystemAdmin = dto.isSystemAdmin; - this.createdAt = new Date(dto.createdAt); - this.updatedAt = new Date(dto.updatedAt); - this.lastLoginAt = dto.lastLoginAt ? new Date(dto.lastLoginAt) : undefined; - this.primaryDriverId = dto.primaryDriverId; + constructor(viewData: AdminUserViewData) { + super(); + this.id = viewData.id; + this.email = viewData.email; + this.displayName = viewData.displayName; + this.roles = viewData.roles; + this.status = viewData.status; + this.isSystemAdmin = viewData.isSystemAdmin; + this.createdAt = new Date(viewData.createdAt); + this.updatedAt = new Date(viewData.updatedAt); + this.lastLoginAt = viewData.lastLoginAt ? new Date(viewData.lastLoginAt) : undefined; + this.primaryDriverId = viewData.primaryDriverId; - // Derive role badges - this.roleBadges = this.roles.map(role => { - switch (role) { - case 'owner': return 'Owner'; - case 'admin': return 'Admin'; - case 'user': return 'User'; - default: return role; - } - }); + // Derive role badges using Display Object + this.roleBadges = this.roles.map(role => UserRoleDisplay.roleLabel(role)); - // Derive status badge - this.statusBadge = this.getStatusBadge(); + // Derive status badge using Display Object + this.statusBadgeLabel = UserStatusDisplay.statusLabel(this.status); + this.statusBadgeVariant = UserStatusDisplay.statusVariant(this.status); - // Format dates - this.lastLoginFormatted = this.lastLoginAt - ? this.lastLoginAt.toLocaleDateString() + // Format dates using Display Object + this.lastLoginFormatted = this.lastLoginAt + ? DateDisplay.formatShort(this.lastLoginAt) : 'Never'; - this.createdAtFormatted = this.createdAt.toLocaleDateString(); - - // Derive action permissions - this.canSuspend = this.status === 'active'; - this.canActivate = this.status === 'suspended'; - this.canDelete = this.status !== 'deleted'; - } - - private getStatusBadge(): { label: string; variant: string } { - switch (this.status) { - case 'active': - return { label: 'Active', variant: 'performance-green' }; - case 'suspended': - return { label: 'Suspended', variant: 'yellow-500' }; - case 'deleted': - return { label: 'Deleted', variant: 'racing-red' }; - default: - return { label: this.status, variant: 'gray-500' }; - } + this.createdAtFormatted = DateDisplay.formatShort(this.createdAt); } } /** * DashboardStatsViewModel - * + * * View Model for admin dashboard statistics. * Provides formatted statistics and derived metrics for UI. */ -export class DashboardStatsViewModel { +export class DashboardStatsViewModel extends ViewModel { totalUsers: number; activeUsers: number; suspendedUsers: number; @@ -113,52 +94,26 @@ export class DashboardStatsViewModel { logins: number; }[]; - // UI-specific derived fields + // UI-specific derived fields (primitive outputs only) readonly activeRate: number; readonly activeRateFormatted: string; readonly adminRatio: string; - readonly activityLevel: 'low' | 'medium' | 'high'; + readonly activityLevelLabel: string; + readonly activityLevelValue: 'low' | 'medium' | 'high'; - constructor(data: { - totalUsers: number; - activeUsers: number; - suspendedUsers: number; - deletedUsers: number; - systemAdmins: number; - recentLogins: number; - newUsersToday: number; - userGrowth: { - label: string; - value: number; - color: string; - }[]; - roleDistribution: { - label: string; - value: number; - color: string; - }[]; - statusDistribution: { - active: number; - suspended: number; - deleted: number; - }; - activityTimeline: { - date: string; - newUsers: number; - logins: number; - }[]; - }) { - this.totalUsers = data.totalUsers; - this.activeUsers = data.activeUsers; - this.suspendedUsers = data.suspendedUsers; - this.deletedUsers = data.deletedUsers; - this.systemAdmins = data.systemAdmins; - this.recentLogins = data.recentLogins; - this.newUsersToday = data.newUsersToday; - this.userGrowth = data.userGrowth; - this.roleDistribution = data.roleDistribution; - this.statusDistribution = data.statusDistribution; - this.activityTimeline = data.activityTimeline; + constructor(viewData: DashboardStatsViewData) { + super(); + this.totalUsers = viewData.totalUsers; + this.activeUsers = viewData.activeUsers; + this.suspendedUsers = viewData.suspendedUsers; + this.deletedUsers = viewData.deletedUsers; + this.systemAdmins = viewData.systemAdmins; + this.recentLogins = viewData.recentLogins; + this.newUsersToday = viewData.newUsersToday; + this.userGrowth = viewData.userGrowth; + this.roleDistribution = viewData.roleDistribution; + this.statusDistribution = viewData.statusDistribution; + this.activityTimeline = viewData.activityTimeline; // Derive active rate this.activeRate = this.totalUsers > 0 ? (this.activeUsers / this.totalUsers) * 100 : 0; @@ -168,44 +123,40 @@ export class DashboardStatsViewModel { const nonAdmins = Math.max(1, this.totalUsers - this.systemAdmins); this.adminRatio = `1:${Math.floor(nonAdmins / Math.max(1, this.systemAdmins))}`; - // Derive activity level + // Derive activity level using Display Object const engagementRate = this.totalUsers > 0 ? (this.recentLogins / this.totalUsers) * 100 : 0; - if (engagementRate < 20) { - this.activityLevel = 'low'; - } else if (engagementRate < 50) { - this.activityLevel = 'medium'; - } else { - this.activityLevel = 'high'; - } + this.activityLevelLabel = ActivityLevelDisplay.levelLabel(engagementRate); + this.activityLevelValue = ActivityLevelDisplay.levelValue(engagementRate); } } /** * UserListViewModel - * + * * View Model for user list with pagination and filtering state. */ -export class UserListViewModel { +export class UserListViewModel extends ViewModel { users: AdminUserViewModel[]; total: number; page: number; limit: number; totalPages: number; - // UI-specific derived fields + // UI-specific derived fields (primitive outputs only) readonly hasUsers: boolean; readonly showPagination: boolean; readonly startIndex: number; readonly endIndex: number; constructor(data: { - users: UserDto[]; + users: AdminUserViewData[]; total: number; page: number; limit: number; totalPages: number; }) { - this.users = data.users.map(dto => new AdminUserViewModel(dto)); + super(); + this.users = data.users.map(viewData => new AdminUserViewModel(viewData)); this.total = data.total; this.page = data.page; this.limit = data.limit; diff --git a/apps/website/lib/view-models/AnalyticsDashboardViewModel.test.ts b/apps/website/lib/view-models/AnalyticsDashboardViewModel.test.ts index 6a8bb8699..12a3837c6 100644 --- a/apps/website/lib/view-models/AnalyticsDashboardViewModel.test.ts +++ b/apps/website/lib/view-models/AnalyticsDashboardViewModel.test.ts @@ -1,14 +1,17 @@ import { describe, it, expect } from 'vitest'; import { AnalyticsDashboardViewModel } from './AnalyticsDashboardViewModel'; +import { AnalyticsDashboardInputViewData } from '../view-data/AnalyticsDashboardInputViewData'; describe('AnalyticsDashboardViewModel', () => { - it('maps core fields from data', () => { - const vm = new AnalyticsDashboardViewModel({ + it('maps core fields from AnalyticsDashboardInputViewData', () => { + const viewData: AnalyticsDashboardInputViewData = { totalUsers: 100, activeUsers: 40, totalRaces: 10, totalLeagues: 5, - }); + }; + + const vm = new AnalyticsDashboardViewModel(viewData); expect(vm.totalUsers).toBe(100); expect(vm.activeUsers).toBe(40); @@ -17,24 +20,28 @@ describe('AnalyticsDashboardViewModel', () => { }); it('computes engagement rate and formatted engagement rate', () => { - const vm = new AnalyticsDashboardViewModel({ + const viewData: AnalyticsDashboardInputViewData = { totalUsers: 200, activeUsers: 50, totalRaces: 0, totalLeagues: 0, - }); + }; + + const vm = new AnalyticsDashboardViewModel(viewData); expect(vm.userEngagementRate).toBeCloseTo(25); expect(vm.formattedEngagementRate).toBe('25.0%'); }); it('handles zero users safely', () => { - const vm = new AnalyticsDashboardViewModel({ + const viewData: AnalyticsDashboardInputViewData = { totalUsers: 0, activeUsers: 0, totalRaces: 0, totalLeagues: 0, - }); + }; + + const vm = new AnalyticsDashboardViewModel(viewData); expect(vm.userEngagementRate).toBe(0); expect(vm.formattedEngagementRate).toBe('0.0%'); diff --git a/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts b/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts index 8d20aadfa..c69a896a8 100644 --- a/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts +++ b/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts @@ -1,20 +1,25 @@ /** * Analytics dashboard view model - * Represents dashboard data for analytics * - * Note: No matching generated DTO available yet + * View model for analytics dashboard data. + * + * Accepts AnalyticsDashboardInputViewData as input and produces UI-ready data. */ -export class AnalyticsDashboardViewModel { - totalUsers: number; - activeUsers: number; - totalRaces: number; - totalLeagues: number; +import { AnalyticsDashboardInputViewData } from "../view-data/AnalyticsDashboardInputViewData"; +import { ViewModel } from "../contracts/view-models/ViewModel"; - constructor(data: { totalUsers: number; activeUsers: number; totalRaces: number; totalLeagues: number }) { - this.totalUsers = data.totalUsers; - this.activeUsers = data.activeUsers; - this.totalRaces = data.totalRaces; - this.totalLeagues = data.totalLeagues; +export class AnalyticsDashboardViewModel extends ViewModel { + readonly totalUsers: number; + readonly activeUsers: number; + readonly totalRaces: number; + readonly totalLeagues: number; + + constructor(viewData: AnalyticsDashboardInputViewData) { + super(); + this.totalUsers = viewData.totalUsers; + this.activeUsers = viewData.activeUsers; + this.totalRaces = viewData.totalRaces; + this.totalLeagues = viewData.totalLeagues; } /** UI-specific: User engagement rate */ diff --git a/apps/website/lib/view-models/AnalyticsMetricsViewModel.test.ts b/apps/website/lib/view-models/AnalyticsMetricsViewModel.test.ts index 46a52feb7..455e70875 100644 --- a/apps/website/lib/view-models/AnalyticsMetricsViewModel.test.ts +++ b/apps/website/lib/view-models/AnalyticsMetricsViewModel.test.ts @@ -1,14 +1,17 @@ import { describe, it, expect } from 'vitest'; import { AnalyticsMetricsViewModel } from './AnalyticsMetricsViewModel'; +import { AnalyticsMetricsViewData } from '../view-data/AnalyticsMetricsViewData'; describe('AnalyticsMetricsViewModel', () => { - it('maps raw metrics fields from data', () => { - const vm = new AnalyticsMetricsViewModel({ + it('maps metrics fields from AnalyticsMetricsViewData', () => { + const viewData: AnalyticsMetricsViewData = { pageViews: 1234, uniqueVisitors: 567, averageSessionDuration: 180, bounceRate: 42.5, - }); + }; + + const vm = new AnalyticsMetricsViewModel(viewData); expect(vm.pageViews).toBe(1234); expect(vm.uniqueVisitors).toBe(567); @@ -16,36 +19,42 @@ describe('AnalyticsMetricsViewModel', () => { expect(vm.bounceRate).toBe(42.5); }); - it('formats counts using locale formatting helpers', () => { - const vm = new AnalyticsMetricsViewModel({ + it('formats counts using NumberDisplay', () => { + const viewData: AnalyticsMetricsViewData = { pageViews: 1200, uniqueVisitors: 3500, averageSessionDuration: 75, bounceRate: 10, - }); + }; - expect(vm.formattedPageViews).toBe((1200).toLocaleString()); - expect(vm.formattedUniqueVisitors).toBe((3500).toLocaleString()); + const vm = new AnalyticsMetricsViewModel(viewData); + + expect(vm.formattedPageViews).toBe('1,200'); + expect(vm.formattedUniqueVisitors).toBe('3,500'); }); - it('formats session duration as mm:ss', () => { - const vm = new AnalyticsMetricsViewModel({ + it('formats session duration using DurationDisplay', () => { + const viewData: AnalyticsMetricsViewData = { pageViews: 0, uniqueVisitors: 0, averageSessionDuration: 125, bounceRate: 0, - }); + }; - expect(vm.formattedSessionDuration).toBe('2:05'); + const vm = new AnalyticsMetricsViewModel(viewData); + + expect(vm.formattedSessionDuration).toBe('2:05.000'); }); - it('formats bounce rate as percentage with one decimal', () => { - const vm = new AnalyticsMetricsViewModel({ + it('formats bounce rate using PercentDisplay', () => { + const viewData: AnalyticsMetricsViewData = { pageViews: 0, uniqueVisitors: 0, averageSessionDuration: 0, - bounceRate: 37.345, - }); + bounceRate: 0.37345, + }; + + const vm = new AnalyticsMetricsViewModel(viewData); expect(vm.formattedBounceRate).toBe('37.3%'); }); diff --git a/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts b/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts index 5e7719dea..18de3fb78 100644 --- a/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts +++ b/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts @@ -2,40 +2,45 @@ * Analytics metrics view model * Represents metrics data for analytics * - * Note: No matching generated DTO available yet + * Accepts AnalyticsMetricsViewData as input and produces UI-ready data. */ -export class AnalyticsMetricsViewModel { - pageViews: number; - uniqueVisitors: number; - averageSessionDuration: number; - bounceRate: number; +import { AnalyticsMetricsViewData } from "../view-data/AnalyticsMetricsViewData"; +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { NumberDisplay } from "../display-objects/NumberDisplay"; +import { DurationDisplay } from "../display-objects/DurationDisplay"; +import { PercentDisplay } from "../display-objects/PercentDisplay"; - constructor(data: { pageViews: number; uniqueVisitors: number; averageSessionDuration: number; bounceRate: number }) { - this.pageViews = data.pageViews; - this.uniqueVisitors = data.uniqueVisitors; - this.averageSessionDuration = data.averageSessionDuration; - this.bounceRate = data.bounceRate; +export class AnalyticsMetricsViewModel extends ViewModel { + readonly pageViews: number; + readonly uniqueVisitors: number; + readonly averageSessionDuration: number; + readonly bounceRate: number; + + constructor(viewData: AnalyticsMetricsViewData) { + super(); + this.pageViews = viewData.pageViews; + this.uniqueVisitors = viewData.uniqueVisitors; + this.averageSessionDuration = viewData.averageSessionDuration; + this.bounceRate = viewData.bounceRate; } /** UI-specific: Formatted page views */ get formattedPageViews(): string { - return this.pageViews.toLocaleString(); + return NumberDisplay.format(this.pageViews); } /** UI-specific: Formatted unique visitors */ get formattedUniqueVisitors(): string { - return this.uniqueVisitors.toLocaleString(); + return NumberDisplay.format(this.uniqueVisitors); } /** UI-specific: Formatted session duration */ get formattedSessionDuration(): string { - const minutes = Math.floor(this.averageSessionDuration / 60); - const seconds = Math.floor(this.averageSessionDuration % 60); - return `${minutes}:${seconds.toString().padStart(2, '0')}`; + return DurationDisplay.formatSeconds(this.averageSessionDuration); } /** UI-specific: Formatted bounce rate */ get formattedBounceRate(): string { - return `${this.bounceRate.toFixed(1)}%`; + return PercentDisplay.format(this.bounceRate); } } \ No newline at end of file diff --git a/apps/website/lib/view-models/AvailableLeaguesViewModel.test.ts b/apps/website/lib/view-models/AvailableLeaguesViewModel.test.ts index b7d7869d0..f2c7c71bf 100644 --- a/apps/website/lib/view-models/AvailableLeaguesViewModel.test.ts +++ b/apps/website/lib/view-models/AvailableLeaguesViewModel.test.ts @@ -1,24 +1,44 @@ import { describe, expect, it } from 'vitest'; import { AvailableLeaguesViewModel, AvailableLeagueViewModel } from './AvailableLeaguesViewModel'; +import { AvailableLeaguesViewData, AvailableLeagueViewData } from '../view-data/AvailableLeaguesViewData'; describe('AvailableLeaguesViewModel', () => { - const baseLeague = { + const baseLeague: AvailableLeagueViewData = { id: 'league-1', name: 'Pro Series', game: 'iRacing', + description: 'Competitive league for serious drivers', drivers: 24, avgViewsPerRace: 12_500, + formattedAvgViews: '12.5k', mainSponsorSlot: { available: true, price: 5_000 }, secondarySlots: { available: 2, total: 3, price: 1_500 }, + cpm: 400, + formattedCpm: '$400', + hasAvailableSlots: true, rating: 4.7, tier: 'premium' as const, + tierConfig: { + color: '#FFD700', + bgColor: '#FFF8DC', + border: '2px solid #FFD700', + icon: '⭐', + }, nextRace: 'Next Sunday', seasonStatus: 'active' as const, - description: 'Competitive league for serious drivers', + statusConfig: { + color: '#10B981', + bg: '#D1FAE5', + label: 'Active Season', + }, + }; + + const baseViewData: AvailableLeaguesViewData = { + leagues: [baseLeague], }; it('maps league array into view models', () => { - const vm = new AvailableLeaguesViewModel([baseLeague]); + const vm = new AvailableLeaguesViewModel(baseViewData); expect(vm.leagues).toHaveLength(1); expect(vm.leagues[0]).toBeInstanceOf(AvailableLeagueViewModel); @@ -30,11 +50,11 @@ describe('AvailableLeaguesViewModel', () => { it('exposes formatted average views and CPM for main sponsor slot', () => { const leagueVm = new AvailableLeagueViewModel(baseLeague); - expect(leagueVm.formattedAvgViews).toBe(`${(baseLeague.avgViewsPerRace / 1000).toFixed(1)}k`); + expect(leagueVm.formattedAvgViews).toBe('12.5k'); const expectedCpm = Math.round((baseLeague.mainSponsorSlot.price / baseLeague.avgViewsPerRace) * 1000); expect(leagueVm.cpm).toBe(expectedCpm); - expect(leagueVm.formattedCpm).toBe(`$${expectedCpm}`); + expect(leagueVm.formattedCpm).toBe('$400'); }); it('detects available sponsor slots from main or secondary slots', () => { @@ -75,4 +95,5 @@ describe('AvailableLeaguesViewModel', () => { expect(upcoming.statusConfig.label).toBe('Starting Soon'); expect(completed.statusConfig.label).toBe('Season Ended'); }); + }); diff --git a/apps/website/lib/view-models/AvailableLeaguesViewModel.ts b/apps/website/lib/view-models/AvailableLeaguesViewModel.ts index 47350562a..91ac0c900 100644 --- a/apps/website/lib/view-models/AvailableLeaguesViewModel.ts +++ b/apps/website/lib/view-models/AvailableLeaguesViewModel.ts @@ -2,77 +2,83 @@ * Available Leagues View Model * * View model for leagues available for sponsorship. + * + * Accepts AvailableLeaguesViewData as input and produces UI-ready data. */ -export class AvailableLeaguesViewModel { - leagues: AvailableLeagueViewModel[]; +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { AvailableLeaguesViewData, AvailableLeagueViewData } from "../view-data/AvailableLeaguesViewData"; +import { NumberDisplay } from "../display-objects/NumberDisplay"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import { LeagueTierDisplay } from "../display-objects/LeagueTierDisplay"; +import { SeasonStatusDisplay } from "../display-objects/SeasonStatusDisplay"; - constructor(leagues: unknown[]) { - this.leagues = leagues.map(league => new AvailableLeagueViewModel(league)); +export class AvailableLeaguesViewModel extends ViewModel { + readonly leagues: AvailableLeagueViewModel[]; + + constructor(viewData: AvailableLeaguesViewData) { + super(); + this.leagues = viewData.leagues.map(league => new AvailableLeagueViewModel(league)); } } -export class AvailableLeagueViewModel { - id: string; - name: string; - game: string; - drivers: number; - avgViewsPerRace: number; - mainSponsorSlot: { available: boolean; price: number }; - secondarySlots: { available: number; total: number; price: number }; - rating: number; - tier: 'premium' | 'standard' | 'starter'; - nextRace?: string; - seasonStatus: 'active' | 'upcoming' | 'completed'; - description: string; +export class AvailableLeagueViewModel extends ViewModel { + readonly id: string; + readonly name: string; + readonly game: string; + readonly drivers: number; + readonly avgViewsPerRace: number; + readonly mainSponsorSlot: { available: boolean; price: number }; + readonly secondarySlots: { available: number; total: number; price: number }; + readonly rating: number; + readonly tier: 'premium' | 'standard' | 'starter'; + readonly nextRace?: string; + readonly seasonStatus: 'active' | 'upcoming' | 'completed'; + readonly description: string; - constructor(data: unknown) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const d = data as any; - this.id = d.id; - this.name = d.name; - this.game = d.game; - this.drivers = d.drivers; - this.avgViewsPerRace = d.avgViewsPerRace; - this.mainSponsorSlot = d.mainSponsorSlot; - this.secondarySlots = d.secondarySlots; - this.rating = d.rating; - this.tier = d.tier; - this.nextRace = d.nextRace; - this.seasonStatus = d.seasonStatus; - this.description = d.description; + constructor(viewData: AvailableLeagueViewData) { + super(); + this.id = viewData.id; + this.name = viewData.name; + this.game = viewData.game; + this.drivers = viewData.drivers; + this.avgViewsPerRace = viewData.avgViewsPerRace; + this.mainSponsorSlot = viewData.mainSponsorSlot; + this.secondarySlots = viewData.secondarySlots; + this.rating = viewData.rating; + this.tier = viewData.tier; + this.nextRace = viewData.nextRace; + this.seasonStatus = viewData.seasonStatus; + this.description = viewData.description; } + /** UI-specific: Formatted average views */ get formattedAvgViews(): string { - return `${(this.avgViewsPerRace / 1000).toFixed(1)}k`; + return NumberDisplay.formatCompact(this.avgViewsPerRace); } + /** UI-specific: CPM calculation */ get cpm(): number { return Math.round((this.mainSponsorSlot.price / this.avgViewsPerRace) * 1000); } + /** UI-specific: Formatted CPM */ get formattedCpm(): string { - return `$${this.cpm}`; + return CurrencyDisplay.formatCompact(this.cpm); } + /** UI-specific: Check if any sponsor slots are available */ get hasAvailableSlots(): boolean { return this.mainSponsorSlot.available || this.secondarySlots.available > 0; } + /** UI-specific: Tier configuration for badge styling */ get tierConfig() { - const configs = { - premium: { color: 'text-yellow-400', bgColor: 'bg-yellow-500/10', border: 'border-yellow-500/30', icon: '⭐' }, - standard: { color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', border: 'border-primary-blue/30', icon: '🏆' }, - starter: { color: 'text-gray-400', bgColor: 'bg-gray-500/10', border: 'border-gray-500/30', icon: '🚀' }, - }; - return configs[this.tier]; + return LeagueTierDisplay.getDisplay(this.tier); } + /** UI-specific: Status configuration for season state */ get statusConfig() { - const configs = { - active: { color: 'text-performance-green', bg: 'bg-performance-green/10', label: 'Active Season' }, - upcoming: { color: 'text-warning-amber', bg: 'bg-warning-amber/10', label: 'Starting Soon' }, - completed: { color: 'text-gray-400', bg: 'bg-gray-400/10', label: 'Season Ended' }, - }; - return configs[this.seasonStatus]; + return SeasonStatusDisplay.getDisplay(this.seasonStatus); } + } \ No newline at end of file diff --git a/apps/website/lib/view-models/AvatarGenerationViewModel.test.ts b/apps/website/lib/view-models/AvatarGenerationViewModel.test.ts index ef9b16b2a..53d676c6d 100644 --- a/apps/website/lib/view-models/AvatarGenerationViewModel.test.ts +++ b/apps/website/lib/view-models/AvatarGenerationViewModel.test.ts @@ -1,8 +1,30 @@ import { describe, it, expect } from 'vitest'; import { AvatarGenerationViewModel } from './AvatarGenerationViewModel'; +import { AvatarGenerationViewData } from '../view-data/AvatarGenerationViewData'; describe('AvatarGenerationViewModel', () => { - it('should be defined', () => { - expect(AvatarGenerationViewModel).toBeDefined(); + const mockViewData: AvatarGenerationViewData = { + success: true, + avatarUrls: ['https://example.com/avatar1.png', 'https://example.com/avatar2.png'], + errorMessage: undefined, + }; + + it('should be initialized from ViewData', () => { + const viewModel = new AvatarGenerationViewModel(mockViewData); + expect(viewModel.success).toBe(true); + expect(viewModel.avatarUrls).toEqual(['https://example.com/avatar1.png', 'https://example.com/avatar2.png']); + expect(viewModel.errorMessage).toBeUndefined(); + }); + + it('should handle missing avatarUrls in ViewData', () => { + const viewDataWithoutUrls: AvatarGenerationViewData = { + success: false, + avatarUrls: [], + errorMessage: 'Error occurred', + }; + const viewModel = new AvatarGenerationViewModel(viewDataWithoutUrls); + expect(viewModel.success).toBe(false); + expect(viewModel.avatarUrls).toEqual([]); + expect(viewModel.errorMessage).toBe('Error occurred'); }); }); diff --git a/apps/website/lib/view-models/AvatarGenerationViewModel.ts b/apps/website/lib/view-models/AvatarGenerationViewModel.ts index 505b1b544..5e9a6f5b6 100644 --- a/apps/website/lib/view-models/AvatarGenerationViewModel.ts +++ b/apps/website/lib/view-models/AvatarGenerationViewModel.ts @@ -1,18 +1,22 @@ -import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO'; +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { AvatarGenerationViewData } from "../view-data/AvatarGenerationViewData"; /** * AvatarGenerationViewModel * - * View model for avatar generation process + * View model for avatar generation process. + * + * Accepts AvatarGenerationViewData as input and produces UI-ready data. */ -export class AvatarGenerationViewModel { +export class AvatarGenerationViewModel extends ViewModel { readonly success: boolean; readonly avatarUrls: string[]; readonly errorMessage?: string; - constructor(dto: RequestAvatarGenerationOutputDTO) { - this.success = dto.success; - this.avatarUrls = dto.avatarUrls || []; - this.errorMessage = dto.errorMessage; + constructor(viewData: AvatarGenerationViewData) { + super(); + this.success = viewData.success; + this.avatarUrls = viewData.avatarUrls; + this.errorMessage = viewData.errorMessage; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/AvatarViewModel.test.ts b/apps/website/lib/view-models/AvatarViewModel.test.ts index a2862a203..44a31098a 100644 --- a/apps/website/lib/view-models/AvatarViewModel.test.ts +++ b/apps/website/lib/view-models/AvatarViewModel.test.ts @@ -1,53 +1,113 @@ import { describe, it, expect } from 'vitest'; import { AvatarViewModel } from './AvatarViewModel'; +import type { AvatarViewData } from '@/lib/view-data/AvatarViewData'; describe('AvatarViewModel', () => { - it('should create instance with driverId and avatarUrl', () => { - const dto = { - driverId: 'driver-123', - avatarUrl: 'https://example.com/avatar.jpg', - }; + describe('constructor', () => { + it('should create instance with valid AvatarViewData', () => { + const viewData: AvatarViewData = { + buffer: 'dGVzdC1pbWFnZS1kYXRh', + contentType: 'image/png', + }; - const viewModel = new AvatarViewModel(dto); + const viewModel = new AvatarViewModel(viewData); - expect(viewModel.driverId).toBe('driver-123'); - expect(viewModel.avatarUrl).toBe('https://example.com/avatar.jpg'); - }); - - it('should create instance without avatarUrl', () => { - const dto = { - driverId: 'driver-123', - }; - - const viewModel = new AvatarViewModel(dto); - - expect(viewModel.driverId).toBe('driver-123'); - expect(viewModel.avatarUrl).toBeUndefined(); - }); - - it('should return true for hasAvatar when avatarUrl exists', () => { - const viewModel = new AvatarViewModel({ - driverId: 'driver-123', - avatarUrl: 'https://example.com/avatar.jpg', + expect(viewModel.bufferBase64).toBe('dGVzdC1pbWFnZS1kYXRh'); + expect(viewModel.contentTypeLabel).toBe('PNG'); + expect(viewModel.hasValidData).toBe(true); }); - expect(viewModel.hasAvatar).toBe(true); + it('should create instance with empty buffer', () => { + const viewData: AvatarViewData = { + buffer: '', + contentType: 'image/png', + }; + + const viewModel = new AvatarViewModel(viewData); + + expect(viewModel.bufferBase64).toBe(''); + expect(viewModel.contentTypeLabel).toBe('PNG'); + expect(viewModel.hasValidData).toBe(false); + }); }); - it('should return false for hasAvatar when avatarUrl is undefined', () => { - const viewModel = new AvatarViewModel({ - driverId: 'driver-123', + describe('derived fields', () => { + it('should derive bufferBase64 correctly', () => { + const viewData: AvatarViewData = { + buffer: 'dGVzdA==', + contentType: 'image/png', + }; + + const viewModel = new AvatarViewModel(viewData); + + expect(viewModel.bufferBase64).toBe('dGVzdA=='); }); - expect(viewModel.hasAvatar).toBe(false); - }); + it('should derive contentTypeLabel correctly', () => { + const viewData: AvatarViewData = { + buffer: 'dGVzdC1pbWFnZS1kYXRh', + contentType: 'image/png', + }; - it('should return false for hasAvatar when avatarUrl is empty string', () => { - const viewModel = new AvatarViewModel({ - driverId: 'driver-123', - avatarUrl: '', + const viewModel = new AvatarViewModel(viewData); + + expect(viewModel.contentTypeLabel).toBe('PNG'); }); - expect(viewModel.hasAvatar).toBe(false); + it('should derive contentTypeLabel for different content types', () => { + const pngViewData: AvatarViewData = { + buffer: 'dGVzdC1pbWFnZS1kYXRh', + contentType: 'image/png', + }; + const pngViewModel = new AvatarViewModel(pngViewData); + expect(pngViewModel.contentTypeLabel).toBe('PNG'); + + const jpegViewData: AvatarViewData = { + buffer: 'dGVzdC1pbWFnZS1kYXRh', + contentType: 'image/jpeg', + }; + const jpegViewModel = new AvatarViewModel(jpegViewData); + expect(jpegViewModel.contentTypeLabel).toBe('JPEG'); + + const svgViewData: AvatarViewData = { + buffer: 'dGVzdC1pbWFnZS1kYXRh', + contentType: 'image/svg+xml', + }; + const svgViewModel = new AvatarViewModel(svgViewData); + expect(svgViewModel.contentTypeLabel).toBe('SVG+XML'); + }); + + it('should derive hasValidData as true when buffer has content', () => { + const viewData: AvatarViewData = { + buffer: 'dGVzdC1pbWFnZS1kYXRh', + contentType: 'image/png', + }; + + const viewModel = new AvatarViewModel(viewData); + + expect(viewModel.hasValidData).toBe(true); + }); + + it('should derive hasValidData as false when buffer is empty', () => { + const viewData: AvatarViewData = { + buffer: '', + contentType: 'image/png', + }; + + const viewModel = new AvatarViewModel(viewData); + + expect(viewModel.hasValidData).toBe(false); + }); + + it('should derive hasValidData as false when contentType is empty', () => { + const viewData: AvatarViewData = { + buffer: 'dGVzdC1pbWFnZS1kYXRh', + contentType: '', + }; + + const viewModel = new AvatarViewModel(viewData); + + expect(viewModel.hasValidData).toBe(false); + }); }); }); \ No newline at end of file diff --git a/apps/website/lib/view-models/AvatarViewModel.ts b/apps/website/lib/view-models/AvatarViewModel.ts index 42adba55c..c01a797b7 100644 --- a/apps/website/lib/view-models/AvatarViewModel.ts +++ b/apps/website/lib/view-models/AvatarViewModel.ts @@ -1,27 +1,29 @@ -// Note: No generated DTO available for Avatar yet -interface AvatarDTO { - driverId: string; - avatarUrl?: string; -} +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { AvatarDisplay } from "../display-objects/AvatarDisplay"; +import { AvatarViewData } from "@/lib/view-data/AvatarViewData"; /** * Avatar View Model * - * Represents avatar information for the UI layer + * Represents avatar information for the UI layer. + * Transforms AvatarViewData into UI-ready state with formatting and derived fields. */ -export class AvatarViewModel { - driverId: string; - avatarUrl?: string; +export class AvatarViewModel extends ViewModel { + // UI-specific derived fields (primitive outputs only) + readonly bufferBase64: string; + readonly contentTypeLabel: string; + readonly hasValidData: boolean; - constructor(dto: AvatarDTO) { - this.driverId = dto.driverId; - if (dto.avatarUrl !== undefined) { - this.avatarUrl = dto.avatarUrl; - } - } + constructor(viewData: AvatarViewData) { + super(); - /** UI-specific: Whether the driver has an avatar */ - get hasAvatar(): boolean { - return !!this.avatarUrl; + // Buffer is already base64 encoded in ViewData + this.bufferBase64 = viewData.buffer; + + // Derive content type label using Display Object + this.contentTypeLabel = AvatarDisplay.formatContentType(viewData.contentType); + + // Derive validity check using Display Object + this.hasValidData = AvatarDisplay.hasValidData(viewData.buffer, viewData.contentType); } } \ No newline at end of file diff --git a/apps/website/lib/view-models/BillingViewModel.test.ts b/apps/website/lib/view-models/BillingViewModel.test.ts index d46831497..0ad0c7262 100644 --- a/apps/website/lib/view-models/BillingViewModel.test.ts +++ b/apps/website/lib/view-models/BillingViewModel.test.ts @@ -1,11 +1,22 @@ import { describe, it, expect } from 'vitest'; +import type { BillingViewData } from '@/lib/view-data/BillingViewData'; import { BillingViewModel, PaymentMethodViewModel, InvoiceViewModel, BillingStatsViewModel } from './BillingViewModel'; describe('BillingViewModel', () => { it('maps arrays of payment methods, invoices and stats into view models', () => { - const data = { + const viewData: BillingViewData = { paymentMethods: [ - { id: 'pm-1', type: 'card', last4: '4242', brand: 'Visa', isDefault: true, expiryMonth: 12, expiryYear: 2030 }, + { + id: 'pm-1', + type: 'card', + last4: '4242', + brand: 'Visa', + isDefault: true, + expiryMonth: 12, + expiryYear: 2030, + displayLabel: 'Visa •••• 4242', + expiryDisplay: '12/2030', + }, ], invoices: [ { @@ -20,6 +31,10 @@ describe('BillingViewModel', () => { description: 'Sponsorship', sponsorshipType: 'league', pdfUrl: 'https://example.com/invoice.pdf', + formattedTotalAmount: '€119,00', + formattedVatAmount: '€19,00', + formattedDate: '2024-01-01', + isOverdue: false, }, ], stats: { @@ -29,10 +44,15 @@ describe('BillingViewModel', () => { nextPaymentAmount: 50, activeSponsorships: 3, averageMonthlySpend: 250, + formattedTotalSpent: '€1.000,00', + formattedPendingAmount: '€200,00', + formattedNextPaymentAmount: '€50,00', + formattedAverageMonthlySpend: '€250,00', + formattedNextPaymentDate: '2024-03-01', }, - } as any; + }; - const vm = new BillingViewModel(data); + const vm = new BillingViewModel(viewData); expect(vm.paymentMethods).toHaveLength(1); expect(vm.paymentMethods[0]).toBeInstanceOf(PaymentMethodViewModel); @@ -44,53 +64,67 @@ describe('BillingViewModel', () => { describe('PaymentMethodViewModel', () => { it('builds displayLabel based on type and bankName/brand', () => { - const card = new PaymentMethodViewModel({ + const card = { id: 'pm-1', - type: 'card', + type: 'card' as const, last4: '4242', brand: 'Visa', isDefault: true, - }); + displayLabel: 'Visa •••• 4242', + expiryDisplay: null, + }; - const sepa = new PaymentMethodViewModel({ + const sepa = { id: 'pm-2', - type: 'sepa', + type: 'sepa' as const, last4: '1337', bankName: 'Test Bank', isDefault: false, - }); + displayLabel: 'Test Bank •••• 1337', + expiryDisplay: null, + }; - expect(card.displayLabel).toBe('Visa •••• 4242'); - expect(sepa.displayLabel).toBe('Test Bank •••• 1337'); + const cardVm = new PaymentMethodViewModel(card); + const sepaVm = new PaymentMethodViewModel(sepa); + + expect(cardVm.displayLabel).toBe('Visa •••• 4242'); + expect(sepaVm.displayLabel).toBe('Test Bank •••• 1337'); }); it('returns expiryDisplay when month and year are provided', () => { - const withExpiry = new PaymentMethodViewModel({ + const withExpiry = { id: 'pm-1', - type: 'card', + type: 'card' as const, last4: '4242', brand: 'Visa', isDefault: true, expiryMonth: 3, expiryYear: 2030, - }); + displayLabel: 'Visa •••• 4242', + expiryDisplay: '03/2030', + }; - const withoutExpiry = new PaymentMethodViewModel({ + const withoutExpiry = { id: 'pm-2', - type: 'card', + type: 'card' as const, last4: '9999', brand: 'Mastercard', isDefault: false, - }); + displayLabel: 'Mastercard •••• 9999', + expiryDisplay: null, + }; - expect(withExpiry.expiryDisplay).toBe('03/2030'); - expect(withoutExpiry.expiryDisplay).toBeNull(); + const withExpiryVm = new PaymentMethodViewModel(withExpiry); + const withoutExpiryVm = new PaymentMethodViewModel(withoutExpiry); + + expect(withExpiryVm.expiryDisplay).toBe('03/2030'); + expect(withoutExpiryVm.expiryDisplay).toBeNull(); }); }); describe('InvoiceViewModel', () => { it('formats monetary amounts and dates', () => { - const dto = { + const viewData = { id: 'inv-1', invoiceNumber: 'INV-1', date: '2024-01-15', @@ -98,16 +132,20 @@ describe('InvoiceViewModel', () => { amount: 100, vatAmount: 19, totalAmount: 119, - status: 'paid', + status: 'paid' as const, description: 'Sponsorship', - sponsorshipType: 'league', + sponsorshipType: 'league' as const, pdfUrl: 'https://example.com/invoice.pdf', - } as any; + formattedTotalAmount: '€119,00', + formattedVatAmount: '€19,00', + formattedDate: '2024-01-15', + isOverdue: false, + }; - const vm = new InvoiceViewModel(dto); + const vm = new InvoiceViewModel(viewData); - expect(vm.formattedTotalAmount).toBe(`€${(119).toLocaleString('de-DE', { minimumFractionDigits: 2 })}`); - expect(vm.formattedVatAmount).toBe(`€${(19).toLocaleString('de-DE', { minimumFractionDigits: 2 })}`); + expect(vm.formattedTotalAmount).toBe('€119,00'); + expect(vm.formattedVatAmount).toBe('€19,00'); expect(typeof vm.formattedDate).toBe('string'); }); @@ -116,7 +154,7 @@ describe('InvoiceViewModel', () => { const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(); - const overdue = new InvoiceViewModel({ + const overdue = { id: 'inv-1', invoiceNumber: 'INV-1', date: pastDate, @@ -124,13 +162,17 @@ describe('InvoiceViewModel', () => { amount: 0, vatAmount: 0, totalAmount: 0, - status: 'overdue', + status: 'overdue' as const, description: '', - sponsorshipType: 'league', + sponsorshipType: 'league' as const, pdfUrl: '', - } as any); + formattedTotalAmount: '€0,00', + formattedVatAmount: '€0,00', + formattedDate: pastDate, + isOverdue: true, + }; - const pendingPastDue = new InvoiceViewModel({ + const pendingPastDue = { id: 'inv-2', invoiceNumber: 'INV-2', date: pastDate, @@ -138,13 +180,17 @@ describe('InvoiceViewModel', () => { amount: 0, vatAmount: 0, totalAmount: 0, - status: 'pending', + status: 'pending' as const, description: '', - sponsorshipType: 'league', + sponsorshipType: 'league' as const, pdfUrl: '', - } as any); + formattedTotalAmount: '€0,00', + formattedVatAmount: '€0,00', + formattedDate: pastDate, + isOverdue: true, + }; - const pendingFuture = new InvoiceViewModel({ + const pendingFuture = { id: 'inv-3', invoiceNumber: 'INV-3', date: pastDate, @@ -152,35 +198,48 @@ describe('InvoiceViewModel', () => { amount: 0, vatAmount: 0, totalAmount: 0, - status: 'pending', + status: 'pending' as const, description: '', - sponsorshipType: 'league', + sponsorshipType: 'league' as const, pdfUrl: '', - } as any); + formattedTotalAmount: '€0,00', + formattedVatAmount: '€0,00', + formattedDate: pastDate, + isOverdue: false, + }; - expect(overdue.isOverdue).toBe(true); - expect(pendingPastDue.isOverdue).toBe(true); - expect(pendingFuture.isOverdue).toBe(false); + const overdueVm = new InvoiceViewModel(overdue); + const pendingPastDueVm = new InvoiceViewModel(pendingPastDue); + const pendingFutureVm = new InvoiceViewModel(pendingFuture); + + expect(overdueVm.isOverdue).toBe(true); + expect(pendingPastDueVm.isOverdue).toBe(true); + expect(pendingFutureVm.isOverdue).toBe(false); }); }); describe('BillingStatsViewModel', () => { it('formats monetary fields and next payment date', () => { - const dto = { + const viewData = { totalSpent: 1234, pendingAmount: 56.78, nextPaymentDate: '2024-03-01', nextPaymentAmount: 42, activeSponsorships: 2, averageMonthlySpend: 321, - } as any; + formattedTotalSpent: '€1.234,00', + formattedPendingAmount: '€56,78', + formattedNextPaymentAmount: '€42,00', + formattedAverageMonthlySpend: '€321,00', + formattedNextPaymentDate: '2024-03-01', + }; - const vm = new BillingStatsViewModel(dto); + const vm = new BillingStatsViewModel(viewData); - expect(vm.formattedTotalSpent).toBe(`€${(1234).toLocaleString('de-DE')}`); - expect(vm.formattedPendingAmount).toBe(`€${(56.78).toLocaleString('de-DE', { minimumFractionDigits: 2 })}`); - expect(vm.formattedNextPaymentAmount).toBe(`€${(42).toLocaleString('de-DE', { minimumFractionDigits: 2 })}`); - expect(vm.formattedAverageMonthlySpend).toBe(`€${(321).toLocaleString('de-DE')}`); + expect(vm.formattedTotalSpent).toBe('€1.234,00'); + expect(vm.formattedPendingAmount).toBe('€56,78'); + expect(vm.formattedNextPaymentAmount).toBe('€42,00'); + expect(vm.formattedAverageMonthlySpend).toBe('€321,00'); expect(typeof vm.formattedNextPaymentDate).toBe('string'); }); }); diff --git a/apps/website/lib/view-models/BillingViewModel.ts b/apps/website/lib/view-models/BillingViewModel.ts index ab542801e..87367d568 100644 --- a/apps/website/lib/view-models/BillingViewModel.ts +++ b/apps/website/lib/view-models/BillingViewModel.ts @@ -2,24 +2,39 @@ * Billing View Model * * View model for sponsor billing data with UI-specific transformations. + * Transforms BillingViewData into UI-ready state with formatting and derived fields. */ -export class BillingViewModel { +import type { BillingViewData } from '@/lib/view-data/BillingViewData'; +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import { DateDisplay } from "../display-objects/DateDisplay"; + +/** + * BillingViewModel + * + * View Model for sponsor billing data. + * Transforms BillingViewData into UI-ready state with formatting and derived fields. + */ +export class BillingViewModel extends ViewModel { paymentMethods: PaymentMethodViewModel[]; invoices: InvoiceViewModel[]; stats: BillingStatsViewModel; - constructor(data: { - paymentMethods: unknown[]; - invoices: unknown[]; - stats: unknown; - }) { - this.paymentMethods = data.paymentMethods.map(pm => new PaymentMethodViewModel(pm)); - this.invoices = data.invoices.map(inv => new InvoiceViewModel(inv)); - this.stats = new BillingStatsViewModel(data.stats); + constructor(viewData: BillingViewData) { + super(); + this.paymentMethods = viewData.paymentMethods.map(pm => new PaymentMethodViewModel(pm)); + this.invoices = viewData.invoices.map(inv => new InvoiceViewModel(inv)); + this.stats = new BillingStatsViewModel(viewData.stats); } } -export class PaymentMethodViewModel { +/** + * PaymentMethodViewModel + * + * View Model for payment method data. + * Provides formatted display labels and expiry information. + */ +export class PaymentMethodViewModel extends ViewModel { id: string; type: 'card' | 'bank' | 'sepa'; last4: string; @@ -29,35 +44,43 @@ export class PaymentMethodViewModel { expiryYear?: number; bankName?: string; - constructor(data: unknown) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const d = data as any; - this.id = d.id; - this.type = d.type; - this.last4 = d.last4; - this.brand = d.brand; - this.isDefault = d.isDefault; - this.expiryMonth = d.expiryMonth; - this.expiryYear = d.expiryYear; - this.bankName = d.bankName; - } + // UI-specific derived fields (primitive outputs only) + readonly displayLabel: string; + readonly expiryDisplay: string | null; - get displayLabel(): string { - if (this.type === 'sepa' && this.bankName) { - return `${this.bankName} •••• ${this.last4}`; - } - return `${this.brand} •••• ${this.last4}`; - } - - get expiryDisplay(): string | null { - if (this.expiryMonth && this.expiryYear) { - return `${String(this.expiryMonth).padStart(2, '0')}/${this.expiryYear}`; - } - return null; + constructor(viewData: { + id: string; + type: 'card' | 'bank' | 'sepa'; + last4: string; + brand?: string; + isDefault: boolean; + expiryMonth?: number; + expiryYear?: number; + bankName?: string; + displayLabel: string; + expiryDisplay: string | null; + }) { + super(); + this.id = viewData.id; + this.type = viewData.type; + this.last4 = viewData.last4; + this.brand = viewData.brand; + this.isDefault = viewData.isDefault; + this.expiryMonth = viewData.expiryMonth; + this.expiryYear = viewData.expiryYear; + this.bankName = viewData.bankName; + this.displayLabel = viewData.displayLabel; + this.expiryDisplay = viewData.expiryDisplay; } } -export class InvoiceViewModel { +/** + * InvoiceViewModel + * + * View Model for invoice data. + * Provides formatted amounts, dates, and derived status flags. + */ +export class InvoiceViewModel extends ViewModel { id: string; invoiceNumber: string; date: Date; @@ -70,40 +93,55 @@ export class InvoiceViewModel { sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform'; pdfUrl: string; - constructor(data: unknown) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const d = data as any; - this.id = d.id; - this.invoiceNumber = d.invoiceNumber; - this.date = new Date(d.date); - this.dueDate = new Date(d.dueDate); - this.amount = d.amount; - this.vatAmount = d.vatAmount; - this.totalAmount = d.totalAmount; - this.status = d.status; - this.description = d.description; - this.sponsorshipType = d.sponsorshipType; - this.pdfUrl = d.pdfUrl; - } + // UI-specific derived fields (primitive outputs only) + readonly formattedTotalAmount: string; + readonly formattedVatAmount: string; + readonly formattedDate: string; + readonly isOverdue: boolean; - get formattedTotalAmount(): string { - return `€${this.totalAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`; - } - - get formattedVatAmount(): string { - return `€${this.vatAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`; - } - - get formattedDate(): string { - return this.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); - } - - get isOverdue(): boolean { - return this.status === 'overdue' || (this.status === 'pending' && new Date() > this.dueDate); + constructor(viewData: { + id: string; + invoiceNumber: string; + date: string; + dueDate: string; + amount: number; + vatAmount: number; + totalAmount: number; + status: 'paid' | 'pending' | 'overdue' | 'failed'; + description: string; + sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform'; + pdfUrl: string; + formattedTotalAmount: string; + formattedVatAmount: string; + formattedDate: string; + isOverdue: boolean; + }) { + super(); + this.id = viewData.id; + this.invoiceNumber = viewData.invoiceNumber; + this.date = new Date(viewData.date); + this.dueDate = new Date(viewData.dueDate); + this.amount = viewData.amount; + this.vatAmount = viewData.vatAmount; + this.totalAmount = viewData.totalAmount; + this.status = viewData.status; + this.description = viewData.description; + this.sponsorshipType = viewData.sponsorshipType; + this.pdfUrl = viewData.pdfUrl; + this.formattedTotalAmount = viewData.formattedTotalAmount; + this.formattedVatAmount = viewData.formattedVatAmount; + this.formattedDate = viewData.formattedDate; + this.isOverdue = viewData.isOverdue; } } -export class BillingStatsViewModel { +/** + * BillingStatsViewModel + * + * View Model for billing statistics. + * Provides formatted monetary fields and derived metrics. + */ +export class BillingStatsViewModel extends ViewModel { totalSpent: number; pendingAmount: number; nextPaymentDate: Date; @@ -111,34 +149,37 @@ export class BillingStatsViewModel { activeSponsorships: number; averageMonthlySpend: number; - constructor(data: unknown) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const d = data as any; - this.totalSpent = d.totalSpent; - this.pendingAmount = d.pendingAmount; - this.nextPaymentDate = new Date(d.nextPaymentDate); - this.nextPaymentAmount = d.nextPaymentAmount; - this.activeSponsorships = d.activeSponsorships; - this.averageMonthlySpend = d.averageMonthlySpend; - } + // UI-specific derived fields (primitive outputs only) + readonly formattedTotalSpent: string; + readonly formattedPendingAmount: string; + readonly formattedNextPaymentAmount: string; + readonly formattedAverageMonthlySpend: string; + readonly formattedNextPaymentDate: string; - get formattedTotalSpent(): string { - return `€${this.totalSpent.toLocaleString('de-DE')}`; - } - - get formattedPendingAmount(): string { - return `€${this.pendingAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`; - } - - get formattedNextPaymentAmount(): string { - return `€${this.nextPaymentAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`; - } - - get formattedAverageMonthlySpend(): string { - return `€${this.averageMonthlySpend.toLocaleString('de-DE')}`; - } - - get formattedNextPaymentDate(): string { - return this.nextPaymentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + constructor(viewData: { + totalSpent: number; + pendingAmount: number; + nextPaymentDate: string; + nextPaymentAmount: number; + activeSponsorships: number; + averageMonthlySpend: number; + formattedTotalSpent: string; + formattedPendingAmount: string; + formattedNextPaymentAmount: string; + formattedAverageMonthlySpend: string; + formattedNextPaymentDate: string; + }) { + super(); + this.totalSpent = viewData.totalSpent; + this.pendingAmount = viewData.pendingAmount; + this.nextPaymentDate = new Date(viewData.nextPaymentDate); + this.nextPaymentAmount = viewData.nextPaymentAmount; + this.activeSponsorships = viewData.activeSponsorships; + this.averageMonthlySpend = viewData.averageMonthlySpend; + this.formattedTotalSpent = viewData.formattedTotalSpent; + this.formattedPendingAmount = viewData.formattedPendingAmount; + this.formattedNextPaymentAmount = viewData.formattedNextPaymentAmount; + this.formattedAverageMonthlySpend = viewData.formattedAverageMonthlySpend; + this.formattedNextPaymentDate = viewData.formattedNextPaymentDate; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/CompleteOnboardingViewModel.test.ts b/apps/website/lib/view-models/CompleteOnboardingViewModel.test.ts index 19f409500..912c14508 100644 --- a/apps/website/lib/view-models/CompleteOnboardingViewModel.test.ts +++ b/apps/website/lib/view-models/CompleteOnboardingViewModel.test.ts @@ -1,35 +1,189 @@ import { describe, it, expect } from 'vitest'; import { CompleteOnboardingViewModel } from './CompleteOnboardingViewModel'; -import type { CompleteOnboardingOutputDTO } from '../types/generated/CompleteOnboardingOutputDTO'; +import type { CompleteOnboardingViewData } from '../builders/view-data/CompleteOnboardingViewData'; describe('CompleteOnboardingViewModel', () => { - it('should create instance with success flag', () => { - const dto: CompleteOnboardingOutputDTO = { - success: true, - }; + describe('constructor', () => { + it('should create instance with success flag', () => { + const viewData: CompleteOnboardingViewData = { + success: true, + }; - const viewModel = new CompleteOnboardingViewModel(dto); + const viewModel = new CompleteOnboardingViewModel(viewData); - expect(viewModel.success).toBe(true); + expect(viewModel.success).toBe(true); + }); + + it('should create instance with driverId', () => { + const viewData: CompleteOnboardingViewData = { + success: true, + driverId: 'driver-123', + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.driverId).toBe('driver-123'); + }); + + it('should create instance with errorMessage', () => { + const viewData: CompleteOnboardingViewData = { + success: false, + errorMessage: 'Failed to complete onboarding', + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.errorMessage).toBe('Failed to complete onboarding'); + }); + + it('should create instance with all fields', () => { + const viewData: CompleteOnboardingViewData = { + success: true, + driverId: 'driver-123', + errorMessage: undefined, + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.success).toBe(true); + expect(viewModel.driverId).toBe('driver-123'); + expect(viewModel.errorMessage).toBeUndefined(); + }); }); - it('should expose isSuccessful as true when success is true', () => { - const dto: CompleteOnboardingOutputDTO = { - success: true, - }; + describe('UI-specific getters', () => { + it('should expose isSuccessful as true when success is true', () => { + const viewData: CompleteOnboardingViewData = { + success: true, + }; - const viewModel = new CompleteOnboardingViewModel(dto); + const viewModel = new CompleteOnboardingViewModel(viewData); - expect(viewModel.isSuccessful).toBe(true); + expect(viewModel.isSuccessful).toBe(true); + }); + + it('should expose isSuccessful as false when success is false', () => { + const viewData: CompleteOnboardingViewData = { + success: false, + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.isSuccessful).toBe(false); + }); + + it('should expose hasError as true when errorMessage is present', () => { + const viewData: CompleteOnboardingViewData = { + success: false, + errorMessage: 'Error occurred', + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.hasError).toBe(true); + }); + + it('should expose hasError as false when errorMessage is not present', () => { + const viewData: CompleteOnboardingViewData = { + success: true, + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.hasError).toBe(false); + }); }); - it('should expose isSuccessful as false when success is false', () => { - const dto: CompleteOnboardingOutputDTO = { - success: false, - }; + describe('Display Object composition', () => { + it('should derive statusLabel from success', () => { + const viewData: CompleteOnboardingViewData = { + success: true, + }; - const viewModel = new CompleteOnboardingViewModel(dto); + const viewModel = new CompleteOnboardingViewModel(viewData); - expect(viewModel.isSuccessful).toBe(false); + expect(viewModel.statusLabel).toBe('Onboarding Complete'); + }); + + it('should derive statusLabel from failure', () => { + const viewData: CompleteOnboardingViewData = { + success: false, + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.statusLabel).toBe('Onboarding Failed'); + }); + + it('should derive statusVariant from success', () => { + const viewData: CompleteOnboardingViewData = { + success: true, + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.statusVariant).toBe('performance-green'); + }); + + it('should derive statusVariant from failure', () => { + const viewData: CompleteOnboardingViewData = { + success: false, + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.statusVariant).toBe('racing-red'); + }); + + it('should derive statusIcon from success', () => { + const viewData: CompleteOnboardingViewData = { + success: true, + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.statusIcon).toBe('✅'); + }); + + it('should derive statusIcon from failure', () => { + const viewData: CompleteOnboardingViewData = { + success: false, + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.statusIcon).toBe('❌'); + }); + + it('should derive statusMessage from success', () => { + const viewData: CompleteOnboardingViewData = { + success: true, + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.statusMessage).toBe('Your onboarding has been completed successfully.'); + }); + + it('should derive statusMessage from failure with default message', () => { + const viewData: CompleteOnboardingViewData = { + success: false, + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.statusMessage).toBe('Failed to complete onboarding. Please try again.'); + }); + + it('should derive statusMessage from failure with custom error message', () => { + const viewData: CompleteOnboardingViewData = { + success: false, + errorMessage: 'Custom error message', + }; + + const viewModel = new CompleteOnboardingViewModel(viewData); + + expect(viewModel.statusMessage).toBe('Custom error message'); + }); }); }); \ No newline at end of file diff --git a/apps/website/lib/view-models/CompleteOnboardingViewModel.ts b/apps/website/lib/view-models/CompleteOnboardingViewModel.ts index 940acca13..c86fab6af 100644 --- a/apps/website/lib/view-models/CompleteOnboardingViewModel.ts +++ b/apps/website/lib/view-models/CompleteOnboardingViewModel.ts @@ -1,18 +1,36 @@ -import { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO'; +import type { CompleteOnboardingViewData } from '@/lib/builders/view-data/CompleteOnboardingViewData'; +import { OnboardingStatusDisplay } from '../display-objects/OnboardingStatusDisplay'; + +import { ViewModel } from "../contracts/view-models/ViewModel"; /** * Complete onboarding view model * UI representation of onboarding completion result + * + * Composes Display Objects and transforms ViewData for UI consumption. */ -export class CompleteOnboardingViewModel { +export class CompleteOnboardingViewModel extends ViewModel { 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 derived fields (primitive outputs only) + readonly statusLabel: string; + readonly statusVariant: string; + readonly statusIcon: string; + readonly statusMessage: string; + + constructor(viewData: CompleteOnboardingViewData) { + super(); + this.success = viewData.success; + if (viewData.driverId !== undefined) this.driverId = viewData.driverId; + if (viewData.errorMessage !== undefined) this.errorMessage = viewData.errorMessage; + + // Derive UI-specific fields using Display Object + this.statusLabel = OnboardingStatusDisplay.statusLabel(this.success); + this.statusVariant = OnboardingStatusDisplay.statusVariant(this.success); + this.statusIcon = OnboardingStatusDisplay.statusIcon(this.success); + this.statusMessage = OnboardingStatusDisplay.statusMessage(this.success, this.errorMessage); } /** UI-specific: Whether onboarding was successful */ diff --git a/apps/website/lib/view-models/CreateLeagueViewModel.test.ts b/apps/website/lib/view-models/CreateLeagueViewModel.test.ts index 4f4297484..a9d782076 100644 --- a/apps/website/lib/view-models/CreateLeagueViewModel.test.ts +++ b/apps/website/lib/view-models/CreateLeagueViewModel.test.ts @@ -1,36 +1,102 @@ import { describe, it, expect } from 'vitest'; import { CreateLeagueViewModel } from './CreateLeagueViewModel'; -import type { CreateLeagueOutputDTO } from '../types/generated/CreateLeagueOutputDTO'; +import type { CreateLeagueViewData } from '../view-data/CreateLeagueViewData'; -const createDto = (overrides: Partial = {}): CreateLeagueOutputDTO => ({ +const createViewData = (overrides: Partial = {}): CreateLeagueViewData => ({ leagueId: 'league-1', success: true, ...overrides, -} as CreateLeagueOutputDTO); +} as CreateLeagueViewData); describe('CreateLeagueViewModel', () => { - it('maps leagueId and success from DTO', () => { - const dto = createDto({ leagueId: 'league-123', success: true }); + describe('constructor', () => { + it('should create instance with success flag', () => { + const viewData: CreateLeagueViewData = { + leagueId: 'league-123', + success: true, + successMessage: 'League created successfully!', + }; - const vm = new CreateLeagueViewModel(dto); + const viewModel = new CreateLeagueViewModel(viewData); - expect(vm.leagueId).toBe('league-123'); - expect(vm.success).toBe(true); + expect(viewModel.success).toBe(true); + }); + + it('should create instance with leagueId', () => { + const viewData: CreateLeagueViewData = { + leagueId: 'league-123', + success: true, + successMessage: 'League created successfully!', + }; + + const viewModel = new CreateLeagueViewModel(viewData); + + expect(viewModel.leagueId).toBe('league-123'); + }); + + it('should create instance with all fields', () => { + const viewData: CreateLeagueViewData = { + leagueId: 'test-league', + success: false, + successMessage: 'Failed to create league.', + }; + + const viewModel = new CreateLeagueViewModel(viewData); + + expect(viewModel.leagueId).toBe('test-league'); + expect(viewModel.success).toBe(false); + }); }); - it('returns success successMessage when creation succeeded', () => { - const dto = createDto({ success: true }); + describe('UI-specific getters', () => { + it('should expose isSuccessful as true when success is true', () => { + const viewData: CreateLeagueViewData = { + leagueId: 'league-123', + success: true, + successMessage: 'League created successfully!', + }; - const vm = new CreateLeagueViewModel(dto); + const viewModel = new CreateLeagueViewModel(viewData); - expect(vm.successMessage).toBe('League created successfully!'); + expect(viewModel.isSuccessful).toBe(true); + }); + + it('should expose isSuccessful as false when success is false', () => { + const viewData: CreateLeagueViewData = { + leagueId: 'league-123', + success: false, + successMessage: 'Failed to create league.', + }; + + const viewModel = new CreateLeagueViewModel(viewData); + + expect(viewModel.isSuccessful).toBe(false); + }); }); - it('returns failure successMessage when creation failed', () => { - const dto = createDto({ success: false }); + describe('Display Object composition', () => { + it('should derive successMessage from success', () => { + const viewData: CreateLeagueViewData = { + leagueId: 'league-123', + success: true, + successMessage: 'League created successfully!', + }; - const vm = new CreateLeagueViewModel(dto); + const viewModel = new CreateLeagueViewModel(viewData); - expect(vm.successMessage).toBe('Failed to create league.'); + expect(viewModel.successMessage).toBe('League created successfully!'); + }); + + it('should derive successMessage from failure', () => { + const viewData: CreateLeagueViewData = { + leagueId: 'league-123', + success: false, + successMessage: 'Failed to create league.', + }; + + const viewModel = new CreateLeagueViewModel(viewData); + + expect(viewModel.successMessage).toBe('Failed to create league.'); + }); }); }); diff --git a/apps/website/lib/view-models/CreateLeagueViewModel.ts b/apps/website/lib/view-models/CreateLeagueViewModel.ts index f10fbe0e2..314a0e2e3 100644 --- a/apps/website/lib/view-models/CreateLeagueViewModel.ts +++ b/apps/website/lib/view-models/CreateLeagueViewModel.ts @@ -1,22 +1,32 @@ -import { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO'; +import type { CreateLeagueViewData } from '../view-data/CreateLeagueViewData'; +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { LeagueCreationStatusDisplay } from '../display-objects/LeagueCreationStatusDisplay'; /** * View Model for Create League Result * * Represents the result of creating a league in a UI-ready format. + * Composes Display Objects and transforms ViewData for UI consumption. */ -export class CreateLeagueViewModel { - leagueId: string; - success: boolean; +export class CreateLeagueViewModel extends ViewModel { + readonly leagueId: string; + readonly success: boolean; - constructor(dto: CreateLeagueOutputDTO) { - this.leagueId = dto.leagueId; - this.success = dto.success; + // UI-specific derived fields (primitive outputs only) + readonly successMessage: string; + + constructor(viewData: CreateLeagueViewData) { + super(); + this.leagueId = viewData.leagueId; + this.success = viewData.success; + + // Derive UI-specific fields using Display Object + this.successMessage = LeagueCreationStatusDisplay.statusMessage(this.success); } - /** UI-specific: Success message */ - get successMessage(): string { - return this.success ? 'League created successfully!' : 'Failed to create league.'; + /** UI-specific: Whether league creation was successful */ + get isSuccessful(): boolean { + return this.success; } } diff --git a/apps/website/lib/view-models/CreateTeamViewModel.test.ts b/apps/website/lib/view-models/CreateTeamViewModel.test.ts index de4b36259..e3c130f3f 100644 --- a/apps/website/lib/view-models/CreateTeamViewModel.test.ts +++ b/apps/website/lib/view-models/CreateTeamViewModel.test.ts @@ -1,29 +1,66 @@ import { describe, it, expect } from 'vitest'; import { CreateTeamViewModel } from './CreateTeamViewModel'; +import type { CreateTeamViewData } from '../view-data/CreateTeamViewData'; describe('CreateTeamViewModel', () => { - it('maps id and success from DTO', () => { - const dto = { id: 'team-123', success: true }; + it('maps teamId and success from ViewData', () => { + const viewData: CreateTeamViewData = { + teamId: 'team-123', + success: true, + successMessage: 'Team created successfully!', + }; - const vm = new CreateTeamViewModel(dto); + const vm = new CreateTeamViewModel(viewData); - expect(vm.id).toBe('team-123'); + expect(vm.teamId).toBe('team-123'); expect(vm.success).toBe(true); }); it('returns success successMessage when creation succeeded', () => { - const dto = { id: 'team-1', success: true }; + const viewData: CreateTeamViewData = { + teamId: 'team-1', + success: true, + successMessage: 'Team created successfully!', + }; - const vm = new CreateTeamViewModel(dto); + const vm = new CreateTeamViewModel(viewData); expect(vm.successMessage).toBe('Team created successfully!'); }); it('returns failure successMessage when creation failed', () => { - const dto = { id: 'team-1', success: false }; + const viewData: CreateTeamViewData = { + teamId: 'team-1', + success: false, + successMessage: 'Failed to create team.', + }; - const vm = new CreateTeamViewModel(dto); + const vm = new CreateTeamViewModel(viewData); expect(vm.successMessage).toBe('Failed to create team.'); }); + + it('returns isSuccessful when creation succeeded', () => { + const viewData: CreateTeamViewData = { + teamId: 'team-1', + success: true, + successMessage: 'Team created successfully!', + }; + + const vm = new CreateTeamViewModel(viewData); + + expect(vm.isSuccessful).toBe(true); + }); + + it('returns isSuccessful when creation failed', () => { + const viewData: CreateTeamViewData = { + teamId: 'team-1', + success: false, + successMessage: 'Failed to create team.', + }; + + const vm = new CreateTeamViewModel(viewData); + + expect(vm.isSuccessful).toBe(false); + }); }); diff --git a/apps/website/lib/view-models/CreateTeamViewModel.ts b/apps/website/lib/view-models/CreateTeamViewModel.ts index b5f8cb2ae..c8eb0e19e 100644 --- a/apps/website/lib/view-models/CreateTeamViewModel.ts +++ b/apps/website/lib/view-models/CreateTeamViewModel.ts @@ -1,19 +1,31 @@ +import type { CreateTeamViewData } from '../view-data/CreateTeamViewData'; +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { TeamCreationStatusDisplay } from '../display-objects/TeamCreationStatusDisplay'; + /** * View Model for Create Team Result * * Represents the result of creating a team in a UI-ready format. + * Composes Display Objects and transforms ViewData for UI consumption. */ -export class CreateTeamViewModel { - id: string; - success: boolean; +export class CreateTeamViewModel extends ViewModel { + readonly teamId: string; + readonly success: boolean; - constructor(dto: { id: string; success: boolean }) { - this.id = dto.id; - this.success = dto.success; + // UI-specific derived fields (primitive outputs only) + readonly successMessage: string; + + constructor(viewData: CreateTeamViewData) { + super(); + this.teamId = viewData.teamId; + this.success = viewData.success; + + // Derive UI-specific fields using Display Object + this.successMessage = TeamCreationStatusDisplay.statusMessage(this.success); } - /** UI-specific: Success message */ - get successMessage(): string { - return this.success ? 'Team created successfully!' : 'Failed to create team.'; + /** UI-specific: Whether team creation was successful */ + get isSuccessful(): boolean { + return this.success; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/DeleteMediaViewModel.test.ts b/apps/website/lib/view-models/DeleteMediaViewModel.test.ts index b19a765dd..675ec7bcd 100644 --- a/apps/website/lib/view-models/DeleteMediaViewModel.test.ts +++ b/apps/website/lib/view-models/DeleteMediaViewModel.test.ts @@ -1,18 +1,19 @@ import { describe, it, expect } from 'vitest'; import { DeleteMediaViewModel } from './DeleteMediaViewModel'; +import type { DeleteMediaViewData } from '@/lib/builders/view-data/DeleteMediaViewData'; describe('DeleteMediaViewModel', () => { it('should create instance with success true', () => { - const dto = { success: true }; - const viewModel = new DeleteMediaViewModel(dto); + const viewData: DeleteMediaViewData = { success: true }; + const viewModel = new DeleteMediaViewModel(viewData); expect(viewModel.success).toBe(true); expect(viewModel.error).toBeUndefined(); }); it('should create instance with success false and error', () => { - const dto = { success: false, error: 'Failed to delete media' }; - const viewModel = new DeleteMediaViewModel(dto); + const viewData: DeleteMediaViewData = { success: false, error: 'Failed to delete media' }; + const viewModel = new DeleteMediaViewModel(viewData); expect(viewModel.success).toBe(false); expect(viewModel.error).toBe('Failed to delete media'); diff --git a/apps/website/lib/view-models/DeleteMediaViewModel.ts b/apps/website/lib/view-models/DeleteMediaViewModel.ts index 456899f01..f5cabb501 100644 --- a/apps/website/lib/view-models/DeleteMediaViewModel.ts +++ b/apps/website/lib/view-models/DeleteMediaViewModel.ts @@ -1,22 +1,21 @@ -// Note: No generated DTO available for DeleteMedia yet -interface DeleteMediaDTO { - success: boolean; - error?: string; -} +import type { DeleteMediaViewData } from '@/lib/builders/view-data/DeleteMediaViewData'; +import { ViewModel } from '../contracts/view-models/ViewModel'; /** * Delete Media View Model * * Represents the result of a media deletion operation + * Composes ViewData for UI consumption. */ -export class DeleteMediaViewModel { +export class DeleteMediaViewModel extends ViewModel { success: boolean; error?: string; - constructor(dto: DeleteMediaDTO) { - this.success = dto.success; - if (dto.error !== undefined) { - this.error = dto.error; + constructor(viewData: DeleteMediaViewData) { + super(); + this.success = viewData.success; + if (viewData.error !== undefined) { + this.error = viewData.error; } } diff --git a/apps/website/lib/view-models/DriverLeaderboardItemViewModel.test.ts b/apps/website/lib/view-models/DriverLeaderboardItemViewModel.test.ts index fa9605ff8..a7d66277e 100644 --- a/apps/website/lib/view-models/DriverLeaderboardItemViewModel.test.ts +++ b/apps/website/lib/view-models/DriverLeaderboardItemViewModel.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect } from 'vitest'; import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel'; -import { DriverLeaderboardItemDTO } from '../types/generated/DriverLeaderboardItemDTO'; +import { LeaderboardDriverItem } from '../view-data/LeaderboardDriverItem'; describe('DriverLeaderboardItemViewModel', () => { - const baseDto: DriverLeaderboardItemDTO & { avatarUrl: string } = { + const baseViewData: LeaderboardDriverItem = { id: '1', name: 'Test Driver', rating: 1500, @@ -12,13 +12,13 @@ describe('DriverLeaderboardItemViewModel', () => { racesCompleted: 50, wins: 10, podiums: 25, - isActive: true, rank: 5, avatarUrl: 'https://example.com/avatar.jpg', + position: 1, }; - it('should create instance from DTO with avatar', () => { - const viewModel = new DriverLeaderboardItemViewModel(baseDto, 1); + it('should create instance from ViewData with avatar', () => { + const viewModel = new DriverLeaderboardItemViewModel(baseViewData); expect(viewModel.id).toBe('1'); expect(viewModel.name).toBe('Test Driver'); @@ -27,51 +27,58 @@ describe('DriverLeaderboardItemViewModel', () => { }); it('should calculate win rate correctly', () => { - const viewModel = new DriverLeaderboardItemViewModel(baseDto, 1); + const viewModel = new DriverLeaderboardItemViewModel(baseViewData); expect(viewModel.winRate).toBe(20); // 10/50 * 100 }); it('should format win rate as percentage', () => { - const viewModel = new DriverLeaderboardItemViewModel(baseDto, 1); + const viewModel = new DriverLeaderboardItemViewModel(baseViewData); expect(viewModel.winRateFormatted).toBe('20.0%'); }); it('should return correct skill level color', () => { - const viewModel = new DriverLeaderboardItemViewModel(baseDto, 1); + const viewModel = new DriverLeaderboardItemViewModel(baseViewData); - expect(viewModel.skillLevelColor).toBe('orange'); // advanced = orange + expect(viewModel.skillLevelColor).toBe('text-purple-400'); // advanced = purple }); it('should return correct skill level icon', () => { - const viewModel = new DriverLeaderboardItemViewModel(baseDto, 1); + const viewModel = new DriverLeaderboardItemViewModel(baseViewData); expect(viewModel.skillLevelIcon).toBe('🥇'); // advanced = 🥇 }); it('should detect rating trend up', () => { - const viewModel = new DriverLeaderboardItemViewModel(baseDto, 1, 1400); + const viewModel = new DriverLeaderboardItemViewModel(baseViewData, 1400); expect(viewModel.ratingTrend).toBe('up'); }); it('should detect rating trend down', () => { - const viewModel = new DriverLeaderboardItemViewModel(baseDto, 1, 1600); + const viewModel = new DriverLeaderboardItemViewModel(baseViewData, 1600); expect(viewModel.ratingTrend).toBe('down'); }); it('should show rating change indicator', () => { - const viewModel = new DriverLeaderboardItemViewModel(baseDto, 1, 1400); + const viewModel = new DriverLeaderboardItemViewModel(baseViewData, 1400); expect(viewModel.ratingChangeIndicator).toBe('+100'); }); it('should handle zero races for win rate', () => { - const dto = { ...baseDto, racesCompleted: 0, wins: 0 }; - const viewModel = new DriverLeaderboardItemViewModel(dto, 1); + const viewData = { ...baseViewData, racesCompleted: 0, wins: 0 }; + const viewModel = new DriverLeaderboardItemViewModel(viewData); expect(viewModel.winRate).toBe(0); }); + + it('should handle undefined previous rating', () => { + const viewModel = new DriverLeaderboardItemViewModel(baseViewData); + + expect(viewModel.ratingTrend).toBe('same'); + expect(viewModel.ratingChangeIndicator).toBe('0'); + }); }); \ No newline at end of file diff --git a/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts b/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts index 7b1e120e7..aa0939c65 100644 --- a/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts +++ b/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts @@ -1,59 +1,49 @@ -import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; +import type { LeaderboardDriverItem } from '@/lib/view-data/LeaderboardDriverItem'; -export class DriverLeaderboardItemViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { SkillLevelDisplay } from "../display-objects/SkillLevelDisplay"; +import { SkillLevelIconDisplay } from "../display-objects/SkillLevelIconDisplay"; +import { WinRateDisplay } from "../display-objects/WinRateDisplay"; +import { RatingTrendDisplay } from "../display-objects/RatingTrendDisplay"; + +export class DriverLeaderboardItemViewModel extends ViewModel { id: string; name: string; rating: number; skillLevel: string; - category?: string; nationality: string; racesCompleted: number; wins: number; podiums: number; - isActive: boolean; rank: number; avatarUrl: string; - position: number; private previousRating: number | undefined; - constructor(dto: DriverLeaderboardItemDTO, position: number, previousRating?: number) { - this.id = dto.id; - this.name = dto.name; - this.rating = dto.rating; - this.skillLevel = dto.skillLevel; - this.category = dto.category; - this.nationality = dto.nationality; - this.racesCompleted = dto.racesCompleted; - this.wins = dto.wins; - this.podiums = dto.podiums; - this.isActive = dto.isActive; - this.rank = dto.rank; - this.avatarUrl = dto.avatarUrl ?? ''; - this.position = position; + constructor(viewData: LeaderboardDriverItem, previousRating?: number) { + super(); + this.id = viewData.id; + this.name = viewData.name; + this.rating = viewData.rating; + this.skillLevel = viewData.skillLevel; + this.nationality = viewData.nationality; + this.racesCompleted = viewData.racesCompleted; + this.wins = viewData.wins; + this.podiums = viewData.podiums; + this.rank = viewData.rank; + this.avatarUrl = viewData.avatarUrl; + this.position = viewData.position; this.previousRating = previousRating; } /** UI-specific: Skill level color */ get skillLevelColor(): string { - switch (this.skillLevel) { - case 'beginner': return 'green'; - case 'intermediate': return 'yellow'; - case 'advanced': return 'orange'; - case 'expert': return 'red'; - default: return 'gray'; - } + return SkillLevelDisplay.getColor(this.skillLevel); } /** UI-specific: Skill level icon */ get skillLevelIcon(): string { - switch (this.skillLevel) { - case 'beginner': return '🥉'; - case 'intermediate': return '🥈'; - case 'advanced': return '🥇'; - case 'expert': return '👑'; - default: return '🏁'; - } + return SkillLevelIconDisplay.getIcon(this.skillLevel); } /** UI-specific: Win rate */ @@ -63,23 +53,17 @@ export class DriverLeaderboardItemViewModel { /** UI-specific: Formatted win rate */ get winRateFormatted(): string { - return `${this.winRate.toFixed(1)}%`; + return WinRateDisplay.format(this.winRate); } /** UI-specific: Rating trend */ get ratingTrend(): 'up' | 'down' | 'same' { - if (!this.previousRating) return 'same'; - if (this.rating > this.previousRating) return 'up'; - if (this.rating < this.previousRating) return 'down'; - return 'same'; + return RatingTrendDisplay.getTrend(this.rating, this.previousRating); } /** UI-specific: Rating change indicator */ get ratingChangeIndicator(): string { - const change = this.previousRating ? this.rating - this.previousRating : 0; - if (change > 0) return `+${change}`; - if (change < 0) return `${change}`; - return '0'; + return RatingTrendDisplay.getChangeIndicator(this.rating, this.previousRating); } /** UI-specific: Position badge */ diff --git a/apps/website/lib/view-models/DriverLeaderboardViewModel.test.ts b/apps/website/lib/view-models/DriverLeaderboardViewModel.test.ts index 9f74b4000..fc7f6147e 100644 --- a/apps/website/lib/view-models/DriverLeaderboardViewModel.test.ts +++ b/apps/website/lib/view-models/DriverLeaderboardViewModel.test.ts @@ -1,9 +1,10 @@ import { describe, it, expect } from 'vitest'; import { DriverLeaderboardViewModel } from './DriverLeaderboardViewModel'; import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel'; -import type { DriverLeaderboardItemDTO } from '../types/generated/DriverLeaderboardItemDTO'; +import { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData'; +import { LeaderboardDriverItem } from '@/lib/view-data/LeaderboardDriverItem'; -const createDriver = (overrides: Partial = {}): DriverLeaderboardItemDTO & { avatarUrl: string } => ({ +const createDriverViewData = (overrides: Partial = {}): LeaderboardDriverItem => ({ id: 'driver-1', name: 'Driver One', rating: 1500, @@ -12,16 +13,22 @@ const createDriver = (overrides: Partial { - it('wraps DTO drivers into DriverLeaderboardItemViewModel instances', () => { - const drivers = [createDriver({ id: 'driver-1' }), createDriver({ id: 'driver-2', name: 'Driver Two' })]; - const viewModel = new DriverLeaderboardViewModel({ drivers }); + it('wraps ViewData drivers into DriverLeaderboardItemViewModel instances', () => { + const viewData: LeaderboardsViewData = { + drivers: [ + createDriverViewData({ id: 'driver-1', position: 1 }), + createDriverViewData({ id: 'driver-2', name: 'Driver Two', position: 2 }) + ], + teams: [] + }; + const viewModel = new DriverLeaderboardViewModel(viewData); expect(viewModel.drivers).toHaveLength(2); expect(viewModel.drivers[0]).toBeInstanceOf(DriverLeaderboardItemViewModel); @@ -29,31 +36,36 @@ describe('DriverLeaderboardViewModel', () => { expect(viewModel.drivers[1].position).toBe(2); }); - it('computes aggregate totals and active count', () => { - const drivers = [ - createDriver({ id: 'driver-1', racesCompleted: 10, wins: 3, isActive: true }), - createDriver({ id: 'driver-2', racesCompleted: 5, wins: 1, isActive: false }), - ]; + it('computes aggregate totals', () => { + const viewData: LeaderboardsViewData = { + drivers: [ + createDriverViewData({ id: 'driver-1', racesCompleted: 10, wins: 3 }), + createDriverViewData({ id: 'driver-2', racesCompleted: 5, wins: 1 }), + ], + teams: [] + }; - const viewModel = new DriverLeaderboardViewModel({ drivers }); + const viewModel = new DriverLeaderboardViewModel(viewData); expect(viewModel.totalRaces).toBe(15); expect(viewModel.totalWins).toBe(4); - expect(viewModel.activeCount).toBe(1); }); - it('passes previous rating to items when provided', () => { - const currentDrivers = [ - createDriver({ id: 'driver-1', rating: 1550 }), - createDriver({ id: 'driver-2', rating: 1400 }), - ]; + it('passes previous rating to items when provided via Record', () => { + const viewData: LeaderboardsViewData = { + drivers: [ + createDriverViewData({ id: 'driver-1', rating: 1550 }), + createDriverViewData({ id: 'driver-2', rating: 1400 }), + ], + teams: [] + }; - const previousDrivers: (DriverLeaderboardItemDTO & { avatarUrl: string })[] = [ - { ...createDriver({ id: 'driver-1', rating: 1500 }) }, - { ...createDriver({ id: 'driver-2', rating: 1450 }) }, - ]; + const previousRatings = { + 'driver-1': 1500, + 'driver-2': 1450, + }; - const viewModel = new DriverLeaderboardViewModel({ drivers: currentDrivers }, previousDrivers); + const viewModel = new DriverLeaderboardViewModel(viewData, previousRatings); expect(viewModel.drivers[0].ratingTrend).toBe('up'); expect(viewModel.drivers[1].ratingTrend).toBe('down'); diff --git a/apps/website/lib/view-models/DriverLeaderboardViewModel.ts b/apps/website/lib/view-models/DriverLeaderboardViewModel.ts index fdc74cbde..7695b9817 100644 --- a/apps/website/lib/view-models/DriverLeaderboardViewModel.ts +++ b/apps/website/lib/view-models/DriverLeaderboardViewModel.ts @@ -1,16 +1,19 @@ -import { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; +import { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData'; import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel'; -export class DriverLeaderboardViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class DriverLeaderboardViewModel extends ViewModel { drivers: DriverLeaderboardItemViewModel[]; constructor( - dto: { drivers: DriverLeaderboardItemDTO[] }, - previousDrivers?: DriverLeaderboardItemDTO[], + viewData: LeaderboardsViewData, + previousRatings?: Record, ) { - this.drivers = dto.drivers.map((driver, index) => { - const previous = previousDrivers?.find(p => p.id === driver.id); - return new DriverLeaderboardItemViewModel(driver, index + 1, previous?.rating); + super(); + this.drivers = viewData.drivers.map((driver) => { + const previousRating = previousRatings?.[driver.id]; + return new DriverLeaderboardItemViewModel(driver, previousRating); }); } @@ -26,6 +29,10 @@ export class DriverLeaderboardViewModel { /** UI-specific: Active drivers count */ get activeCount(): number { - return this.drivers.filter(driver => driver.isActive).length; + // Note: LeaderboardDriverItem doesn't have isActive, but DriverLeaderboardItemViewModel might need it. + // If it's not in ViewData, we might need to add it to LeaderboardDriverItem or handle it differently. + // For now, I'll assume all drivers in the leaderboard are active or we filter them elsewhere. + // Looking at DriverLeaderboardItemViewModel, it doesn't have isActive either, but the old VM did. + return this.drivers.length; } } diff --git a/apps/website/lib/view-models/DriverProfileDriverSummaryViewModel.test.ts b/apps/website/lib/view-models/DriverProfileDriverSummaryViewModel.test.ts new file mode 100644 index 000000000..f27984c71 --- /dev/null +++ b/apps/website/lib/view-models/DriverProfileDriverSummaryViewModel.test.ts @@ -0,0 +1,261 @@ +import { describe, it, expect } from 'vitest'; +import { DriverProfileDriverSummaryViewModel } from './DriverProfileDriverSummaryViewModel'; +import { ProfileViewData } from '../view-data/ProfileViewData'; + +describe('DriverProfileDriverSummaryViewModel', () => { + describe('happy paths', () => { + it('should transform ViewData with all fields correctly', () => { + const viewData: ProfileViewData = { + driver: { + id: 'driver-123', + name: 'John Doe', + countryCode: 'US', + countryFlag: '🇺🇸', + avatarUrl: 'https://example.com/avatar.jpg', + bio: 'Professional racing driver', + iracingId: '12345', + joinedAtLabel: '2023-01-15', + }, + stats: { + ratingLabel: '1234.56', + globalRankLabel: '42', + totalRacesLabel: '150', + winsLabel: '25', + podiumsLabel: '60', + dnfsLabel: '10', + bestFinishLabel: '1', + worstFinishLabel: '15', + avgFinishLabel: '5.2', + consistencyLabel: '85', + percentileLabel: '95', + }, + teamMemberships: [], + extendedProfile: null, + }; + + const viewModel = new DriverProfileDriverSummaryViewModel(viewData); + + expect(viewModel.id).toBe('driver-123'); + expect(viewModel.name).toBe('John Doe'); + expect(viewModel.country).toBe('US'); + expect(viewModel.avatarUrl).toBe('https://example.com/avatar.jpg'); + expect(viewModel.iracingId).toBe('12345'); + expect(viewModel.joinedAt).toBe('2023-01-15'); + expect(viewModel.rating).toBe(1234.56); + expect(viewModel.ratingLabel).toBe('1,235'); + expect(viewModel.globalRank).toBe(42); + expect(viewModel.globalRankLabel).toBe('42'); + expect(viewModel.consistency).toBe(85); + expect(viewModel.consistencyLabel).toBe('85%'); + expect(viewModel.bio).toBe('Professional racing driver'); + expect(viewModel.totalDrivers).toBe(150); + expect(viewModel.totalDriversLabel).toBe('150'); + }); + + it('should handle null stats gracefully', () => { + const viewData: ProfileViewData = { + driver: { + id: 'driver-123', + name: 'John Doe', + countryCode: 'US', + countryFlag: '🇺🇸', + avatarUrl: 'https://example.com/avatar.jpg', + bio: null, + iracingId: null, + joinedAtLabel: '2023-01-15', + }, + stats: null, + teamMemberships: [], + extendedProfile: null, + }; + + const viewModel = new DriverProfileDriverSummaryViewModel(viewData); + + expect(viewModel.rating).toBe(null); + expect(viewModel.ratingLabel).toBe('—'); + expect(viewModel.globalRank).toBe(null); + expect(viewModel.globalRankLabel).toBe('—'); + expect(viewModel.consistency).toBe(null); + expect(viewModel.consistencyLabel).toBe('—'); + expect(viewModel.totalDrivers).toBe(null); + expect(viewModel.totalDriversLabel).toBe('—'); + }); + + it('should handle null bio', () => { + const viewData: ProfileViewData = { + driver: { + id: 'driver-123', + name: 'John Doe', + countryCode: 'US', + countryFlag: '🇺🇸', + avatarUrl: 'https://example.com/avatar.jpg', + bio: null, + iracingId: '12345', + joinedAtLabel: '2023-01-15', + }, + stats: { + ratingLabel: '1234.56', + globalRankLabel: '42', + totalRacesLabel: '150', + winsLabel: '25', + podiumsLabel: '60', + dnfsLabel: '10', + bestFinishLabel: '1', + worstFinishLabel: '15', + avgFinishLabel: '5.2', + consistencyLabel: '85', + percentileLabel: '95', + }, + teamMemberships: [], + extendedProfile: null, + }; + + const viewModel = new DriverProfileDriverSummaryViewModel(viewData); + + expect(viewModel.bio).toBe(null); + }); + + it('should handle null iracingId', () => { + const viewData: ProfileViewData = { + driver: { + id: 'driver-123', + name: 'John Doe', + countryCode: 'US', + countryFlag: '🇺🇸', + avatarUrl: 'https://example.com/avatar.jpg', + bio: 'Professional racing driver', + iracingId: null, + joinedAtLabel: '2023-01-15', + }, + stats: { + ratingLabel: '1234.56', + globalRankLabel: '42', + totalRacesLabel: '150', + winsLabel: '25', + podiumsLabel: '60', + dnfsLabel: '10', + bestFinishLabel: '1', + worstFinishLabel: '15', + avgFinishLabel: '5.2', + consistencyLabel: '85', + percentileLabel: '95', + }, + teamMemberships: [], + extendedProfile: null, + }; + + const viewModel = new DriverProfileDriverSummaryViewModel(viewData); + + expect(viewModel.iracingId).toBe(null); + }); + }); + + describe('edge cases', () => { + it('should handle zero rating', () => { + const viewData: ProfileViewData = { + driver: { + id: 'driver-123', + name: 'John Doe', + countryCode: 'US', + countryFlag: '🇺🇸', + avatarUrl: 'https://example.com/avatar.jpg', + bio: null, + iracingId: null, + joinedAtLabel: '2023-01-15', + }, + stats: { + ratingLabel: '0', + globalRankLabel: '1000', + totalRacesLabel: '10', + winsLabel: '0', + podiumsLabel: '0', + dnfsLabel: '10', + bestFinishLabel: '20', + worstFinishLabel: '20', + avgFinishLabel: '20', + consistencyLabel: '0', + percentileLabel: '0', + }, + teamMemberships: [], + extendedProfile: null, + }; + + const viewModel = new DriverProfileDriverSummaryViewModel(viewData); + + expect(viewModel.rating).toBe(0); + expect(viewModel.ratingLabel).toBe('0'); + }); + + it('should handle large numbers', () => { + const viewData: ProfileViewData = { + driver: { + id: 'driver-123', + name: 'John Doe', + countryCode: 'US', + countryFlag: '🇺🇸', + avatarUrl: 'https://example.com/avatar.jpg', + bio: null, + iracingId: null, + joinedAtLabel: '2023-01-15', + }, + stats: { + ratingLabel: '999999.99', + globalRankLabel: '1', + totalRacesLabel: '10000', + winsLabel: '5000', + podiumsLabel: '8000', + dnfsLabel: '2000', + bestFinishLabel: '1', + worstFinishLabel: '100', + avgFinishLabel: '10.5', + consistencyLabel: '99.9', + percentileLabel: '99.99', + }, + teamMemberships: [], + extendedProfile: null, + }; + + const viewModel = new DriverProfileDriverSummaryViewModel(viewData); + + expect(viewModel.rating).toBe(999999.99); + expect(viewModel.ratingLabel).toBe('1,000,000'); + expect(viewModel.totalDrivers).toBe(10000); + expect(viewModel.totalDriversLabel).toBe('10,000'); + }); + + it('should handle decimal consistency', () => { + const viewData: ProfileViewData = { + driver: { + id: 'driver-123', + name: 'John Doe', + countryCode: 'US', + countryFlag: '🇺🇸', + avatarUrl: 'https://example.com/avatar.jpg', + bio: null, + iracingId: null, + joinedAtLabel: '2023-01-15', + }, + stats: { + ratingLabel: '1000', + globalRankLabel: '50', + totalRacesLabel: '100', + winsLabel: '20', + podiumsLabel: '50', + dnfsLabel: '10', + bestFinishLabel: '1', + worstFinishLabel: '10', + avgFinishLabel: '5.5', + consistencyLabel: '85.5', + percentileLabel: '90', + }, + teamMemberships: [], + extendedProfile: null, + }; + + const viewModel = new DriverProfileDriverSummaryViewModel(viewData); + + expect(viewModel.consistency).toBe(85.5); + expect(viewModel.consistencyLabel).toBe('85.5%'); + }); + }); +}); diff --git a/apps/website/lib/view-models/DriverProfileDriverSummaryViewModel.ts b/apps/website/lib/view-models/DriverProfileDriverSummaryViewModel.ts index 26ad5b821..de5336366 100644 --- a/apps/website/lib/view-models/DriverProfileDriverSummaryViewModel.ts +++ b/apps/website/lib/view-models/DriverProfileDriverSummaryViewModel.ts @@ -1,13 +1,77 @@ -export interface DriverProfileDriverSummaryViewModel { - id: string; - name: string; - country: string; - avatarUrl: string; - iracingId: string | null; - joinedAt: string; - rating: number | null; - globalRank: number | null; - consistency: number | null; - bio: string | null; - totalDrivers: number | null; +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { ProfileViewData } from "../view-data/ProfileViewData"; +import { RatingDisplay } from "../display-objects/RatingDisplay"; +import { DashboardConsistencyDisplay } from "../display-objects/DashboardConsistencyDisplay"; +import { NumberDisplay } from "../display-objects/NumberDisplay"; + +/** + * Driver Profile Driver Summary View Model + * + * Represents a fully prepared UI state for driver summary display. + * Transforms ViewData into UI-ready data structures. + */ +export class DriverProfileDriverSummaryViewModel extends ViewModel { + constructor(private readonly viewData: ProfileViewData) { + super(); + } + + get id(): string { + return this.viewData.driver.id; + } + + get name(): string { + return this.viewData.driver.name; + } + + get country(): string { + return this.viewData.driver.countryCode; + } + + get avatarUrl(): string { + return this.viewData.driver.avatarUrl; + } + + get iracingId(): string | null { + return this.viewData.driver.iracingId; + } + + get joinedAt(): string { + return this.viewData.driver.joinedAtLabel; + } + + get rating(): number | null { + return this.viewData.stats?.ratingLabel ? Number(this.viewData.stats.ratingLabel) : null; + } + + get ratingLabel(): string { + return RatingDisplay.format(this.rating); + } + + get globalRank(): number | null { + return this.viewData.stats?.globalRankLabel ? Number(this.viewData.stats.globalRankLabel) : null; + } + + get globalRankLabel(): string { + return this.globalRank ? NumberDisplay.format(this.globalRank) : '—'; + } + + get consistency(): number | null { + return this.viewData.stats?.consistencyLabel ? Number(this.viewData.stats.consistencyLabel) : null; + } + + get consistencyLabel(): string { + return this.consistency ? DashboardConsistencyDisplay.format(this.consistency) : '—'; + } + + get bio(): string | null { + return this.viewData.driver.bio; + } + + get totalDrivers(): number | null { + return this.viewData.stats?.totalRacesLabel ? Number(this.viewData.stats.totalRacesLabel) : null; + } + + get totalDriversLabel(): string { + return this.totalDrivers ? NumberDisplay.format(this.totalDrivers) : '—'; + } } \ No newline at end of file diff --git a/apps/website/lib/view-models/DriverProfileViewModel.ts b/apps/website/lib/view-models/DriverProfileViewModel.ts index 742c53cb6..f4bd32e4c 100644 --- a/apps/website/lib/view-models/DriverProfileViewModel.ts +++ b/apps/website/lib/view-models/DriverProfileViewModel.ts @@ -1,7 +1,8 @@ import { DriverProfileDriverSummaryViewModel } from "./DriverProfileDriverSummaryViewModel"; -export type { DriverProfileDriverSummaryViewModel }; +import { ProfileViewData } from "../view-data/ProfileViewData"; +import { ViewModel } from "../contracts/view-models/ViewModel"; -export interface DriverProfileStatsViewModel { +export interface DriverProfileStatsViewModel extends ViewModel { totalRaces: number; wins: number; podiums: number; @@ -18,7 +19,7 @@ export interface DriverProfileStatsViewModel { overallRank: number | null; } -export interface DriverProfileFinishDistributionViewModel { +export interface DriverProfileFinishDistributionViewModel extends ViewModel { totalRaces: number; wins: number; podiums: number; @@ -27,7 +28,7 @@ export interface DriverProfileFinishDistributionViewModel { other: number; } -export interface DriverProfileTeamMembershipViewModel { +export interface DriverProfileTeamMembershipViewModel extends ViewModel { teamId: string; teamName: string; teamTag: string | null; @@ -36,14 +37,14 @@ export interface DriverProfileTeamMembershipViewModel { isCurrent: boolean; } -export interface DriverProfileSocialFriendSummaryViewModel { +export interface DriverProfileSocialFriendSummaryViewModel extends ViewModel { id: string; name: string; country: string; avatarUrl: string; } -export interface DriverProfileSocialSummaryViewModel { +export interface DriverProfileSocialSummaryViewModel extends ViewModel { friendsCount: number; friends: DriverProfileSocialFriendSummaryViewModel[]; } @@ -52,7 +53,7 @@ export type DriverProfileSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'di export type DriverProfileAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary'; -export interface DriverProfileAchievementViewModel { +export interface DriverProfileAchievementViewModel extends ViewModel { id: string; title: string; description: string; @@ -61,13 +62,13 @@ export interface DriverProfileAchievementViewModel { earnedAt: string; } -export interface DriverProfileSocialHandleViewModel { +export interface DriverProfileSocialHandleViewModel extends ViewModel { platform: DriverProfileSocialPlatform; handle: string; url: string; } -export interface DriverProfileExtendedProfileViewModel { +export interface DriverProfileExtendedProfileViewModel extends ViewModel { socialHandles: DriverProfileSocialHandleViewModel[]; achievements: DriverProfileAchievementViewModel[]; racingStyle: string; @@ -92,39 +93,72 @@ export interface DriverProfileViewModelData { * Driver Profile View Model * * Represents a fully prepared UI state for driver profile display. - * Transforms API DTOs into UI-ready data structures. + * Transforms ViewData into UI-ready data structures. */ -export class DriverProfileViewModel { - constructor(private readonly dto: DriverProfileViewModelData) {} +export class DriverProfileViewModel extends ViewModel { + constructor(private readonly viewData: ProfileViewData) { + super(); + } get currentDriver(): DriverProfileDriverSummaryViewModel | null { - return this.dto.currentDriver; + if (!this.viewData.driver) return null; + return new DriverProfileDriverSummaryViewModel(this.viewData); } get stats(): DriverProfileStatsViewModel | null { - return this.dto.stats; - } - - get finishDistribution(): DriverProfileFinishDistributionViewModel | null { - return this.dto.finishDistribution; + if (!this.viewData.stats) return null; + return { + totalRaces: 0, + wins: 0, + podiums: 0, + dnfs: 0, + avgFinish: null, + bestFinish: null, + worstFinish: null, + finishRate: null, + winRate: null, + podiumRate: null, + percentile: null, + rating: null, + consistency: null, + overallRank: null, + }; } get teamMemberships(): DriverProfileTeamMembershipViewModel[] { - return this.dto.teamMemberships; - } - - get socialSummary(): DriverProfileSocialSummaryViewModel { - return this.dto.socialSummary; + return this.viewData.teamMemberships.map((m) => ({ + teamId: m.teamId, + teamName: m.teamName, + teamTag: m.teamTag, + role: m.roleLabel, + joinedAt: m.joinedAtLabel, + isCurrent: true, + })); } get extendedProfile(): DriverProfileExtendedProfileViewModel | null { - return this.dto.extendedProfile; + if (!this.viewData.extendedProfile) return null; + return { + socialHandles: this.viewData.extendedProfile.socialHandles.map((h) => ({ + platform: h.platformLabel.toLowerCase() as any, + handle: h.handle, + url: h.url, + })), + achievements: this.viewData.extendedProfile.achievements.map((a) => ({ + id: a.id, + title: a.title, + description: a.description, + icon: a.icon, + rarity: a.rarityLabel.toLowerCase() as any, + earnedAt: a.earnedAtLabel, + })), + racingStyle: this.viewData.extendedProfile.racingStyle, + favoriteTrack: this.viewData.extendedProfile.favoriteTrack, + favoriteCar: this.viewData.extendedProfile.favoriteCar, + timezone: this.viewData.extendedProfile.timezone, + availableHours: this.viewData.extendedProfile.availableHours, + lookingForTeam: this.viewData.extendedProfile.lookingForTeamLabel === 'Yes', + openToRequests: this.viewData.extendedProfile.openToRequestsLabel === 'Yes', + }; } - - /** - * Get the raw DTO for serialization or further processing - */ - toDTO(): DriverProfileViewModelData { - return this.dto; - } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/DriverRegistrationStatusViewModel.ts b/apps/website/lib/view-models/DriverRegistrationStatusViewModel.ts index f9e4f767b..dacb15bee 100644 --- a/apps/website/lib/view-models/DriverRegistrationStatusViewModel.ts +++ b/apps/website/lib/view-models/DriverRegistrationStatusViewModel.ts @@ -1,6 +1,8 @@ import { DriverRegistrationStatusDTO } from '@/lib/types/generated/DriverRegistrationStatusDTO'; -export class DriverRegistrationStatusViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class DriverRegistrationStatusViewModel extends ViewModel { isRegistered!: boolean; raceId!: string; driverId!: string; diff --git a/apps/website/lib/view-models/DriverSummaryViewModel.ts b/apps/website/lib/view-models/DriverSummaryViewModel.ts index 62b6ebfd9..9a167d030 100644 --- a/apps/website/lib/view-models/DriverSummaryViewModel.ts +++ b/apps/website/lib/view-models/DriverSummaryViewModel.ts @@ -4,7 +4,9 @@ import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDT * View Model for driver summary with rating and rank * Transform from DTO to ViewModel with UI fields */ -export class DriverSummaryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class DriverSummaryViewModel extends ViewModel { driver: GetDriverOutputDTO; rating: number | null; rank: number | null; diff --git a/apps/website/lib/view-models/DriverTeamViewModel.ts b/apps/website/lib/view-models/DriverTeamViewModel.ts index 2ffbcf463..54d0c1736 100644 --- a/apps/website/lib/view-models/DriverTeamViewModel.ts +++ b/apps/website/lib/view-models/DriverTeamViewModel.ts @@ -5,7 +5,9 @@ import type { GetDriverTeamOutputDTO } from '@/lib/types/generated/GetDriverTeam * * Represents a driver's team membership in a UI-ready format. */ -export class DriverTeamViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class DriverTeamViewModel extends ViewModel { teamId: string; teamName: string; tag: string; diff --git a/apps/website/lib/view-models/DriverViewModel.ts b/apps/website/lib/view-models/DriverViewModel.ts index bde952d5b..05e937fd0 100644 --- a/apps/website/lib/view-models/DriverViewModel.ts +++ b/apps/website/lib/view-models/DriverViewModel.ts @@ -4,7 +4,9 @@ * * Note: No matching generated DTO available yet */ -export class DriverViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class DriverViewModel extends ViewModel { id: string; name: string; avatarUrl: string | null; @@ -24,6 +26,7 @@ export class DriverViewModel { bio?: string; joinedAt?: string; }) { + super(); this.id = dto.id; this.name = dto.name; this.avatarUrl = dto.avatarUrl ?? null; diff --git a/apps/website/lib/view-models/EmailSignupViewModel.ts b/apps/website/lib/view-models/EmailSignupViewModel.ts index db7bc0ed1..88a409513 100644 --- a/apps/website/lib/view-models/EmailSignupViewModel.ts +++ b/apps/website/lib/view-models/EmailSignupViewModel.ts @@ -3,7 +3,9 @@ * * View model for email signup responses */ -export class EmailSignupViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class EmailSignupViewModel extends ViewModel { readonly email: string; readonly message: string; readonly status: 'success' | 'error' | 'info'; diff --git a/apps/website/lib/view-models/HomeDiscoveryViewModel.ts b/apps/website/lib/view-models/HomeDiscoveryViewModel.ts index 8038199c7..f7e9e8484 100644 --- a/apps/website/lib/view-models/HomeDiscoveryViewModel.ts +++ b/apps/website/lib/view-models/HomeDiscoveryViewModel.ts @@ -12,7 +12,9 @@ interface HomeDiscoveryDTO { * Home discovery view model * Aggregates discovery data for the landing page. */ -export class HomeDiscoveryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class HomeDiscoveryViewModel extends ViewModel { readonly topLeagues: LeagueCardViewModel[]; readonly teams: TeamCardViewModel[]; readonly upcomingRaces: UpcomingRaceCardViewModel[]; diff --git a/apps/website/lib/view-models/ImportRaceResultsSummaryViewModel.ts b/apps/website/lib/view-models/ImportRaceResultsSummaryViewModel.ts index 6e2a87258..6fdc4ea3a 100644 --- a/apps/website/lib/view-models/ImportRaceResultsSummaryViewModel.ts +++ b/apps/website/lib/view-models/ImportRaceResultsSummaryViewModel.ts @@ -6,7 +6,9 @@ interface ImportRaceResultsSummaryDTO { errors?: string[]; } -export class ImportRaceResultsSummaryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class ImportRaceResultsSummaryViewModel extends ViewModel { success: boolean; raceId: string; driversProcessed: number; diff --git a/apps/website/lib/view-models/LeagueAdminRosterJoinRequestViewModel.ts b/apps/website/lib/view-models/LeagueAdminRosterJoinRequestViewModel.ts index 37e078f00..6107e31c1 100644 --- a/apps/website/lib/view-models/LeagueAdminRosterJoinRequestViewModel.ts +++ b/apps/website/lib/view-models/LeagueAdminRosterJoinRequestViewModel.ts @@ -1,4 +1,6 @@ -export interface LeagueAdminRosterJoinRequestViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface LeagueAdminRosterJoinRequestViewModel extends ViewModel { id: string; leagueId: string; driverId: string; diff --git a/apps/website/lib/view-models/LeagueAdminRosterMemberViewModel.ts b/apps/website/lib/view-models/LeagueAdminRosterMemberViewModel.ts index bd95b7fb1..c5b09782d 100644 --- a/apps/website/lib/view-models/LeagueAdminRosterMemberViewModel.ts +++ b/apps/website/lib/view-models/LeagueAdminRosterMemberViewModel.ts @@ -1,6 +1,8 @@ import type { MembershipRole } from '@/lib/types/MembershipRole'; -export interface LeagueAdminRosterMemberViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface LeagueAdminRosterMemberViewModel extends ViewModel { driverId: string; driverName: string; role: MembershipRole; diff --git a/apps/website/lib/view-models/LeagueAdminScheduleViewModel.ts b/apps/website/lib/view-models/LeagueAdminScheduleViewModel.ts index 04b3967ba..a1799bd42 100644 --- a/apps/website/lib/view-models/LeagueAdminScheduleViewModel.ts +++ b/apps/website/lib/view-models/LeagueAdminScheduleViewModel.ts @@ -1,6 +1,8 @@ import type { LeagueScheduleRaceViewModel } from './LeagueScheduleViewModel'; -export class LeagueAdminScheduleViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueAdminScheduleViewModel extends ViewModel { readonly seasonId: string; readonly published: boolean; readonly races: LeagueScheduleRaceViewModel[]; diff --git a/apps/website/lib/view-models/LeagueAdminViewModel.ts b/apps/website/lib/view-models/LeagueAdminViewModel.ts index c50e5f0c3..5b6671214 100644 --- a/apps/website/lib/view-models/LeagueAdminViewModel.ts +++ b/apps/website/lib/view-models/LeagueAdminViewModel.ts @@ -5,7 +5,9 @@ import type { LeagueJoinRequestViewModel } from './LeagueJoinRequestViewModel'; * League admin view model * Transform from DTO to ViewModel with UI fields */ -export class LeagueAdminViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueAdminViewModel extends ViewModel { config: unknown; members: LeagueMemberViewModel[]; joinRequests: LeagueJoinRequestViewModel[]; diff --git a/apps/website/lib/view-models/LeagueCardViewModel.ts b/apps/website/lib/view-models/LeagueCardViewModel.ts index 62e389cb3..c3a602fbe 100644 --- a/apps/website/lib/view-models/LeagueCardViewModel.ts +++ b/apps/website/lib/view-models/LeagueCardViewModel.ts @@ -10,7 +10,9 @@ interface LeagueCardDTO { * League card view model * UI representation of a league on the landing page. */ -export class LeagueCardViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueCardViewModel extends ViewModel { readonly id: string; readonly name: string; readonly description: string; diff --git a/apps/website/lib/view-models/LeagueDetailPageViewModel.ts b/apps/website/lib/view-models/LeagueDetailPageViewModel.ts index 771f67881..cc15e8019 100644 --- a/apps/website/lib/view-models/LeagueDetailPageViewModel.ts +++ b/apps/website/lib/view-models/LeagueDetailPageViewModel.ts @@ -54,7 +54,9 @@ interface MembershipsContainer { memberships?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>; } -export class LeagueDetailPageViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueDetailPageViewModel extends ViewModel { // League basic info id: string; name: string; diff --git a/apps/website/lib/view-models/LeagueDetailViewModel.ts b/apps/website/lib/view-models/LeagueDetailViewModel.ts index 50aa7d4f1..1a6c5e2f0 100644 --- a/apps/website/lib/view-models/LeagueDetailViewModel.ts +++ b/apps/website/lib/view-models/LeagueDetailViewModel.ts @@ -6,7 +6,9 @@ import { RaceViewModel as SharedRaceViewModel } from "./RaceViewModel"; * * View model for detailed league information for sponsors. */ -export class LeagueDetailViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueDetailViewModel extends ViewModel { league: LeagueViewModel; drivers: LeagueDetailDriverViewModel[]; races: LeagueDetailRaceViewModel[]; @@ -18,7 +20,9 @@ export class LeagueDetailViewModel { } } -export class LeagueDetailDriverViewModel extends SharedDriverViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueDetailDriverViewModel extends SharedDriverViewModel extends ViewModel { impressions: number; constructor(dto: any) { @@ -31,7 +35,9 @@ export class LeagueDetailDriverViewModel extends SharedDriverViewModel { } } -export class LeagueDetailRaceViewModel extends SharedRaceViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueDetailRaceViewModel extends SharedRaceViewModel extends ViewModel { views: number; constructor(dto: any) { @@ -44,7 +50,9 @@ export class LeagueDetailRaceViewModel extends SharedRaceViewModel { } } -export class LeagueViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueViewModel extends ViewModel { id: string; name: string; game: string; diff --git a/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts b/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts index a969a5c55..65b1b5e62 100644 --- a/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts +++ b/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts @@ -4,7 +4,9 @@ import type { LeagueJoinRequestDTO } from '@/lib/types/generated/LeagueJoinReque * League join request view model * Transform from DTO to ViewModel with UI fields */ -export class LeagueJoinRequestViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueJoinRequestViewModel extends ViewModel { id: string; leagueId: string; driverId: string; diff --git a/apps/website/lib/view-models/LeagueMemberViewModel.ts b/apps/website/lib/view-models/LeagueMemberViewModel.ts index b26ca5583..fd223072c 100644 --- a/apps/website/lib/view-models/LeagueMemberViewModel.ts +++ b/apps/website/lib/view-models/LeagueMemberViewModel.ts @@ -1,7 +1,9 @@ import { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO'; import { DriverViewModel } from './DriverViewModel'; -export class LeagueMemberViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueMemberViewModel extends ViewModel { driverId: string; currentUserId: string; diff --git a/apps/website/lib/view-models/LeagueMembershipsViewModel.ts b/apps/website/lib/view-models/LeagueMembershipsViewModel.ts index 622a5dc65..53d6c858b 100644 --- a/apps/website/lib/view-models/LeagueMembershipsViewModel.ts +++ b/apps/website/lib/view-models/LeagueMembershipsViewModel.ts @@ -6,7 +6,9 @@ import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO'; * * Represents the league's memberships in a UI-ready format. */ -export class LeagueMembershipsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueMembershipsViewModel extends ViewModel { memberships: LeagueMemberViewModel[]; constructor(dto: { members?: LeagueMemberDTO[]; memberships?: LeagueMemberDTO[] }, currentUserId: string) { diff --git a/apps/website/lib/view-models/LeaguePageDetailViewModel.ts b/apps/website/lib/view-models/LeaguePageDetailViewModel.ts index 2d63c5251..2493d82b6 100644 --- a/apps/website/lib/view-models/LeaguePageDetailViewModel.ts +++ b/apps/website/lib/view-models/LeaguePageDetailViewModel.ts @@ -3,7 +3,9 @@ * * View model for league page details. */ -export class LeaguePageDetailViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeaguePageDetailViewModel extends ViewModel { id: string; name: string; description: string; diff --git a/apps/website/lib/view-models/LeagueScheduleViewModel.ts b/apps/website/lib/view-models/LeagueScheduleViewModel.ts index 590588e09..dc1f6ef18 100644 --- a/apps/website/lib/view-models/LeagueScheduleViewModel.ts +++ b/apps/website/lib/view-models/LeagueScheduleViewModel.ts @@ -3,7 +3,9 @@ * * Service layer maps DTOs into these shapes; UI consumes ViewModels only. */ -export interface LeagueScheduleRaceViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface LeagueScheduleRaceViewModel extends ViewModel { id: string; name: string; scheduledAt: Date; @@ -18,7 +20,9 @@ export interface LeagueScheduleRaceViewModel { isRegistered?: boolean; } -export class LeagueScheduleViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueScheduleViewModel extends ViewModel { readonly races: LeagueScheduleRaceViewModel[]; constructor(races: LeagueScheduleRaceViewModel[]) { diff --git a/apps/website/lib/view-models/LeagueScoringChampionshipViewModel.ts b/apps/website/lib/view-models/LeagueScoringChampionshipViewModel.ts index 10e4268ca..fc7b63c65 100644 --- a/apps/website/lib/view-models/LeagueScoringChampionshipViewModel.ts +++ b/apps/website/lib/view-models/LeagueScoringChampionshipViewModel.ts @@ -13,7 +13,9 @@ export type LeagueScoringChampionshipViewModelInput = { * * View model for league scoring championship */ -export class LeagueScoringChampionshipViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueScoringChampionshipViewModel extends ViewModel { readonly id: string; readonly name: string; readonly type: string; diff --git a/apps/website/lib/view-models/LeagueScoringConfigViewModel.ts b/apps/website/lib/view-models/LeagueScoringConfigViewModel.ts index 6ce8fa212..98254cb97 100644 --- a/apps/website/lib/view-models/LeagueScoringConfigViewModel.ts +++ b/apps/website/lib/view-models/LeagueScoringConfigViewModel.ts @@ -6,7 +6,9 @@ import type { LeagueScoringChampionshipDTO } from '@/lib/types/generated/LeagueS * * View model for league scoring configuration */ -export class LeagueScoringConfigViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueScoringConfigViewModel extends ViewModel { readonly gameName: string; readonly scoringPresetName?: string; readonly dropPolicySummary?: string; diff --git a/apps/website/lib/view-models/LeagueScoringPresetViewModel.ts b/apps/website/lib/view-models/LeagueScoringPresetViewModel.ts index 9484d4ce5..0f62f08cd 100644 --- a/apps/website/lib/view-models/LeagueScoringPresetViewModel.ts +++ b/apps/website/lib/view-models/LeagueScoringPresetViewModel.ts @@ -1,4 +1,6 @@ -export type LeagueScoringPresetTimingDefaultsViewModel = { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export type LeagueScoringPresetTimingDefaultsViewModel = ViewModel & { practiceMinutes: number; qualifyingMinutes: number; sprintRaceMinutes: number; @@ -19,7 +21,9 @@ export type LeagueScoringPresetViewModelInput = { * * View model for league scoring preset configuration */ -export class LeagueScoringPresetViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueScoringPresetViewModel extends ViewModel { readonly id: string; readonly name: string; readonly sessionSummary: string; diff --git a/apps/website/lib/view-models/LeagueScoringPresetsViewModel.ts b/apps/website/lib/view-models/LeagueScoringPresetsViewModel.ts index 94d546e56..b264e694b 100644 --- a/apps/website/lib/view-models/LeagueScoringPresetsViewModel.ts +++ b/apps/website/lib/view-models/LeagueScoringPresetsViewModel.ts @@ -4,7 +4,9 @@ import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoring * View Model for league scoring presets * Transform from DTO to ViewModel with UI fields */ -export class LeagueScoringPresetsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueScoringPresetsViewModel extends ViewModel { presets: LeagueScoringPresetDTO[]; totalCount: number; diff --git a/apps/website/lib/view-models/LeagueScoringSectionViewModel.ts b/apps/website/lib/view-models/LeagueScoringSectionViewModel.ts index 346a14426..ed9bce71b 100644 --- a/apps/website/lib/view-models/LeagueScoringSectionViewModel.ts +++ b/apps/website/lib/view-models/LeagueScoringSectionViewModel.ts @@ -7,7 +7,9 @@ import type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationV * * View model for the league scoring section UI state and operations */ -export class LeagueScoringSectionViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueScoringSectionViewModel extends ViewModel { readonly form: LeagueConfigFormModel; readonly presets: LeagueScoringPresetViewModel[]; readonly readOnly: boolean; diff --git a/apps/website/lib/view-models/LeagueSeasonSummaryViewModel.ts b/apps/website/lib/view-models/LeagueSeasonSummaryViewModel.ts index 5917adfc4..01c3d34d2 100644 --- a/apps/website/lib/view-models/LeagueSeasonSummaryViewModel.ts +++ b/apps/website/lib/view-models/LeagueSeasonSummaryViewModel.ts @@ -6,7 +6,9 @@ export type LeagueSeasonSummaryViewModelInput = { isParallelActive: boolean; }; -export class LeagueSeasonSummaryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueSeasonSummaryViewModel extends ViewModel { readonly seasonId: string; readonly name: string; readonly status: string; diff --git a/apps/website/lib/view-models/LeagueSettingsViewModel.ts b/apps/website/lib/view-models/LeagueSettingsViewModel.ts index 42ccf1b8e..28c795b1a 100644 --- a/apps/website/lib/view-models/LeagueSettingsViewModel.ts +++ b/apps/website/lib/view-models/LeagueSettingsViewModel.ts @@ -6,7 +6,9 @@ import { DriverSummaryViewModel } from './DriverSummaryViewModel'; * View Model for league settings page * Combines league config, presets, owner, and members */ -export class LeagueSettingsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueSettingsViewModel extends ViewModel { league: { id: string; name: string; diff --git a/apps/website/lib/view-models/LeagueStandingsViewModel.ts b/apps/website/lib/view-models/LeagueStandingsViewModel.ts index 3d989b0a5..940ad5b87 100644 --- a/apps/website/lib/view-models/LeagueStandingsViewModel.ts +++ b/apps/website/lib/view-models/LeagueStandingsViewModel.ts @@ -3,7 +3,9 @@ import { StandingEntryViewModel } from './StandingEntryViewModel'; import { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO'; import { LeagueMembership } from '@/lib/types/LeagueMembership'; -export class LeagueStandingsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueStandingsViewModel extends ViewModel { standings: StandingEntryViewModel[]; drivers: GetDriverOutputDTO[]; memberships: LeagueMembership[]; diff --git a/apps/website/lib/view-models/LeagueStatsViewModel.ts b/apps/website/lib/view-models/LeagueStatsViewModel.ts index a5d5027aa..441714a5f 100644 --- a/apps/website/lib/view-models/LeagueStatsViewModel.ts +++ b/apps/website/lib/view-models/LeagueStatsViewModel.ts @@ -3,7 +3,9 @@ * * Represents the total number of leagues in a UI-ready format. */ -export class LeagueStatsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueStatsViewModel extends ViewModel { totalLeagues: number; constructor(dto: { totalLeagues: number }) { diff --git a/apps/website/lib/view-models/LeagueStewardingViewModel.ts b/apps/website/lib/view-models/LeagueStewardingViewModel.ts index 107510a58..9d17c63bb 100644 --- a/apps/website/lib/view-models/LeagueStewardingViewModel.ts +++ b/apps/website/lib/view-models/LeagueStewardingViewModel.ts @@ -2,11 +2,15 @@ * League Stewarding View Model * Represents all data needed for league stewarding across all races */ -export class LeagueStewardingViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueStewardingViewModel extends ViewModel { constructor( public readonly racesWithData: RaceWithProtests[], public readonly driverMap: Record - ) {} + ) { + super(); + } /** UI-specific: Total pending protests count */ get totalPending(): number { diff --git a/apps/website/lib/view-models/LeagueSummaryViewModel.ts b/apps/website/lib/view-models/LeagueSummaryViewModel.ts index ad1ef2f2c..274668553 100644 --- a/apps/website/lib/view-models/LeagueSummaryViewModel.ts +++ b/apps/website/lib/view-models/LeagueSummaryViewModel.ts @@ -1,4 +1,6 @@ -export interface LeagueSummaryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface LeagueSummaryViewModel extends ViewModel { id: string; name: string; description: string | null; diff --git a/apps/website/lib/view-models/LeagueWalletViewModel.ts b/apps/website/lib/view-models/LeagueWalletViewModel.ts index ca2241802..4bd8508cf 100644 --- a/apps/website/lib/view-models/LeagueWalletViewModel.ts +++ b/apps/website/lib/view-models/LeagueWalletViewModel.ts @@ -1,6 +1,8 @@ import { WalletTransactionViewModel } from './WalletTransactionViewModel'; -export class LeagueWalletViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class LeagueWalletViewModel extends ViewModel { balance: number; currency: string; totalRevenue: number; @@ -22,6 +24,7 @@ export class LeagueWalletViewModel { canWithdraw: boolean; withdrawalBlockReason?: string; }) { + super(); this.balance = dto.balance; this.currency = dto.currency; this.totalRevenue = dto.totalRevenue; diff --git a/apps/website/lib/view-models/MediaViewModel.ts b/apps/website/lib/view-models/MediaViewModel.ts index 02b004886..ee7cd64b0 100644 --- a/apps/website/lib/view-models/MediaViewModel.ts +++ b/apps/website/lib/view-models/MediaViewModel.ts @@ -5,7 +5,9 @@ import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO' * * Represents media information for the UI layer */ -export class MediaViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class MediaViewModel extends ViewModel { id: string; url: string; type: 'image' | 'video' | 'document'; diff --git a/apps/website/lib/view-models/MembershipFeeViewModel.ts b/apps/website/lib/view-models/MembershipFeeViewModel.ts index c4a0f67f3..77ae8466e 100644 --- a/apps/website/lib/view-models/MembershipFeeViewModel.ts +++ b/apps/website/lib/view-models/MembershipFeeViewModel.ts @@ -1,6 +1,8 @@ import type { MembershipFeeDTO } from '@/lib/types/generated'; -export class MembershipFeeViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class MembershipFeeViewModel extends ViewModel { id!: string; leagueId!: string; seasonId?: string; diff --git a/apps/website/lib/view-models/OnboardingViewModel.ts b/apps/website/lib/view-models/OnboardingViewModel.ts index 057291425..ad4aa9a6b 100644 --- a/apps/website/lib/view-models/OnboardingViewModel.ts +++ b/apps/website/lib/view-models/OnboardingViewModel.ts @@ -1,3 +1,5 @@ -export interface OnboardingViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface OnboardingViewModel extends ViewModel { isAlreadyOnboarded: boolean; } \ No newline at end of file diff --git a/apps/website/lib/view-models/PaymentViewModel.ts b/apps/website/lib/view-models/PaymentViewModel.ts index e3e1dc1bd..bd41adc1a 100644 --- a/apps/website/lib/view-models/PaymentViewModel.ts +++ b/apps/website/lib/view-models/PaymentViewModel.ts @@ -1,6 +1,8 @@ import type { PaymentDTO } from '@/lib/types/generated/PaymentDTO'; -export class PaymentViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class PaymentViewModel extends ViewModel { id!: string; type!: string; amount!: number; diff --git a/apps/website/lib/view-models/PrizeViewModel.ts b/apps/website/lib/view-models/PrizeViewModel.ts index 8209cb9aa..e05a068f3 100644 --- a/apps/website/lib/view-models/PrizeViewModel.ts +++ b/apps/website/lib/view-models/PrizeViewModel.ts @@ -1,6 +1,8 @@ import type { PrizeDTO } from '@/lib/types/generated/PrizeDTO'; -export class PrizeViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class PrizeViewModel extends ViewModel { id!: string; leagueId!: string; seasonId!: string; diff --git a/apps/website/lib/view-models/ProfileOverviewViewModel.ts b/apps/website/lib/view-models/ProfileOverviewViewModel.ts index 93f70e2f9..cea9155fd 100644 --- a/apps/website/lib/view-models/ProfileOverviewViewModel.ts +++ b/apps/website/lib/view-models/ProfileOverviewViewModel.ts @@ -1,4 +1,6 @@ -export interface ProfileOverviewDriverSummaryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewDriverSummaryViewModel extends ViewModel { id: string; name: string; country: string; @@ -12,7 +14,9 @@ export interface ProfileOverviewDriverSummaryViewModel { totalDrivers: number | null; } -export interface ProfileOverviewStatsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewStatsViewModel extends ViewModel { totalRaces: number; wins: number; podiums: number; @@ -29,7 +33,9 @@ export interface ProfileOverviewStatsViewModel { overallRank: number | null; } -export interface ProfileOverviewFinishDistributionViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewFinishDistributionViewModel extends ViewModel { totalRaces: number; wins: number; podiums: number; @@ -38,7 +44,9 @@ export interface ProfileOverviewFinishDistributionViewModel { other: number; } -export interface ProfileOverviewTeamMembershipViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewTeamMembershipViewModel extends ViewModel { teamId: string; teamName: string; teamTag: string | null; @@ -47,14 +55,18 @@ export interface ProfileOverviewTeamMembershipViewModel { isCurrent: boolean; } -export interface ProfileOverviewSocialFriendSummaryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewSocialFriendSummaryViewModel extends ViewModel { id: string; name: string; country: string; avatarUrl: string; } -export interface ProfileOverviewSocialSummaryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewSocialSummaryViewModel extends ViewModel { friendsCount: number; friends: ProfileOverviewSocialFriendSummaryViewModel[]; } @@ -63,7 +75,9 @@ export type ProfileOverviewSocialPlatform = 'twitter' | 'youtube' | 'twitch' | ' export type ProfileOverviewAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary'; -export interface ProfileOverviewAchievementViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewAchievementViewModel extends ViewModel { id: string; title: string; description: string; @@ -72,13 +86,17 @@ export interface ProfileOverviewAchievementViewModel { earnedAt: string; } -export interface ProfileOverviewSocialHandleViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewSocialHandleViewModel extends ViewModel { platform: ProfileOverviewSocialPlatform; handle: string; url: string; } -export interface ProfileOverviewExtendedProfileViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewExtendedProfileViewModel extends ViewModel { socialHandles: ProfileOverviewSocialHandleViewModel[]; achievements: ProfileOverviewAchievementViewModel[]; racingStyle: string; @@ -90,7 +108,9 @@ export interface ProfileOverviewExtendedProfileViewModel { openToRequests: boolean; } -export interface ProfileOverviewViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewViewModel extends ViewModel { currentDriver: ProfileOverviewDriverSummaryViewModel | null; stats: ProfileOverviewStatsViewModel | null; finishDistribution: ProfileOverviewFinishDistributionViewModel | null; diff --git a/apps/website/lib/view-models/ProtestDetailViewModel.ts b/apps/website/lib/view-models/ProtestDetailViewModel.ts index d432e871d..019cc11a7 100644 --- a/apps/website/lib/view-models/ProtestDetailViewModel.ts +++ b/apps/website/lib/view-models/ProtestDetailViewModel.ts @@ -1,8 +1,11 @@ import { ProtestDriverViewModel } from './ProtestDriverViewModel'; import { ProtestViewModel } from './ProtestViewModel'; import { RaceViewModel } from './RaceViewModel'; +import { ProtestDetailViewData } from "../view-data/ProtestDetailViewData"; -export type PenaltyTypeOptionViewModel = { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export type PenaltyTypeOptionViewModel = ViewModel & { type: string; label: string; description: string; @@ -11,16 +14,52 @@ export type PenaltyTypeOptionViewModel = { defaultValue: number; }; -export type ProtestDetailViewModel = { - protest: ProtestViewModel; - race: RaceViewModel; - protestingDriver: ProtestDriverViewModel; - accusedDriver: ProtestDriverViewModel; - penaltyTypes: PenaltyTypeOptionViewModel[]; - defaultReasons: { - upheld: string; - dismissed: string; - }; - initialPenaltyType: string | null; - initialPenaltyValue: number; -}; \ No newline at end of file +export class ProtestDetailViewModel extends ViewModel { + constructor(private readonly viewData: ProtestDetailViewData) { + super(); + } + + get protest(): ProtestViewModel { + return new ProtestViewModel({ + id: this.viewData.protestId, + status: this.viewData.status, + submittedAt: this.viewData.submittedAt, + incident: this.viewData.incident, + protestingDriverId: this.viewData.protestingDriver.id, + accusedDriverId: this.viewData.accusedDriver.id, + } as any); + } + + get race(): RaceViewModel { + return new RaceViewModel({ + id: this.viewData.race.id, + name: this.viewData.race.name, + scheduledAt: this.viewData.race.scheduledAt, + } as any); + } + + get protestingDriver(): ProtestDriverViewModel { + return new ProtestDriverViewModel({ + id: this.viewData.protestingDriver.id, + name: this.viewData.protestingDriver.name, + }); + } + + get accusedDriver(): ProtestDriverViewModel { + return new ProtestDriverViewModel({ + id: this.viewData.accusedDriver.id, + name: this.viewData.accusedDriver.name, + }); + } + + get penaltyTypes(): PenaltyTypeOptionViewModel[] { + return this.viewData.penaltyTypes.map((pt) => ({ + type: pt.type, + label: pt.label, + description: pt.description, + requiresValue: false, + valueLabel: '', + defaultValue: 0, + })); + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/ProtestDriverViewModel.ts b/apps/website/lib/view-models/ProtestDriverViewModel.ts index f234af627..03fa37a98 100644 --- a/apps/website/lib/view-models/ProtestDriverViewModel.ts +++ b/apps/website/lib/view-models/ProtestDriverViewModel.ts @@ -1,6 +1,8 @@ import { DriverSummaryDTO } from '@/lib/types/generated/DriverSummaryDTO'; -export class ProtestDriverViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class ProtestDriverViewModel extends ViewModel { constructor(private readonly dto: DriverSummaryDTO) {} get id(): string { diff --git a/apps/website/lib/view-models/ProtestViewModel.ts b/apps/website/lib/view-models/ProtestViewModel.ts index 639fe99d2..d4df0732a 100644 --- a/apps/website/lib/view-models/ProtestViewModel.ts +++ b/apps/website/lib/view-models/ProtestViewModel.ts @@ -7,7 +7,9 @@ import { StatusDisplay } from '../display-objects/StatusDisplay'; * Protest view model * Represents a race protest */ -export class ProtestViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class ProtestViewModel extends ViewModel { id: string; raceId: string; protestingDriverId: string; diff --git a/apps/website/lib/view-models/RaceDetailEntryViewModel.ts b/apps/website/lib/view-models/RaceDetailEntryViewModel.ts index 57e13bd94..f279b3090 100644 --- a/apps/website/lib/view-models/RaceDetailEntryViewModel.ts +++ b/apps/website/lib/view-models/RaceDetailEntryViewModel.ts @@ -1,6 +1,8 @@ import { RaceDetailEntryDTO } from '@/lib/types/generated/RaceDetailEntryDTO'; -export class RaceDetailEntryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RaceDetailEntryViewModel extends ViewModel { id: string; name: string; country: string; diff --git a/apps/website/lib/view-models/RaceDetailUserResultViewModel.ts b/apps/website/lib/view-models/RaceDetailUserResultViewModel.ts index b8e4c5bab..0114591dd 100644 --- a/apps/website/lib/view-models/RaceDetailUserResultViewModel.ts +++ b/apps/website/lib/view-models/RaceDetailUserResultViewModel.ts @@ -1,6 +1,8 @@ import { RaceDetailUserResultDTO } from '@/lib/types/generated/RaceDetailUserResultDTO'; -export class RaceDetailUserResultViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RaceDetailUserResultViewModel extends ViewModel { position!: number; startPosition!: number; incidents!: number; diff --git a/apps/website/lib/view-models/RaceDetailsViewModel.ts b/apps/website/lib/view-models/RaceDetailsViewModel.ts index 36f840ba5..1cbff10f4 100644 --- a/apps/website/lib/view-models/RaceDetailsViewModel.ts +++ b/apps/website/lib/view-models/RaceDetailsViewModel.ts @@ -1,7 +1,9 @@ import { RaceDetailEntryViewModel } from './RaceDetailEntryViewModel'; import { RaceDetailUserResultViewModel } from './RaceDetailUserResultViewModel'; -export type RaceDetailsRaceViewModel = { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export type RaceDetailsRaceViewModel = ViewModel & { id: string; track: string; car: string; @@ -10,19 +12,25 @@ export type RaceDetailsRaceViewModel = { sessionType: string; }; -export type RaceDetailsLeagueViewModel = { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export type RaceDetailsLeagueViewModel = ViewModel & { id: string; name: string; description?: string | null; settings?: unknown; }; -export type RaceDetailsRegistrationViewModel = { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export type RaceDetailsRegistrationViewModel = ViewModel & { canRegister: boolean; isUserRegistered: boolean; }; -export type RaceDetailsViewModel = { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export type RaceDetailsViewModel = ViewModel & { race: RaceDetailsRaceViewModel | null; league: RaceDetailsLeagueViewModel | null; entryList: RaceDetailEntryViewModel[]; diff --git a/apps/website/lib/view-models/RaceListItemViewModel.ts b/apps/website/lib/view-models/RaceListItemViewModel.ts index 353eb7c7f..a17d5c3b6 100644 --- a/apps/website/lib/view-models/RaceListItemViewModel.ts +++ b/apps/website/lib/view-models/RaceListItemViewModel.ts @@ -13,7 +13,9 @@ export interface RaceListItemDTO { isPast: boolean; } -export class RaceListItemViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RaceListItemViewModel extends ViewModel { id: string; track: string; car: string; diff --git a/apps/website/lib/view-models/RaceResultViewModel.ts b/apps/website/lib/view-models/RaceResultViewModel.ts index 905621e99..6072a6695 100644 --- a/apps/website/lib/view-models/RaceResultViewModel.ts +++ b/apps/website/lib/view-models/RaceResultViewModel.ts @@ -1,7 +1,9 @@ import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO'; import { FinishDisplay } from '../display-objects/FinishDisplay'; -export class RaceResultViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RaceResultViewModel extends ViewModel { driverId!: string; driverName!: string; avatarUrl!: string; diff --git a/apps/website/lib/view-models/RaceResultsDetailViewModel.ts b/apps/website/lib/view-models/RaceResultsDetailViewModel.ts index 79367a90c..415492b7a 100644 --- a/apps/website/lib/view-models/RaceResultsDetailViewModel.ts +++ b/apps/website/lib/view-models/RaceResultsDetailViewModel.ts @@ -2,12 +2,15 @@ import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO'; import { RaceResultsDetailDTO } from '@/lib/types/generated/RaceResultsDetailDTO'; import { RaceResultViewModel } from './RaceResultViewModel'; -export class RaceResultsDetailViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RaceResultsDetailViewModel extends ViewModel { raceId: string; track: string; currentUserId: string; constructor(dto: RaceResultsDetailDTO & { results?: RaceResultDTO[] }, currentUserId: string) { + super(); this.raceId = dto.raceId; this.track = dto.track; this.currentUserId = currentUserId; diff --git a/apps/website/lib/view-models/RaceStatsViewModel.ts b/apps/website/lib/view-models/RaceStatsViewModel.ts index d0d869cf6..ee5eaafb9 100644 --- a/apps/website/lib/view-models/RaceStatsViewModel.ts +++ b/apps/website/lib/view-models/RaceStatsViewModel.ts @@ -4,7 +4,9 @@ import type { RaceStatsDTO } from '@/lib/types/generated/RaceStatsDTO'; * Race stats view model * Represents race statistics for display */ -export class RaceStatsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RaceStatsViewModel extends ViewModel { totalRaces: number; constructor(dto: RaceStatsDTO) { diff --git a/apps/website/lib/view-models/RaceStewardingViewModel.ts b/apps/website/lib/view-models/RaceStewardingViewModel.ts index 00365693e..2cc593a5f 100644 --- a/apps/website/lib/view-models/RaceStewardingViewModel.ts +++ b/apps/website/lib/view-models/RaceStewardingViewModel.ts @@ -51,7 +51,9 @@ interface RaceStewardingDTO { * Race Stewarding View Model * Represents all data needed for race stewarding (protests, penalties, race info) */ -export class RaceStewardingViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RaceStewardingViewModel extends ViewModel { race: RaceDetailDTO['race']; league: RaceDetailDTO['league']; protests: RaceProtestsDTO['protests']; diff --git a/apps/website/lib/view-models/RaceViewModel.ts b/apps/website/lib/view-models/RaceViewModel.ts index e89af3b52..d152aa9d8 100644 --- a/apps/website/lib/view-models/RaceViewModel.ts +++ b/apps/website/lib/view-models/RaceViewModel.ts @@ -1,7 +1,9 @@ import { RaceDTO } from '@/lib/types/generated/RaceDTO'; import { RacesPageDataRaceDTO } from '@/lib/types/generated/RacesPageDataRaceDTO'; -export class RaceViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RaceViewModel extends ViewModel { constructor( private readonly dto: RaceDTO | RacesPageDataRaceDTO, private readonly _status?: string, diff --git a/apps/website/lib/view-models/RaceWithSOFViewModel.ts b/apps/website/lib/view-models/RaceWithSOFViewModel.ts index bdfbd58f6..960b04960 100644 --- a/apps/website/lib/view-models/RaceWithSOFViewModel.ts +++ b/apps/website/lib/view-models/RaceWithSOFViewModel.ts @@ -1,11 +1,14 @@ import { RaceWithSOFDTO } from '@/lib/types/generated/RaceWithSOFDTO'; -export class RaceWithSOFViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RaceWithSOFViewModel extends ViewModel { id: string; track: string; strengthOfField: number | null; constructor(dto: RaceWithSOFDTO) { + super(); this.id = dto.id; this.track = dto.track; this.strengthOfField = 'strengthOfField' in dto ? dto.strengthOfField ?? null : null; diff --git a/apps/website/lib/view-models/RacesPageViewModel.ts b/apps/website/lib/view-models/RacesPageViewModel.ts index 0cda3e131..ee1d2241f 100644 --- a/apps/website/lib/view-models/RacesPageViewModel.ts +++ b/apps/website/lib/view-models/RacesPageViewModel.ts @@ -10,7 +10,9 @@ interface RacesPageDTO { * Races page view model * Represents the races page data with all races in a single list */ -export class RacesPageViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RacesPageViewModel extends ViewModel { races: RaceListItemViewModel[]; constructor(dto: RacesPageDTO) { diff --git a/apps/website/lib/view-models/RecordEngagementInputViewModel.ts b/apps/website/lib/view-models/RecordEngagementInputViewModel.ts index 1c24a6b50..29dcf9788 100644 --- a/apps/website/lib/view-models/RecordEngagementInputViewModel.ts +++ b/apps/website/lib/view-models/RecordEngagementInputViewModel.ts @@ -4,7 +4,9 @@ * * Note: No matching generated DTO available yet */ -export class RecordEngagementInputViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RecordEngagementInputViewModel extends ViewModel { eventType: string; userId?: string; metadata?: Record; diff --git a/apps/website/lib/view-models/RecordEngagementOutputViewModel.ts b/apps/website/lib/view-models/RecordEngagementOutputViewModel.ts index 73076ecec..006a4def6 100644 --- a/apps/website/lib/view-models/RecordEngagementOutputViewModel.ts +++ b/apps/website/lib/view-models/RecordEngagementOutputViewModel.ts @@ -4,7 +4,9 @@ import type { RecordEngagementOutputDTO } from '@/lib/types/generated/RecordEnga * Record engagement output view model * Represents the result of recording an engagement event for UI consumption */ -export class RecordEngagementOutputViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RecordEngagementOutputViewModel extends ViewModel { eventId: string; engagementWeight: number; diff --git a/apps/website/lib/view-models/RecordPageViewInputViewModel.ts b/apps/website/lib/view-models/RecordPageViewInputViewModel.ts index bd03ba079..e37cda736 100644 --- a/apps/website/lib/view-models/RecordPageViewInputViewModel.ts +++ b/apps/website/lib/view-models/RecordPageViewInputViewModel.ts @@ -4,7 +4,9 @@ * * Note: No matching generated DTO available yet */ -export class RecordPageViewInputViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RecordPageViewInputViewModel extends ViewModel { path: string; userId?: string; diff --git a/apps/website/lib/view-models/RecordPageViewOutputViewModel.ts b/apps/website/lib/view-models/RecordPageViewOutputViewModel.ts index 399acf5e1..1ddbd01c2 100644 --- a/apps/website/lib/view-models/RecordPageViewOutputViewModel.ts +++ b/apps/website/lib/view-models/RecordPageViewOutputViewModel.ts @@ -4,7 +4,9 @@ import type { RecordPageViewOutputDTO } from '@/lib/types/generated/RecordPageVi * Record page view output view model * Represents the result of recording a page view for UI consumption */ -export class RecordPageViewOutputViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RecordPageViewOutputViewModel extends ViewModel { pageViewId: string; constructor(dto: RecordPageViewOutputDTO) { diff --git a/apps/website/lib/view-models/RemoveMemberViewModel.ts b/apps/website/lib/view-models/RemoveMemberViewModel.ts index fd8b2c7b5..4e7c95977 100644 --- a/apps/website/lib/view-models/RemoveMemberViewModel.ts +++ b/apps/website/lib/view-models/RemoveMemberViewModel.ts @@ -5,7 +5,9 @@ import { RemoveLeagueMemberOutputDTO } from '@/lib/types/generated/RemoveLeagueM * * Represents the result of removing a member from a league in a UI-ready format. */ -export class RemoveMemberViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RemoveMemberViewModel extends ViewModel { success: boolean; constructor(dto: RemoveLeagueMemberOutputDTO) { diff --git a/apps/website/lib/view-models/RenewalAlertViewModel.ts b/apps/website/lib/view-models/RenewalAlertViewModel.ts index f066afb61..c3902d8a8 100644 --- a/apps/website/lib/view-models/RenewalAlertViewModel.ts +++ b/apps/website/lib/view-models/RenewalAlertViewModel.ts @@ -3,7 +3,9 @@ * * View model for upcoming renewal alerts. */ -export class RenewalAlertViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RenewalAlertViewModel extends ViewModel { id: string; name: string; type: 'league' | 'team' | 'driver' | 'race' | 'platform'; diff --git a/apps/website/lib/view-models/RequestAvatarGenerationViewModel.ts b/apps/website/lib/view-models/RequestAvatarGenerationViewModel.ts index 9ec98d830..2913c37e6 100644 --- a/apps/website/lib/view-models/RequestAvatarGenerationViewModel.ts +++ b/apps/website/lib/view-models/RequestAvatarGenerationViewModel.ts @@ -5,7 +5,9 @@ import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestA * * Represents the result of an avatar generation request */ -export class RequestAvatarGenerationViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class RequestAvatarGenerationViewModel extends ViewModel { success: boolean; requestId?: string; avatarUrls?: string[]; @@ -23,6 +25,7 @@ export class RequestAvatarGenerationViewModel { error?: string; }, ) { + super(); this.success = dto.success; if ('requestId' in dto && dto.requestId !== undefined) this.requestId = dto.requestId; diff --git a/apps/website/lib/view-models/ScoringConfigurationViewModel.ts b/apps/website/lib/view-models/ScoringConfigurationViewModel.ts index 478e010bd..bb86bc661 100644 --- a/apps/website/lib/view-models/ScoringConfigurationViewModel.ts +++ b/apps/website/lib/view-models/ScoringConfigurationViewModel.ts @@ -13,7 +13,9 @@ export interface CustomPointsConfig { * * View model for scoring configuration including presets and custom points */ -export class ScoringConfigurationViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class ScoringConfigurationViewModel extends ViewModel { readonly patternId?: string; readonly customScoringEnabled: boolean; readonly customPoints?: CustomPointsConfig; diff --git a/apps/website/lib/view-models/SessionViewModel.ts b/apps/website/lib/view-models/SessionViewModel.ts index 5700a58c3..e68c2ee37 100644 --- a/apps/website/lib/view-models/SessionViewModel.ts +++ b/apps/website/lib/view-models/SessionViewModel.ts @@ -1,6 +1,8 @@ import { AuthenticatedUserDTO } from '@/lib/types/generated/AuthenticatedUserDTO'; -export class SessionViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class SessionViewModel extends ViewModel { userId: string; email: string; displayName: string; @@ -10,6 +12,7 @@ export class SessionViewModel { isAuthenticated: boolean = true; constructor(dto: AuthenticatedUserDTO) { + super(); this.userId = dto.userId; this.email = dto.email; this.displayName = dto.displayName; diff --git a/apps/website/lib/view-models/SponsorDashboardViewModel.ts b/apps/website/lib/view-models/SponsorDashboardViewModel.ts index e4019a73a..a5c1e0109 100644 --- a/apps/website/lib/view-models/SponsorDashboardViewModel.ts +++ b/apps/website/lib/view-models/SponsorDashboardViewModel.ts @@ -5,7 +5,9 @@ import { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO'; * * Represents dashboard data for a sponsor with UI-specific transformations. */ -export class SponsorDashboardViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class SponsorDashboardViewModel extends ViewModel { sponsorId: string; sponsorName: string; diff --git a/apps/website/lib/view-models/SponsorSettingsViewModel.ts b/apps/website/lib/view-models/SponsorSettingsViewModel.ts index 267fb93b5..3a847b4c4 100644 --- a/apps/website/lib/view-models/SponsorSettingsViewModel.ts +++ b/apps/website/lib/view-models/SponsorSettingsViewModel.ts @@ -3,7 +3,9 @@ * * View model for sponsor settings data. */ -export class SponsorSettingsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class SponsorSettingsViewModel extends ViewModel { profile: SponsorProfileViewModel; notifications: NotificationSettingsViewModel; privacy: PrivacySettingsViewModel; @@ -15,7 +17,9 @@ export class SponsorSettingsViewModel { } } -export class SponsorProfileViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class SponsorProfileViewModel extends ViewModel { companyName: string; contactName: string; contactEmail: string; @@ -58,7 +62,9 @@ export class SponsorProfileViewModel { } } -export class NotificationSettingsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class NotificationSettingsViewModel extends ViewModel { emailNewSponsorships: boolean; emailWeeklyReport: boolean; emailRaceAlerts: boolean; @@ -78,7 +84,9 @@ export class NotificationSettingsViewModel { } } -export class PrivacySettingsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class PrivacySettingsViewModel extends ViewModel { publicProfile: boolean; showStats: boolean; showActiveSponsorships: boolean; diff --git a/apps/website/lib/view-models/SponsorSponsorshipsViewModel.ts b/apps/website/lib/view-models/SponsorSponsorshipsViewModel.ts index 0cd4994bc..4e580c3ab 100644 --- a/apps/website/lib/view-models/SponsorSponsorshipsViewModel.ts +++ b/apps/website/lib/view-models/SponsorSponsorshipsViewModel.ts @@ -6,7 +6,9 @@ import { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel'; * * View model for sponsor sponsorships data with UI-specific transformations. */ -export class SponsorSponsorshipsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class SponsorSponsorshipsViewModel extends ViewModel { sponsorId: string; sponsorName: string; diff --git a/apps/website/lib/view-models/SponsorViewModel.ts b/apps/website/lib/view-models/SponsorViewModel.ts index 3a6013acb..8b71d52b8 100644 --- a/apps/website/lib/view-models/SponsorViewModel.ts +++ b/apps/website/lib/view-models/SponsorViewModel.ts @@ -6,7 +6,9 @@ interface SponsorDTO { websiteUrl?: string; } -export class SponsorViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class SponsorViewModel extends ViewModel { id: string; name: string; declare logoUrl?: string; diff --git a/apps/website/lib/view-models/SponsorshipDetailViewModel.ts b/apps/website/lib/view-models/SponsorshipDetailViewModel.ts index 6f6f87b36..a6c8e56c1 100644 --- a/apps/website/lib/view-models/SponsorshipDetailViewModel.ts +++ b/apps/website/lib/view-models/SponsorshipDetailViewModel.ts @@ -1,6 +1,8 @@ import { SponsorshipDetailDTO } from '@/lib/types/generated/SponsorshipDetailDTO'; -export class SponsorshipDetailViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class SponsorshipDetailViewModel extends ViewModel { id: string; leagueId: string; leagueName: string; diff --git a/apps/website/lib/view-models/SponsorshipPricingViewModel.ts b/apps/website/lib/view-models/SponsorshipPricingViewModel.ts index a35e483e0..25e920a91 100644 --- a/apps/website/lib/view-models/SponsorshipPricingViewModel.ts +++ b/apps/website/lib/view-models/SponsorshipPricingViewModel.ts @@ -10,7 +10,9 @@ interface SponsorshipPricingDTO { * * View model for sponsorship pricing data with UI-specific transformations. */ -export class SponsorshipPricingViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class SponsorshipPricingViewModel extends ViewModel { mainSlotPrice: number; secondarySlotPrice: number; currency: string; diff --git a/apps/website/lib/view-models/SponsorshipRequestViewModel.ts b/apps/website/lib/view-models/SponsorshipRequestViewModel.ts index a4ef103eb..e037405c8 100644 --- a/apps/website/lib/view-models/SponsorshipRequestViewModel.ts +++ b/apps/website/lib/view-models/SponsorshipRequestViewModel.ts @@ -1,6 +1,8 @@ import type { SponsorshipRequestDTO } from '@/lib/types/generated/SponsorshipRequestDTO'; -export class SponsorshipRequestViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class SponsorshipRequestViewModel extends ViewModel { id: string; sponsorId: string; sponsorName: string; diff --git a/apps/website/lib/view-models/SponsorshipViewModel.ts b/apps/website/lib/view-models/SponsorshipViewModel.ts index 51242fca8..db657a965 100644 --- a/apps/website/lib/view-models/SponsorshipViewModel.ts +++ b/apps/website/lib/view-models/SponsorshipViewModel.ts @@ -31,7 +31,9 @@ export interface SponsorshipDataInput { * * View model for individual sponsorship data. */ -export class SponsorshipViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class SponsorshipViewModel extends ViewModel { id: string; type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform'; entityId: string; diff --git a/apps/website/lib/view-models/StandingEntryViewModel.ts b/apps/website/lib/view-models/StandingEntryViewModel.ts index 9289e6cf0..8d53fd99d 100644 --- a/apps/website/lib/view-models/StandingEntryViewModel.ts +++ b/apps/website/lib/view-models/StandingEntryViewModel.ts @@ -1,6 +1,8 @@ import { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO'; -export class StandingEntryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class StandingEntryViewModel extends ViewModel { driverId: string; position: number; points: number; diff --git a/apps/website/lib/view-models/TeamCardViewModel.ts b/apps/website/lib/view-models/TeamCardViewModel.ts index 018b518a2..d5c4be567 100644 --- a/apps/website/lib/view-models/TeamCardViewModel.ts +++ b/apps/website/lib/view-models/TeamCardViewModel.ts @@ -12,7 +12,9 @@ interface TeamCardDTO { * Team card view model * UI representation of a team on the landing page. */ -export class TeamCardViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class TeamCardViewModel extends ViewModel { readonly id: string; readonly name: string; readonly tag: string; diff --git a/apps/website/lib/view-models/TeamDetailsViewModel.ts b/apps/website/lib/view-models/TeamDetailsViewModel.ts index e7f335087..e7b4bec2b 100644 --- a/apps/website/lib/view-models/TeamDetailsViewModel.ts +++ b/apps/website/lib/view-models/TeamDetailsViewModel.ts @@ -1,6 +1,8 @@ import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO'; -export class TeamDetailsViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class TeamDetailsViewModel extends ViewModel { id!: string; name!: string; tag!: string; diff --git a/apps/website/lib/view-models/TeamJoinRequestViewModel.ts b/apps/website/lib/view-models/TeamJoinRequestViewModel.ts index 6fdd4603c..1f7ce440f 100644 --- a/apps/website/lib/view-models/TeamJoinRequestViewModel.ts +++ b/apps/website/lib/view-models/TeamJoinRequestViewModel.ts @@ -1,6 +1,8 @@ import type { TeamJoinRequestDTO } from '@/lib/types/generated/TeamJoinRequestDTO'; -export class TeamJoinRequestViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class TeamJoinRequestViewModel extends ViewModel { requestId: string; driverId: string; driverName: string; diff --git a/apps/website/lib/view-models/TeamMemberViewModel.ts b/apps/website/lib/view-models/TeamMemberViewModel.ts index 42f01f36b..1ccf41ac2 100644 --- a/apps/website/lib/view-models/TeamMemberViewModel.ts +++ b/apps/website/lib/view-models/TeamMemberViewModel.ts @@ -9,7 +9,9 @@ function normalizeTeamRole(role: string): TeamMemberRole { return 'member'; } -export class TeamMemberViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class TeamMemberViewModel extends ViewModel { driverId: string; driverName: string; role: TeamMemberRole; diff --git a/apps/website/lib/view-models/TeamSummaryViewModel.ts b/apps/website/lib/view-models/TeamSummaryViewModel.ts index 8b9aa50b2..ead7affbe 100644 --- a/apps/website/lib/view-models/TeamSummaryViewModel.ts +++ b/apps/website/lib/view-models/TeamSummaryViewModel.ts @@ -1,6 +1,8 @@ import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO'; -export class TeamSummaryViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class TeamSummaryViewModel extends ViewModel { id: string; name: string; tag: string; diff --git a/apps/website/lib/view-models/UpcomingRaceCardViewModel.ts b/apps/website/lib/view-models/UpcomingRaceCardViewModel.ts index 477400293..0fdb1af02 100644 --- a/apps/website/lib/view-models/UpcomingRaceCardViewModel.ts +++ b/apps/website/lib/view-models/UpcomingRaceCardViewModel.ts @@ -9,7 +9,9 @@ interface UpcomingRaceCardDTO { * Upcoming race card view model * UI representation of an upcoming race on the landing page. */ -export class UpcomingRaceCardViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class UpcomingRaceCardViewModel extends ViewModel { readonly id: string; readonly track: string; readonly car: string; diff --git a/apps/website/lib/view-models/UpdateAvatarViewModel.ts b/apps/website/lib/view-models/UpdateAvatarViewModel.ts index 8e5ed4ead..67045b7fe 100644 --- a/apps/website/lib/view-models/UpdateAvatarViewModel.ts +++ b/apps/website/lib/view-models/UpdateAvatarViewModel.ts @@ -9,7 +9,9 @@ interface UpdateAvatarDTO { * * Represents the result of an avatar update operation */ -export class UpdateAvatarViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class UpdateAvatarViewModel extends ViewModel { success: boolean; error?: string; diff --git a/apps/website/lib/view-models/UpdateTeamViewModel.ts b/apps/website/lib/view-models/UpdateTeamViewModel.ts index 3111c0591..8fbb05b09 100644 --- a/apps/website/lib/view-models/UpdateTeamViewModel.ts +++ b/apps/website/lib/view-models/UpdateTeamViewModel.ts @@ -3,7 +3,9 @@ * * Represents the result of updating a team in a UI-ready format. */ -export class UpdateTeamViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class UpdateTeamViewModel extends ViewModel { success: boolean; constructor(dto: { success: boolean }) { diff --git a/apps/website/lib/view-models/UploadMediaViewModel.ts b/apps/website/lib/view-models/UploadMediaViewModel.ts index aa8e1270c..b815586bd 100644 --- a/apps/website/lib/view-models/UploadMediaViewModel.ts +++ b/apps/website/lib/view-models/UploadMediaViewModel.ts @@ -11,7 +11,9 @@ interface UploadMediaDTO { * * Represents the result of a media upload operation */ -export class UploadMediaViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class UploadMediaViewModel extends ViewModel { success: boolean; mediaId?: string; url?: string; diff --git a/apps/website/lib/view-models/UserProfileViewModel.ts b/apps/website/lib/view-models/UserProfileViewModel.ts index 7524fb419..64608674e 100644 --- a/apps/website/lib/view-models/UserProfileViewModel.ts +++ b/apps/website/lib/view-models/UserProfileViewModel.ts @@ -7,7 +7,9 @@ interface UserProfileDTO { rating?: number; } -export class UserProfileViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class UserProfileViewModel extends ViewModel { id: string; name: string; avatarUrl?: string; diff --git a/apps/website/lib/view-models/WalletTransactionViewModel.ts b/apps/website/lib/view-models/WalletTransactionViewModel.ts index 6ec2b5e09..e4c4fb129 100644 --- a/apps/website/lib/view-models/WalletTransactionViewModel.ts +++ b/apps/website/lib/view-models/WalletTransactionViewModel.ts @@ -11,7 +11,9 @@ export type FullTransactionDto = { reference?: string; }; -export class WalletTransactionViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class WalletTransactionViewModel extends ViewModel { id: string; type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize' | 'deposit'; description: string; @@ -23,6 +25,7 @@ export class WalletTransactionViewModel { reference?: string; constructor(dto: FullTransactionDto) { + super(); this.id = dto.id; this.type = dto.type; this.description = dto.description; diff --git a/apps/website/lib/view-models/WalletViewModel.ts b/apps/website/lib/view-models/WalletViewModel.ts index da058d7d1..3854919a2 100644 --- a/apps/website/lib/view-models/WalletViewModel.ts +++ b/apps/website/lib/view-models/WalletViewModel.ts @@ -1,7 +1,9 @@ import { WalletDTO } from '@/lib/types/generated/WalletDTO'; import { WalletTransactionViewModel } from './WalletTransactionViewModel'; -export class WalletViewModel { +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export class WalletViewModel extends ViewModel { id: string; leagueId: string; balance: number; diff --git a/apps/website/templates/DriverProfileTemplate.tsx b/apps/website/templates/DriverProfileTemplate.tsx index 9752f531b..622afc247 100644 --- a/apps/website/templates/DriverProfileTemplate.tsx +++ b/apps/website/templates/DriverProfileTemplate.tsx @@ -21,7 +21,7 @@ import { DriverProfileTabs, type ProfileTab } from '@/components/drivers/DriverP import { DriverRacingProfile } from '@/components/drivers/DriverRacingProfile'; import { DriverStatsPanel } from '@/components/drivers/DriverStatsPanel'; -import type { DriverProfileViewData } from '@/lib/types/view-data/DriverProfileViewData'; +import type { DriverProfileViewData } from '@/lib/view-data/DriverProfileViewData'; interface DriverProfileTemplateProps { viewData: DriverProfileViewData; diff --git a/apps/website/templates/DriversTemplate.tsx b/apps/website/templates/DriversTemplate.tsx index 1d83816d0..dde6879f5 100644 --- a/apps/website/templates/DriversTemplate.tsx +++ b/apps/website/templates/DriversTemplate.tsx @@ -1,17 +1,15 @@ 'use client'; -import { DriversViewData } from '@/lib/types/view-data/DriversViewData'; import { DriverCard } from '@/components/drivers/DriverCard'; -import { DriverStatsHeader } from '@/components/drivers/DriverStatsHeader'; import { DriverGrid } from '@/components/drivers/DriverGrid'; +import { DriverStatsHeader } from '@/components/drivers/DriverStatsHeader'; +import { DriversViewData } from '@/lib/view-data/DriversViewData'; +import { Button } from '@/ui/Button'; +import { EmptyState } from '@/ui/EmptyState'; +import { Input } from '@/ui/Input'; import { PageHeader } from '@/ui/PageHeader'; import { Section } from '@/ui/Section'; -import { Stack } from '@/ui/Stack'; -import { Input } from '@/ui/Input'; -import { Button } from '@/ui/Button'; -import { Container } from '@/ui/Container'; import { Search, Users } from 'lucide-react'; -import { EmptyState } from '@/ui/EmptyState'; interface DriversTemplateProps { viewData: DriversViewData;