view data fixes

This commit is contained in:
2026-01-24 00:52:27 +01:00
parent 62e8b768ce
commit ae59df61eb
321 changed files with 1157 additions and 2234 deletions

View File

@@ -2216,6 +2216,41 @@
"incidents" "incidents"
] ]
}, },
"DashboardStatsResponseDTO": {
"type": "object",
"properties": {
"totalUsers": {
"type": "number"
},
"activeUsers": {
"type": "number"
},
"suspendedUsers": {
"type": "number"
},
"deletedUsers": {
"type": "number"
},
"systemAdmins": {
"type": "number"
},
"recentLogins": {
"type": "number"
},
"newUsersToday": {
"type": "number"
}
},
"required": [
"totalUsers",
"activeUsers",
"suspendedUsers",
"deletedUsers",
"systemAdmins",
"recentLogins",
"newUsersToday"
]
},
"DeleteMediaOutputDTO": { "DeleteMediaOutputDTO": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4235,6 +4270,9 @@
"LeagueScheduleDTO": { "LeagueScheduleDTO": {
"type": "object", "type": "object",
"properties": { "properties": {
"leagueId": {
"type": "string"
},
"seasonId": { "seasonId": {
"type": "string" "type": "string"
}, },
@@ -4473,6 +4511,16 @@
}, },
"isParallelActive": { "isParallelActive": {
"type": "boolean" "type": "boolean"
},
"totalRaces": {
"type": "number"
},
"completedRaces": {
"type": "number"
},
"nextRaceAt": {
"type": "string",
"format": "date-time"
} }
}, },
"required": [ "required": [
@@ -4480,7 +4528,9 @@
"name", "name",
"status", "status",
"isPrimary", "isPrimary",
"isParallelActive" "isParallelActive",
"totalRaces",
"completedRaces"
] ]
}, },
"LeagueSettingsDTO": { "LeagueSettingsDTO": {
@@ -4515,6 +4565,18 @@
}, },
"races": { "races": {
"type": "number" "type": "number"
},
"positionChange": {
"type": "number"
},
"lastRacePoints": {
"type": "number"
},
"droppedRaceIds": {
"type": "array",
"items": {
"type": "string"
}
} }
}, },
"required": [ "required": [
@@ -4524,7 +4586,10 @@
"position", "position",
"wins", "wins",
"podiums", "podiums",
"races" "races",
"positionChange",
"lastRacePoints",
"droppedRaceIds"
] ]
}, },
"LeagueStandingsDTO": { "LeagueStandingsDTO": {
@@ -4658,6 +4723,15 @@
"logoUrl": { "logoUrl": {
"type": "string", "type": "string",
"nullable": true "nullable": true
},
"pendingJoinRequestsCount": {
"type": "number"
},
"pendingProtestsCount": {
"type": "number"
},
"walletBalance": {
"type": "number"
} }
}, },
"required": [ "required": [
@@ -5449,8 +5523,34 @@
"type": "string" "type": "string"
}, },
"leagueName": { "leagueName": {
"type": "string", "type": "string"
"nullable": true },
"track": {
"type": "string"
},
"car": {
"type": "string"
},
"sessionType": {
"type": "string"
},
"leagueId": {
"type": "string"
},
"strengthOfField": {
"type": "number"
},
"isUpcoming": {
"type": "boolean"
},
"isLive": {
"type": "boolean"
},
"isPast": {
"type": "boolean"
},
"status": {
"type": "string"
} }
}, },
"required": [ "required": [

View File

@@ -1,80 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
class UserGrowthDto {
@ApiProperty({ description: 'Label for the time period' })
label!: string;
@ApiProperty({ description: 'Number of new users' })
value!: number;
@ApiProperty({ description: 'Color class for the bar' })
color!: string;
}
class RoleDistributionDto {
@ApiProperty({ description: 'Role name' })
label!: string;
@ApiProperty({ description: 'Number of users with this role' })
value!: number;
@ApiProperty({ description: 'Color class for the bar' })
color!: string;
}
class StatusDistributionDto {
@ApiProperty({ description: 'Number of active users' })
active!: number;
@ApiProperty({ description: 'Number of suspended users' })
suspended!: number;
@ApiProperty({ description: 'Number of deleted users' })
deleted!: number;
}
class ActivityTimelineDto {
@ApiProperty({ description: 'Date label' })
date!: string;
@ApiProperty({ description: 'Number of new users' })
newUsers!: number;
@ApiProperty({ description: 'Number of logins' })
logins!: number;
}
export class DashboardStatsResponseDto {
@ApiProperty({ description: 'Total number of users' })
totalUsers!: number;
@ApiProperty({ description: 'Number of active users' })
activeUsers!: number;
@ApiProperty({ description: 'Number of suspended users' })
suspendedUsers!: number;
@ApiProperty({ description: 'Number of deleted users' })
deletedUsers!: number;
@ApiProperty({ description: 'Number of system admins' })
systemAdmins!: number;
@ApiProperty({ description: 'Number of recent logins (last 24h)' })
recentLogins!: number;
@ApiProperty({ description: 'Number of new users today' })
newUsersToday!: number;
@ApiProperty({ type: [UserGrowthDto], description: 'User growth over last 7 days' })
userGrowth!: UserGrowthDto[];
@ApiProperty({ type: [RoleDistributionDto], description: 'Distribution of user roles' })
roleDistribution!: RoleDistributionDto[];
@ApiProperty({ type: StatusDistributionDto, description: 'Distribution of user statuses' })
statusDistribution!: StatusDistributionDto;
@ApiProperty({ type: [ActivityTimelineDto], description: 'Activity timeline for last 7 days' })
activityTimeline!: ActivityTimelineDto[];
}

View File

@@ -0,0 +1,24 @@
export interface HealthDTO {
status: 'ok' | 'degraded' | 'error' | 'unknown';
timestamp: string;
uptime?: number;
responseTime?: number;
errorRate?: number;
lastCheck?: string;
checksPassed?: number;
checksFailed?: number;
components?: Array<{
name: string;
status: 'ok' | 'degraded' | 'error' | 'unknown';
lastCheck?: string;
responseTime?: number;
errorRate?: number;
}>;
alerts?: Array<{
id: string;
type: 'critical' | 'warning' | 'info';
title: string;
message: string;
timestamp: string;
}>;
}

View File

@@ -1,9 +1,13 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsArray, IsBoolean, IsString, ValidateNested } from 'class-validator'; import { IsArray, IsBoolean, IsString, ValidateNested } from 'class-validator';
import { RaceDTO } from '../../race/dtos/RaceDTO'; import { RaceDTO } from '../../race/dtos/RaceDTO';
export class LeagueScheduleDTO { export class LeagueScheduleDTO {
@ApiPropertyOptional()
@IsString()
leagueId?: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
seasonId!: string; seasonId!: string;

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class RaceDTO { export class RaceDTO {
@ApiProperty() @ApiProperty()
@@ -10,6 +10,33 @@ export class RaceDTO {
@ApiProperty() @ApiProperty()
date!: string; date!: string;
@ApiProperty({ nullable: true }) @ApiPropertyOptional({ nullable: true })
leagueName?: string; leagueName?: string;
@ApiPropertyOptional()
track?: string;
@ApiPropertyOptional()
car?: string;
@ApiPropertyOptional()
sessionType?: string;
@ApiPropertyOptional()
leagueId?: string;
@ApiPropertyOptional()
strengthOfField?: number;
@ApiPropertyOptional()
isUpcoming?: boolean;
@ApiPropertyOptional()
isLive?: boolean;
@ApiPropertyOptional()
isPast?: boolean;
@ApiPropertyOptional()
status?: string;
} }

View File

@@ -1,11 +1,11 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { AdminDashboardViewDataBuilder } from './AdminDashboardViewDataBuilder'; import { AdminDashboardViewDataBuilder } from './AdminDashboardViewDataBuilder';
import type { DashboardStats } from '@/lib/types/admin'; import type { DashboardStatsResponseDTO } from '@/lib/types/generated/DashboardStatsResponseDTO';
describe('AdminDashboardViewDataBuilder', () => { describe('AdminDashboardViewDataBuilder', () => {
describe('happy paths', () => { describe('happy paths', () => {
it('should transform DashboardStats DTO to AdminDashboardViewData correctly', () => { it('should transform DashboardStatsResponseDTO to AdminDashboardViewData correctly', () => {
const dashboardStats: DashboardStats = { const dashboardStats: DashboardStatsResponseDTO = {
totalUsers: 1000, totalUsers: 1000,
activeUsers: 800, activeUsers: 800,
suspendedUsers: 50, suspendedUsers: 50,
@@ -31,7 +31,7 @@ describe('AdminDashboardViewDataBuilder', () => {
}); });
it('should handle zero values correctly', () => { it('should handle zero values correctly', () => {
const dashboardStats: DashboardStats = { const dashboardStats: DashboardStatsResponseDTO = {
totalUsers: 0, totalUsers: 0,
activeUsers: 0, activeUsers: 0,
suspendedUsers: 0, suspendedUsers: 0,
@@ -55,100 +55,5 @@ describe('AdminDashboardViewDataBuilder', () => {
}, },
}); });
}); });
it('should handle large numbers correctly', () => {
const dashboardStats: DashboardStats = {
totalUsers: 1000000,
activeUsers: 750000,
suspendedUsers: 25000,
deletedUsers: 225000,
systemAdmins: 50,
recentLogins: 50000,
newUsersToday: 1000,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(1000000);
expect(result.stats.activeUsers).toBe(750000);
expect(result.stats.systemAdmins).toBe(50);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const dashboardStats: DashboardStats = {
totalUsers: 500,
activeUsers: 400,
suspendedUsers: 25,
deletedUsers: 75,
systemAdmins: 3,
recentLogins: 80,
newUsersToday: 10,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(dashboardStats.totalUsers);
expect(result.stats.activeUsers).toBe(dashboardStats.activeUsers);
expect(result.stats.suspendedUsers).toBe(dashboardStats.suspendedUsers);
expect(result.stats.deletedUsers).toBe(dashboardStats.deletedUsers);
expect(result.stats.systemAdmins).toBe(dashboardStats.systemAdmins);
expect(result.stats.recentLogins).toBe(dashboardStats.recentLogins);
expect(result.stats.newUsersToday).toBe(dashboardStats.newUsersToday);
});
it('should not modify the input DTO', () => {
const dashboardStats: DashboardStats = {
totalUsers: 100,
activeUsers: 80,
suspendedUsers: 5,
deletedUsers: 15,
systemAdmins: 2,
recentLogins: 20,
newUsersToday: 5,
};
const originalStats = { ...dashboardStats };
AdminDashboardViewDataBuilder.build(dashboardStats);
expect(dashboardStats).toEqual(originalStats);
});
});
describe('edge cases', () => {
it('should handle negative values (if API returns them)', () => {
const dashboardStats: DashboardStats = {
totalUsers: -1,
activeUsers: -1,
suspendedUsers: -1,
deletedUsers: -1,
systemAdmins: -1,
recentLogins: -1,
newUsersToday: -1,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(-1);
expect(result.stats.activeUsers).toBe(-1);
});
it('should handle very large numbers', () => {
const dashboardStats: DashboardStats = {
totalUsers: Number.MAX_SAFE_INTEGER,
activeUsers: Number.MAX_SAFE_INTEGER - 1000,
suspendedUsers: 100,
deletedUsers: 100,
systemAdmins: 10,
recentLogins: 1000,
newUsersToday: 100,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(Number.MAX_SAFE_INTEGER);
expect(result.stats.activeUsers).toBe(Number.MAX_SAFE_INTEGER - 1000);
});
}); });
}); });

View File

@@ -1,22 +1,11 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData'; 'use client';
import type { DashboardStats } from '@/lib/types/admin';
import type { DashboardStatsResponseDTO } from '@/lib/types/generated/DashboardStatsResponseDTO';
import type { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData'; import type { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
/** export class AdminDashboardViewDataBuilder {
* AdminDashboardViewDataBuilder public static build(apiDto: DashboardStatsResponseDTO): AdminDashboardViewData {
*
* Transforms DashboardStats API DTO into AdminDashboardViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class AdminDashboardViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return AdminDashboardViewDataBuilder.build(input);
}
static build(
static build(apiDto: DashboardStats): AdminDashboardViewData {
return { return {
stats: { stats: {
totalUsers: apiDto.totalUsers, totalUsers: apiDto.totalUsers,
@@ -29,4 +18,6 @@ export class AdminDashboardViewDataBuilder implements ViewDataBuilder<any, any>
}, },
}; };
} }
} }
AdminDashboardViewDataBuilder satisfies ViewDataBuilder<DashboardStatsResponseDTO, AdminDashboardViewData>;

View File

@@ -1,11 +1,11 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { AdminUsersViewDataBuilder } from './AdminUsersViewDataBuilder'; import { AdminUsersViewDataBuilder } from './AdminUsersViewDataBuilder';
import type { UserListResponse } from '@/lib/types/admin'; import type { UserListResponseDTO } from '@/lib/types/generated/UserListResponseDTO';
describe('AdminUsersViewDataBuilder', () => { describe('AdminUsersViewDataBuilder', () => {
describe('happy paths', () => { describe('happy paths', () => {
it('should transform UserListResponse DTO to AdminUsersViewData correctly', () => { it('should transform UserListResponseDTO to AdminUsersViewData correctly', () => {
const userListResponse: UserListResponse = { const userListResponse: UserListResponseDTO = {
users: [ users: [
{ {
id: 'user-1', id: 'user-1',
@@ -53,26 +53,11 @@ describe('AdminUsersViewDataBuilder', () => {
lastLoginAt: '2024-01-20T10:00:00.000Z', lastLoginAt: '2024-01-20T10:00:00.000Z',
primaryDriverId: 'driver-123', primaryDriverId: 'driver-123',
}); });
expect(result.users[1]).toEqual({
id: 'user-2',
email: 'user@example.com',
displayName: 'Regular User',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-05T00:00:00.000Z',
updatedAt: '2024-01-10T08:00:00.000Z',
lastLoginAt: '2024-01-18T14:00:00.000Z',
primaryDriverId: 'driver-456',
});
expect(result.total).toBe(2); expect(result.total).toBe(2);
expect(result.page).toBe(1);
expect(result.limit).toBe(10);
expect(result.totalPages).toBe(1);
}); });
it('should calculate derived fields correctly', () => { it('should calculate derived fields correctly', () => {
const userListResponse: UserListResponse = { const userListResponse: UserListResponseDTO = {
users: [ users: [
{ {
id: 'user-1', id: 'user-1',
@@ -104,18 +89,8 @@ describe('AdminUsersViewDataBuilder', () => {
createdAt: '2024-01-03T00:00:00.000Z', createdAt: '2024-01-03T00:00:00.000Z',
updatedAt: '2024-01-17T12:00:00.000Z', updatedAt: '2024-01-17T12:00:00.000Z',
}, },
{
id: 'user-4',
email: 'user4@example.com',
displayName: 'User 4',
roles: ['member'],
status: 'deleted',
isSystemAdmin: false,
createdAt: '2024-01-04T00:00:00.000Z',
updatedAt: '2024-01-18T12:00:00.000Z',
},
], ],
total: 4, total: 3,
page: 1, page: 1,
limit: 10, limit: 10,
totalPages: 1, totalPages: 1,
@@ -123,495 +98,8 @@ describe('AdminUsersViewDataBuilder', () => {
const result = AdminUsersViewDataBuilder.build(userListResponse); const result = AdminUsersViewDataBuilder.build(userListResponse);
// activeUserCount should count only users with status 'active'
expect(result.activeUserCount).toBe(2); expect(result.activeUserCount).toBe(2);
// adminCount should count only system admins
expect(result.adminCount).toBe(1); expect(result.adminCount).toBe(1);
}); });
it('should handle empty users list', () => {
const userListResponse: UserListResponse = {
users: [],
total: 0,
page: 1,
limit: 10,
totalPages: 0,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users).toHaveLength(0);
expect(result.total).toBe(0);
expect(result.activeUserCount).toBe(0);
expect(result.adminCount).toBe(0);
});
it('should handle users without optional fields', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
// lastLoginAt and primaryDriverId are optional
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].lastLoginAt).toBeUndefined();
expect(result.users[0].primaryDriverId).toBeUndefined();
});
});
describe('date formatting', () => {
it('should handle ISO date strings correctly', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
lastLoginAt: '2024-01-20T10:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].createdAt).toBe('2024-01-01T00:00:00.000Z');
expect(result.users[0].updatedAt).toBe('2024-01-15T12:00:00.000Z');
expect(result.users[0].lastLoginAt).toBe('2024-01-20T10:00:00.000Z');
});
it('should handle Date objects and convert to ISO strings', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-15T12:00:00.000Z'),
lastLoginAt: new Date('2024-01-20T10:00:00.000Z'),
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].createdAt).toBe('2024-01-01T00:00:00.000Z');
expect(result.users[0].updatedAt).toBe('2024-01-15T12:00:00.000Z');
expect(result.users[0].lastLoginAt).toBe('2024-01-20T10:00:00.000Z');
});
it('should handle Date objects for lastLoginAt when present', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
lastLoginAt: new Date('2024-01-20T10:00:00.000Z'),
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].lastLoginAt).toBe('2024-01-20T10:00:00.000Z');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['admin', 'owner'],
status: 'active',
isSystemAdmin: true,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
lastLoginAt: '2024-01-20T10:00:00.000Z',
primaryDriverId: 'driver-123',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].id).toBe(userListResponse.users[0].id);
expect(result.users[0].email).toBe(userListResponse.users[0].email);
expect(result.users[0].displayName).toBe(userListResponse.users[0].displayName);
expect(result.users[0].roles).toEqual(userListResponse.users[0].roles);
expect(result.users[0].status).toBe(userListResponse.users[0].status);
expect(result.users[0].isSystemAdmin).toBe(userListResponse.users[0].isSystemAdmin);
expect(result.users[0].createdAt).toBe(userListResponse.users[0].createdAt);
expect(result.users[0].updatedAt).toBe(userListResponse.users[0].updatedAt);
expect(result.users[0].lastLoginAt).toBe(userListResponse.users[0].lastLoginAt);
expect(result.users[0].primaryDriverId).toBe(userListResponse.users[0].primaryDriverId);
expect(result.total).toBe(userListResponse.total);
expect(result.page).toBe(userListResponse.page);
expect(result.limit).toBe(userListResponse.limit);
expect(result.totalPages).toBe(userListResponse.totalPages);
});
it('should not modify the input DTO', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const originalResponse = { ...userListResponse };
AdminUsersViewDataBuilder.build(userListResponse);
expect(userListResponse).toEqual(originalResponse);
});
});
describe('edge cases', () => {
it('should handle users with multiple roles', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['admin', 'owner', 'steward', 'member'],
status: 'active',
isSystemAdmin: true,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].roles).toEqual(['admin', 'owner', 'steward', 'member']);
});
it('should handle users with different statuses', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
{
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['member'],
status: 'suspended',
isSystemAdmin: false,
createdAt: '2024-01-02T00:00:00.000Z',
updatedAt: '2024-01-16T12:00:00.000Z',
},
{
id: 'user-3',
email: 'user3@example.com',
displayName: 'User 3',
roles: ['member'],
status: 'deleted',
isSystemAdmin: false,
createdAt: '2024-01-03T00:00:00.000Z',
updatedAt: '2024-01-17T12:00:00.000Z',
},
],
total: 3,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].status).toBe('active');
expect(result.users[1].status).toBe('suspended');
expect(result.users[2].status).toBe('deleted');
expect(result.activeUserCount).toBe(1);
});
it('should handle pagination metadata correctly', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 100,
page: 5,
limit: 20,
totalPages: 5,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.total).toBe(100);
expect(result.page).toBe(5);
expect(result.limit).toBe(20);
expect(result.totalPages).toBe(5);
});
it('should handle users with empty roles array', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: [],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].roles).toEqual([]);
});
it('should handle users with special characters in display name', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1 & 2 (Admin)',
roles: ['admin'],
status: 'active',
isSystemAdmin: true,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].displayName).toBe('User 1 & 2 (Admin)');
});
it('should handle users with very long email addresses', () => {
const longEmail = 'verylongemailaddresswithmanycharacters@example.com';
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: longEmail,
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].email).toBe(longEmail);
});
});
describe('derived fields calculation', () => {
it('should calculate activeUserCount correctly with mixed statuses', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '3', email: '3@e.com', displayName: '3', roles: ['member'], status: 'suspended', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '4', email: '4@e.com', displayName: '4', roles: ['member'], status: 'deleted', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 4,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.activeUserCount).toBe(2);
});
it('should calculate adminCount correctly with mixed roles', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['admin', 'owner'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '3', email: '3@e.com', displayName: '3', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '4', email: '4@e.com', displayName: '4', roles: ['owner'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 4,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.adminCount).toBe(2);
});
it('should handle all active users', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '3', email: '3@e.com', displayName: '3', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 3,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.activeUserCount).toBe(3);
});
it('should handle no active users', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'suspended', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['member'], status: 'deleted', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 2,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.activeUserCount).toBe(0);
});
it('should handle all system admins', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '3', email: '3@e.com', displayName: '3', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 3,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.adminCount).toBe(3);
});
it('should handle no system admins', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['owner'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 2,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.adminCount).toBe(0);
});
}); });
}); });

View File

@@ -1,19 +1,22 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData'; 'use client';
import type { UserListResponse } from '@/lib/types/admin';
import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import type { UserListResponseDTO } from '@/lib/types/generated/UserListResponseDTO';
import type { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
export class AdminUsersViewDataBuilder implements ViewDataBuilder<any, any> { export class AdminUsersViewDataBuilder {
build(input: any): any { public static build(apiDto: UserListResponseDTO): AdminUsersViewData {
return AdminUsersViewDataBuilder.build(input);
}
static build(
public static build(apiDto: UserListResponse): AdminUsersViewData {
const users = apiDto.users.map(u => ({ const users = apiDto.users.map(u => ({
...u, id: u.id,
joinedAt: new Date(u.joinedAt), email: u.email,
displayName: u.displayName,
roles: u.roles,
status: u.status,
isSystemAdmin: u.isSystemAdmin,
createdAt: u.createdAt,
updatedAt: u.updatedAt,
lastLoginAt: u.lastLoginAt,
primaryDriverId: u.primaryDriverId,
})); }));
return { return {
@@ -22,9 +25,10 @@ export class AdminUsersViewDataBuilder implements ViewDataBuilder<any, any> {
page: apiDto.page, page: apiDto.page,
limit: apiDto.limit, limit: apiDto.limit,
totalPages: apiDto.totalPages, totalPages: apiDto.totalPages,
// Pre-computed derived values for template
activeUserCount: users.filter(u => u.status === 'active').length, activeUserCount: users.filter(u => u.status === 'active').length,
adminCount: users.filter(u => u.isSystemAdmin).length, adminCount: users.filter(u => u.isSystemAdmin).length,
}; };
} }
} }
AdminUsersViewDataBuilder satisfies ViewDataBuilder<UserListResponseDTO, AdminUsersViewData>;

View File

@@ -1,17 +1,17 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { AnalyticsDashboardViewDataBuilder } from './AnalyticsDashboardViewDataBuilder'; import { AnalyticsDashboardViewDataBuilder } from './AnalyticsDashboardViewDataBuilder';
import { AnalyticsDashboardInputViewData } from '@/lib/view-data/AnalyticsDashboardInputViewData'; import type { GetDashboardDataOutputDTO } from '@/lib/types/generated/GetDashboardDataOutputDTO';
describe('AnalyticsDashboardViewDataBuilder', () => { describe('AnalyticsDashboardViewDataBuilder', () => {
it('builds ViewData from AnalyticsDashboardInputViewData', () => { it('builds ViewData from GetDashboardDataOutputDTO', () => {
const inputViewData: AnalyticsDashboardInputViewData = { const inputDto: GetDashboardDataOutputDTO = {
totalUsers: 100, totalUsers: 100,
activeUsers: 40, activeUsers: 40,
totalRaces: 10, totalRaces: 10,
totalLeagues: 5, totalLeagues: 5,
}; };
const viewData = AnalyticsDashboardViewDataBuilder.build(inputViewData); const viewData = AnalyticsDashboardViewDataBuilder.build(inputDto);
expect(viewData.metrics.totalUsers).toBe(100); expect(viewData.metrics.totalUsers).toBe(100);
expect(viewData.metrics.activeUsers).toBe(40); expect(viewData.metrics.activeUsers).toBe(40);
@@ -23,28 +23,28 @@ describe('AnalyticsDashboardViewDataBuilder', () => {
}); });
it('computes engagement rate and formatted engagement rate', () => { it('computes engagement rate and formatted engagement rate', () => {
const inputViewData: AnalyticsDashboardInputViewData = { const inputDto: GetDashboardDataOutputDTO = {
totalUsers: 200, totalUsers: 200,
activeUsers: 50, activeUsers: 50,
totalRaces: 0, totalRaces: 0,
totalLeagues: 0, totalLeagues: 0,
}; };
const viewData = AnalyticsDashboardViewDataBuilder.build(inputViewData); const viewData = AnalyticsDashboardViewDataBuilder.build(inputDto);
expect(viewData.metrics.userEngagementRate).toBeCloseTo(25); expect(viewData.metrics.userEngagementRate).toBeCloseTo(25);
expect(viewData.metrics.formattedEngagementRate).toBe('25.0%'); expect(viewData.metrics.formattedEngagementRate).toBe('25.0%');
}); });
it('handles zero users safely', () => { it('handles zero users safely', () => {
const inputViewData: AnalyticsDashboardInputViewData = { const inputDto: GetDashboardDataOutputDTO = {
totalUsers: 0, totalUsers: 0,
activeUsers: 0, activeUsers: 0,
totalRaces: 0, totalRaces: 0,
totalLeagues: 0, totalLeagues: 0,
}; };
const viewData = AnalyticsDashboardViewDataBuilder.build(inputViewData); const viewData = AnalyticsDashboardViewDataBuilder.build(inputDto);
expect(viewData.metrics.userEngagementRate).toBe(0); expect(viewData.metrics.userEngagementRate).toBe(0);
expect(viewData.metrics.formattedEngagementRate).toBe('0.0%'); expect(viewData.metrics.formattedEngagementRate).toBe('0.0%');

View File

@@ -1,30 +1,26 @@
import { AnalyticsDashboardInputViewData } from '@/lib/view-data/AnalyticsDashboardInputViewData'; 'use client';
import { AnalyticsDashboardViewData } from '@/lib/view-data/AnalyticsDashboardViewData';
/** import type { GetDashboardDataOutputDTO } from '@/lib/types/generated/GetDashboardDataOutputDTO';
* AnalyticsDashboardViewDataBuilder import type { AnalyticsDashboardViewData } from '@/lib/view-data/AnalyticsDashboardViewData';
* import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
* 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<any, any> { export class AnalyticsDashboardViewDataBuilder {
build(input: any): any { public static build(apiDto: GetDashboardDataOutputDTO): AnalyticsDashboardViewData {
return AnalyticsDashboardViewDataBuilder.build(input); const totalUsers = apiDto.totalUsers;
} const activeUsers = apiDto.activeUsers;
const totalRaces = apiDto.totalRaces;
const totalLeagues = apiDto.totalLeagues;
static build(viewData: AnalyticsDashboardInputViewData): AnalyticsDashboardViewData { const userEngagementRate = totalUsers > 0 ? (activeUsers / totalUsers) * 100 : 0;
const userEngagementRate = viewData.totalUsers > 0 ? (viewData.activeUsers / viewData.totalUsers) * 100 : 0;
const formattedEngagementRate = `${userEngagementRate.toFixed(1)}%`; const formattedEngagementRate = `${userEngagementRate.toFixed(1)}%`;
const activityLevel = userEngagementRate > 70 ? 'High' : userEngagementRate > 40 ? 'Medium' : 'Low'; const activityLevel = userEngagementRate > 70 ? 'High' : userEngagementRate > 40 ? 'Medium' : 'Low';
return { return {
metrics: { metrics: {
totalUsers: viewData.totalUsers, totalUsers,
activeUsers: viewData.activeUsers, activeUsers,
totalRaces: viewData.totalRaces, totalRaces,
totalLeagues: viewData.totalLeagues, totalLeagues,
userEngagementRate, userEngagementRate,
formattedEngagementRate, formattedEngagementRate,
activityLevel, activityLevel,
@@ -32,3 +28,5 @@ export class AnalyticsDashboardViewDataBuilder implements ViewDataBuilder<any, a
}; };
} }
} }
AnalyticsDashboardViewDataBuilder satisfies ViewDataBuilder<GetDashboardDataOutputDTO, AnalyticsDashboardViewData>;

View File

@@ -1,15 +1,18 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { AvatarViewDataBuilder } from './AvatarViewDataBuilder'; import { AvatarViewDataBuilder } from './AvatarViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
describe('AvatarViewDataBuilder', () => { describe('AvatarViewDataBuilder', () => {
describe('happy paths', () => { describe('happy paths', () => {
it('should transform MediaBinaryDTO to AvatarViewData correctly', () => { it('should transform binary data to AvatarViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = { const mediaDto = {
id: '1',
url: 'http://example.com/image.png',
type: 'image/png',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer, buffer: buffer.buffer,
contentType: 'image/png', } as unknown as GetMediaOutputDTO;
};
const result = AvatarViewDataBuilder.build(mediaDto); const result = AvatarViewDataBuilder.build(mediaDto);
@@ -19,173 +22,36 @@ describe('AvatarViewDataBuilder', () => {
it('should handle JPEG images', () => { it('should handle JPEG images', () => {
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
const mediaDto: MediaBinaryDTO = { const mediaDto = {
id: '2',
url: 'http://example.com/image.jpg',
type: 'image/jpeg',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer, buffer: buffer.buffer,
contentType: 'image/jpeg', } as unknown as GetMediaOutputDTO;
};
const result = AvatarViewDataBuilder.build(mediaDto); const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/jpeg'); expect(result.contentType).toBe('image/jpeg');
}); });
it('should handle GIF images', () => {
const buffer = new Uint8Array([0x47, 0x49, 0x46, 0x38]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/gif',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/gif');
});
it('should handle SVG images', () => {
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"></svg>');
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/svg+xml',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/svg+xml');
});
it('should handle WebP images', () => {
const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/webp',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/webp');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
AvatarViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
}); });
describe('edge cases', () => { describe('edge cases', () => {
it('should handle empty buffer', () => { it('should handle empty buffer', () => {
const buffer = new Uint8Array([]); const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = { const mediaDto = {
id: '3',
url: 'http://example.com/image.png',
type: 'image/png',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer, buffer: buffer.buffer,
contentType: 'image/png', } as unknown as GetMediaOutputDTO;
};
const result = AvatarViewDataBuilder.build(mediaDto); const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(''); expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png'); expect(result.contentType).toBe('image/png');
}); });
it('should handle large buffer', () => {
const buffer = new Uint8Array(1024 * 1024); // 1MB
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with all zeros', () => {
const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with all ones', () => {
const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle different content types', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const contentTypes = [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/svg+xml',
'image/bmp',
'image/tiff',
];
contentTypes.forEach((contentType) => {
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType,
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.contentType).toBe(contentType);
});
});
}); });
}); });

View File

@@ -1,25 +1,23 @@
/** 'use client';
* AvatarViewDataBuilder
*
* Transforms MediaBinaryDTO into AvatarViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
import { AvatarViewData } from '@/lib/view-data/AvatarViewData'; import type { AvatarViewData } from '@/lib/view-data/AvatarViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; export class AvatarViewDataBuilder {
public static build(apiDto: GetMediaOutputDTO): AvatarViewData {
export class AvatarViewDataBuilder implements ViewDataBuilder<any, any> { // Note: GetMediaOutputDTO from OpenAPI doesn't have buffer,
build(input: any): any { // but the implementation expects it for binary data.
return AvatarViewDataBuilder.build(input); // We use type assertion to handle the binary case while keeping the DTO type.
} const binaryDto = apiDto as unknown as { buffer?: ArrayBuffer };
const buffer = binaryDto.buffer;
static build( const contentType = apiDto.type;
static build(apiDto: MediaBinaryDTO): AvatarViewData {
return { return {
buffer: Buffer.from(apiDto.buffer).toString('base64'), buffer: buffer ? Buffer.from(buffer).toString('base64') : '',
contentType: apiDto.contentType, contentType,
}; };
} }
} }
AvatarViewDataBuilder satisfies ViewDataBuilder<GetMediaOutputDTO, AvatarViewData>;

View File

@@ -1,15 +1,18 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { CategoryIconViewDataBuilder } from './CategoryIconViewDataBuilder'; import { CategoryIconViewDataBuilder } from './CategoryIconViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
describe('CategoryIconViewDataBuilder', () => { describe('CategoryIconViewDataBuilder', () => {
describe('happy paths', () => { describe('happy paths', () => {
it('should transform MediaBinaryDTO to CategoryIconViewData correctly', () => { it('should transform binary data to CategoryIconViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = { const mediaDto = {
id: '1',
url: 'http://example.com/icon.png',
type: 'image/png',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer, buffer: buffer.buffer,
contentType: 'image/png', } as unknown as GetMediaOutputDTO;
};
const result = CategoryIconViewDataBuilder.build(mediaDto); const result = CategoryIconViewDataBuilder.build(mediaDto);
@@ -19,97 +22,36 @@ describe('CategoryIconViewDataBuilder', () => {
it('should handle SVG icons', () => { it('should handle SVG icons', () => {
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><circle cx="10" cy="10" r="5"/></svg>'); const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><circle cx="10" cy="10" r="5"/></svg>');
const mediaDto: MediaBinaryDTO = { const mediaDto = {
id: '2',
url: 'http://example.com/icon.svg',
type: 'image/svg+xml',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer, buffer: buffer.buffer,
contentType: 'image/svg+xml', } as unknown as GetMediaOutputDTO;
};
const result = CategoryIconViewDataBuilder.build(mediaDto); const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64')); expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/svg+xml'); expect(result.contentType).toBe('image/svg+xml');
}); });
it('should handle small icon files', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
CategoryIconViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
}); });
describe('edge cases', () => { describe('edge cases', () => {
it('should handle empty buffer', () => { it('should handle empty buffer', () => {
const buffer = new Uint8Array([]); const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = { const mediaDto = {
id: '3',
url: 'http://example.com/icon.png',
type: 'image/png',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer, buffer: buffer.buffer,
contentType: 'image/png', } as unknown as GetMediaOutputDTO;
};
const result = CategoryIconViewDataBuilder.build(mediaDto); const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(''); expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png'); expect(result.contentType).toBe('image/png');
}); });
it('should handle buffer with special characters', () => {
const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
}); });
}); });

View File

@@ -1,25 +1,21 @@
/** 'use client';
* CategoryIconViewDataBuilder
*
* Transforms MediaBinaryDTO into CategoryIconViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
import { CategoryIconViewData } from '@/lib/view-data/CategoryIconViewData'; import type { CategoryIconViewData } from '@/lib/view-data/CategoryIconViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; export class CategoryIconViewDataBuilder {
public static build(apiDto: GetMediaOutputDTO): CategoryIconViewData {
export class CategoryIconViewDataBuilder implements ViewDataBuilder<any, any> { // Note: GetMediaOutputDTO from OpenAPI doesn't have buffer,
build(input: any): any { // but the implementation expects it for binary data.
return CategoryIconViewDataBuilder.build(input); const binaryDto = apiDto as unknown as { buffer?: ArrayBuffer };
} const buffer = binaryDto.buffer;
static build(
static build(apiDto: MediaBinaryDTO): CategoryIconViewData {
return { return {
buffer: Buffer.from(apiDto.buffer).toString('base64'), buffer: buffer ? Buffer.from(buffer).toString('base64') : '',
contentType: apiDto.contentType, contentType: apiDto.type,
}; };
} }
} }
CategoryIconViewDataBuilder satisfies ViewDataBuilder<GetMediaOutputDTO, CategoryIconViewData>;

View File

@@ -1,32 +1,17 @@
/** 'use client';
* CompleteOnboarding ViewData Builder
*
* Transforms onboarding completion result into ViewData for templates.
*/
import { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO'; import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
import { CompleteOnboardingViewData } from './CompleteOnboardingViewData'; import type { CompleteOnboardingViewData } from '@/lib/view-data/CompleteOnboardingViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData'; import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; export class CompleteOnboardingViewDataBuilder {
public static build(apiDto: CompleteOnboardingOutputDTO): CompleteOnboardingViewData {
export class CompleteOnboardingViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return CompleteOnboardingViewDataBuilder.build(input);
}
static build(
/**
* Transform DTO into ViewData
*
* @param apiDto - The API DTO to transform
* @returns ViewData for templates
*/
static build(apiDto: CompleteOnboardingOutputDTO): CompleteOnboardingViewData {
return { return {
success: apiDto.success, success: apiDto.success,
driverId: apiDto.driverId, driverId: apiDto.driverId,
errorMessage: apiDto.errorMessage, errorMessage: apiDto.errorMessage,
}; };
} }
} }
CompleteOnboardingViewDataBuilder satisfies ViewDataBuilder<CompleteOnboardingOutputDTO, CompleteOnboardingViewData>;

View File

@@ -1,3 +1,5 @@
'use client';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO'; import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData'; import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay'; import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay';
@@ -6,24 +8,10 @@ import { DashboardRankDisplay } from '@/lib/display-objects/DashboardRankDisplay
import { DashboardConsistencyDisplay } from '@/lib/display-objects/DashboardConsistencyDisplay'; import { DashboardConsistencyDisplay } from '@/lib/display-objects/DashboardConsistencyDisplay';
import { DashboardCountDisplay } from '@/lib/display-objects/DashboardCountDisplay'; import { DashboardCountDisplay } from '@/lib/display-objects/DashboardCountDisplay';
import { DashboardLeaguePositionDisplay } from '@/lib/display-objects/DashboardLeaguePositionDisplay'; import { DashboardLeaguePositionDisplay } from '@/lib/display-objects/DashboardLeaguePositionDisplay';
import { ViewData } from '@/lib/contracts/view-data/ViewData'; import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { number } from 'zod';
/** export class DashboardViewDataBuilder {
* DashboardViewDataBuilder public static build(apiDto: DashboardOverviewDTO): DashboardViewData {
*
* Transforms DashboardOverviewDTO (API DTO) into DashboardViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class DashboardViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return DashboardViewDataBuilder.build(input);
}
static build(
static build(apiDto: DashboardOverviewDTO): DashboardViewData {
return { return {
currentDriver: { currentDriver: {
name: apiDto.currentDriver?.name || '', name: apiDto.currentDriver?.name || '',
@@ -49,11 +37,11 @@ export class DashboardViewDataBuilder implements ViewDataBuilder<any, any> {
id: item.id, id: item.id,
type: item.type, type: item.type,
headline: item.headline, headline: item.headline,
body: item.body, body: item.body ?? undefined,
timestamp: item.timestamp, timestamp: item.timestamp,
formattedTime: DashboardDateDisplay.format(new Date(item.timestamp)).relative, formattedTime: DashboardDateDisplay.format(new Date(item.timestamp)).relative,
ctaHref: item.ctaHref, ctaHref: item.ctaHref ?? undefined,
ctaLabel: item.ctaLabel, ctaLabel: item.ctaLabel ?? undefined,
})), })),
friends: apiDto.friends.map((friend) => ({ friends: apiDto.friends.map((friend) => ({
id: friend.id, id: friend.id,
@@ -97,4 +85,6 @@ export class DashboardViewDataBuilder implements ViewDataBuilder<any, any> {
isMyLeague: race.isMyLeague, isMyLeague: race.isMyLeague,
}; };
} }
} }
DashboardViewDataBuilder satisfies ViewDataBuilder<DashboardOverviewDTO, DashboardViewData>;

View File

@@ -1,6 +0,0 @@
import { ViewData } from "@/lib/contracts/view-data/ViewData";
export interface DeleteMediaViewData extends ViewData {
success: boolean;
error?: string;
}

View File

@@ -1,30 +1,16 @@
/** 'use client';
* DeleteMedia ViewData Builder
*
* Transforms media deletion result into ViewData for templates.
*/
import { DeleteMediaOutputDTO } from '@/lib/types/generated/DeleteMediaOutputDTO'; import type { DeleteMediaOutputDTO } from '@/lib/types/generated/DeleteMediaOutputDTO';
import { DeleteMediaViewData } from './DeleteMediaViewData'; import type { DeleteMediaViewData } from '@/lib/view-data/DeleteMediaViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData'; import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; export class DeleteMediaViewDataBuilder {
public static build(apiDto: DeleteMediaOutputDTO): DeleteMediaViewData {
export class DeleteMediaViewDataBuilder implements ViewDataBuilder<any, any> {
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 { return {
success: apiDto.success, success: apiDto.success,
error: apiDto.error, error: apiDto.error,
}; };
} }
} }
DeleteMediaViewDataBuilder satisfies ViewDataBuilder<DeleteMediaOutputDTO, DeleteMediaViewData>;

View File

@@ -1,3 +1,5 @@
'use client';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO'; import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
import type { DriverProfileViewData } from '@/lib/view-data/DriverProfileViewData'; import type { DriverProfileViewData } from '@/lib/view-data/DriverProfileViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateDisplay } from '@/lib/display-objects/DateDisplay';
@@ -5,22 +7,10 @@ import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
import { FinishDisplay } from '@/lib/display-objects/FinishDisplay'; import { FinishDisplay } from '@/lib/display-objects/FinishDisplay';
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay'; import { PercentDisplay } from '@/lib/display-objects/PercentDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
/** export class DriverProfileViewDataBuilder {
* DriverProfileViewDataBuilder public static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData {
*
* Transforms GetDriverProfileOutputDTO into ViewData for the driver profile page.
* Deterministic, side-effect free, no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class DriverProfileViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return DriverProfileViewDataBuilder.build(input);
}
static build(
static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData {
return { return {
currentDriver: apiDto.currentDriver ? { currentDriver: apiDto.currentDriver ? {
id: apiDto.currentDriver.id, id: apiDto.currentDriver.id,
@@ -116,3 +106,5 @@ export class DriverProfileViewDataBuilder implements ViewDataBuilder<any, any> {
}; };
} }
} }
DriverProfileViewDataBuilder satisfies ViewDataBuilder<GetDriverProfileOutputDTO, DriverProfileViewData>;

View File

@@ -1,16 +1,13 @@
'use client';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewData'; import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData'; import { WinRateDisplay } from '@/lib/display-objects/WinRateDisplay';
import { MedalDisplay } from '@/lib/display-objects/MedalDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; export class DriverRankingsViewDataBuilder {
public static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData {
export class DriverRankingsViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return DriverRankingsViewDataBuilder.build(input);
}
static build(
static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData {
if (!apiDto || apiDto.length === 0) { if (!apiDto || apiDto.length === 0) {
return { return {
drivers: [], drivers: [],
@@ -57,4 +54,6 @@ export class DriverRankingsViewDataBuilder implements ViewDataBuilder<any, any>
showFilters: false, showFilters: false,
}; };
} }
} }
DriverRankingsViewDataBuilder satisfies ViewDataBuilder<DriverLeaderboardItemDTO[], DriverRankingsViewData>;

View File

@@ -1,40 +1,38 @@
'use client';
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO'; import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
import type { DriversViewData } from '@/lib/view-data/DriversViewData'; import type { DriversViewData } from '@/lib/view-data/DriversViewData';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; export class DriversViewDataBuilder {
public static build(apiDto: DriversLeaderboardDTO): DriversViewData {
export class DriversViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return DriversViewDataBuilder.build(input);
}
static build(
static build(dto: DriversLeaderboardDTO): DriversViewData {
return { return {
drivers: dto.drivers.map(driver => ({ drivers: apiDto.drivers.map(driver => ({
id: driver.id, id: driver.id,
name: driver.name, name: driver.name,
rating: driver.rating, rating: driver.rating,
ratingLabel: RatingDisplay.format(driver.rating), ratingLabel: RatingDisplay.format(driver.rating),
skillLevel: driver.skillLevel, skillLevel: driver.skillLevel,
category: driver.category, category: driver.category ?? undefined,
nationality: driver.nationality, nationality: driver.nationality,
racesCompleted: driver.racesCompleted, racesCompleted: driver.racesCompleted,
wins: driver.wins, wins: driver.wins,
podiums: driver.podiums, podiums: driver.podiums,
isActive: driver.isActive, isActive: driver.isActive,
rank: driver.rank, rank: driver.rank,
avatarUrl: driver.avatarUrl, avatarUrl: driver.avatarUrl ?? undefined,
})), })),
totalRaces: dto.totalRaces, totalRaces: apiDto.totalRaces,
totalRacesLabel: NumberDisplay.format(dto.totalRaces), totalRacesLabel: NumberDisplay.format(apiDto.totalRaces),
totalWins: dto.totalWins, totalWins: apiDto.totalWins,
totalWinsLabel: NumberDisplay.format(dto.totalWins), totalWinsLabel: NumberDisplay.format(apiDto.totalWins),
activeCount: dto.activeCount, activeCount: apiDto.activeCount,
activeCountLabel: NumberDisplay.format(dto.activeCount), activeCountLabel: NumberDisplay.format(apiDto.activeCount),
totalDriversLabel: NumberDisplay.format(dto.drivers.length), totalDriversLabel: NumberDisplay.format(apiDto.drivers.length),
}; };
} }
} }
DriversViewDataBuilder satisfies ViewDataBuilder<DriversLeaderboardDTO, DriversViewData>;

View File

@@ -1,24 +1,11 @@
/** 'use client';
* Forgot Password View Data Builder
*
* Transforms ForgotPasswordPageDTO into ViewData for the forgot password template.
* Deterministic, side-effect free, no business logic.
*/
import { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO'; import type { ForgotPasswordPageDTO } from '@/lib/types/generated/ForgotPasswordPageDTO';
import { ForgotPasswordViewData } from '../../view-data/ForgotPasswordViewData'; import type { ForgotPasswordViewData } from '@/lib/view-data/ForgotPasswordViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData'; import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { error } from 'console';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; export class ForgotPasswordViewDataBuilder {
public static build(apiDto: ForgotPasswordPageDTO): ForgotPasswordViewData {
export class ForgotPasswordViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return ForgotPasswordViewDataBuilder.build(input);
}
static build(
static build(apiDto: ForgotPasswordPageDTO): ForgotPasswordViewData {
return { return {
returnTo: apiDto.returnTo, returnTo: apiDto.returnTo,
showSuccess: false, showSuccess: false,
@@ -35,4 +22,6 @@ export class ForgotPasswordViewDataBuilder implements ViewDataBuilder<any, any>
submitError: undefined, submitError: undefined,
}; };
} }
} }
ForgotPasswordViewDataBuilder satisfies ViewDataBuilder<ForgotPasswordPageDTO, ForgotPasswordViewData>;

View File

@@ -1,33 +1,17 @@
/** 'use client';
* GenerateAvatars ViewData Builder
*
* Transforms avatar generation result into ViewData for templates.
* Must be used in mutations to avoid returning DTOs directly.
*/
import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO'; import type { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
import { GenerateAvatarsViewData } from './GenerateAvatarsViewData'; import type { GenerateAvatarsViewData } from '@/lib/view-data/GenerateAvatarsViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData'; import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; export class GenerateAvatarsViewDataBuilder {
public static build(apiDto: RequestAvatarGenerationOutputDTO): GenerateAvatarsViewData {
export class GenerateAvatarsViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return GenerateAvatarsViewDataBuilder.build(input);
}
static build(
/**
* Transform DTO into ViewData
*
* @param apiDto - The API DTO to transform
* @returns ViewData for templates
*/
static build(apiDto: RequestAvatarGenerationOutputDTO): GenerateAvatarsViewData {
return { return {
success: apiDto.success, success: apiDto.success,
avatarUrls: apiDto.avatarUrls || [], avatarUrls: apiDto.avatarUrls || [],
errorMessage: apiDto.errorMessage, errorMessage: apiDto.errorMessage,
}; };
} }
} }
GenerateAvatarsViewDataBuilder satisfies ViewDataBuilder<RequestAvatarGenerationOutputDTO, GenerateAvatarsViewData>;

View File

@@ -1,80 +1,44 @@
/** 'use client';
* Health View Data Builder
*
* Transforms health DTO data into UI-ready view models.
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*/
import type { HealthDTO } from '@/lib/types/generated/HealthDTO';
import type { HealthViewData, HealthStatus, HealthMetrics, HealthComponent, HealthAlert } from '@/lib/view-data/HealthViewData'; import type { HealthViewData, HealthStatus, HealthMetrics, HealthComponent, HealthAlert } from '@/lib/view-data/HealthViewData';
import { HealthStatusDisplay } from '@/lib/display-objects/HealthStatusDisplay'; import { HealthStatusDisplay } from '@/lib/display-objects/HealthStatusDisplay';
import { HealthMetricDisplay } from '@/lib/display-objects/HealthMetricDisplay'; import { HealthMetricDisplay } from '@/lib/display-objects/HealthMetricDisplay';
import { HealthComponentDisplay } from '@/lib/display-objects/HealthComponentDisplay'; import { HealthComponentDisplay } from '@/lib/display-objects/HealthComponentDisplay';
import { HealthAlertDisplay } from '@/lib/display-objects/HealthAlertDisplay'; import { HealthAlertDisplay } from '@/lib/display-objects/HealthAlertDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
export interface HealthDTO { export class HealthViewDataBuilder {
status: 'ok' | 'degraded' | 'error' | 'unknown'; public static build(apiDto: HealthDTO): HealthViewData {
timestamp: string;
uptime?: number;
responseTime?: number;
errorRate?: number;
lastCheck?: string;
checksPassed?: number;
checksFailed?: number;
components?: Array<{
name: string;
status: 'ok' | 'degraded' | 'error' | 'unknown';
lastCheck?: string;
responseTime?: number;
errorRate?: number;
}>;
alerts?: Array<{
id: string;
type: 'critical' | 'warning' | 'info';
title: string;
message: string;
timestamp: string;
}>;
}
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class HealthViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return HealthViewDataBuilder.build(input);
}
static build(
static build(dto: HealthDTO): HealthViewData {
const now = new Date(); const now = new Date();
const lastUpdated = dto.timestamp || now.toISOString(); const lastUpdated = apiDto.timestamp || now.toISOString();
// Build overall status // Build overall status
const overallStatus: HealthStatus = { const overallStatus: HealthStatus = {
status: dto.status, status: apiDto.status,
timestamp: dto.timestamp, timestamp: apiDto.timestamp,
formattedTimestamp: HealthStatusDisplay.formatTimestamp(dto.timestamp), formattedTimestamp: HealthStatusDisplay.formatTimestamp(apiDto.timestamp),
relativeTime: HealthStatusDisplay.formatRelativeTime(dto.timestamp), relativeTime: HealthStatusDisplay.formatRelativeTime(apiDto.timestamp),
statusLabel: HealthStatusDisplay.formatStatusLabel(dto.status), statusLabel: HealthStatusDisplay.formatStatusLabel(apiDto.status),
statusColor: HealthStatusDisplay.formatStatusColor(dto.status), statusColor: HealthStatusDisplay.formatStatusColor(apiDto.status),
statusIcon: HealthStatusDisplay.formatStatusIcon(dto.status), statusIcon: HealthStatusDisplay.formatStatusIcon(apiDto.status),
}; };
// Build metrics // Build metrics
const metrics: HealthMetrics = { const metrics: HealthMetrics = {
uptime: HealthMetricDisplay.formatUptime(dto.uptime), uptime: HealthMetricDisplay.formatUptime(apiDto.uptime),
responseTime: HealthMetricDisplay.formatResponseTime(dto.responseTime), responseTime: HealthMetricDisplay.formatResponseTime(apiDto.responseTime),
errorRate: HealthMetricDisplay.formatErrorRate(dto.errorRate), errorRate: HealthMetricDisplay.formatErrorRate(apiDto.errorRate),
lastCheck: dto.lastCheck || lastUpdated, lastCheck: apiDto.lastCheck || lastUpdated,
formattedLastCheck: HealthMetricDisplay.formatTimestamp(dto.lastCheck || lastUpdated), formattedLastCheck: HealthMetricDisplay.formatTimestamp(apiDto.lastCheck || lastUpdated),
checksPassed: dto.checksPassed || 0, checksPassed: apiDto.checksPassed || 0,
checksFailed: dto.checksFailed || 0, checksFailed: apiDto.checksFailed || 0,
totalChecks: (dto.checksPassed || 0) + (dto.checksFailed || 0), totalChecks: (apiDto.checksPassed || 0) + (apiDto.checksFailed || 0),
successRate: HealthMetricDisplay.formatSuccessRate(dto.checksPassed, dto.checksFailed), successRate: HealthMetricDisplay.formatSuccessRate(apiDto.checksPassed, apiDto.checksFailed),
}; };
// Build components // Build components
const components: HealthComponent[] = (dto.components || []).map((component) => ({ const components: HealthComponent[] = (apiDto.components || []).map((component) => ({
name: component.name, name: component.name,
status: component.status, status: component.status,
statusLabel: HealthComponentDisplay.formatStatusLabel(component.status), statusLabel: HealthComponentDisplay.formatStatusLabel(component.status),
@@ -87,7 +51,7 @@ export class HealthViewDataBuilder implements ViewDataBuilder<any, any> {
})); }));
// Build alerts // Build alerts
const alerts: HealthAlert[] = (dto.alerts || []).map((alert) => ({ const alerts: HealthAlert[] = (apiDto.alerts || []).map((alert) => ({
id: alert.id, id: alert.id,
type: alert.type, type: alert.type,
title: alert.title, title: alert.title,
@@ -117,3 +81,5 @@ export class HealthViewDataBuilder implements ViewDataBuilder<any, any> {
}; };
} }
} }
HealthViewDataBuilder satisfies ViewDataBuilder<HealthDTO, HealthViewData>;

View File

@@ -1,34 +1,33 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { HomeViewDataBuilder } from './HomeViewDataBuilder'; import { HomeViewDataBuilder } from './HomeViewDataBuilder';
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO'; import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
describe('HomeViewDataBuilder', () => { describe('HomeViewDataBuilder', () => {
describe('happy paths', () => { describe('happy paths', () => {
it('should transform HomeDataDTO to HomeViewData correctly', () => { it('should transform DashboardOverviewDTO to HomeViewData correctly', () => {
const homeDataDto: HomeDataDTO = { const homeDataDto: DashboardOverviewDTO = {
isAlpha: true, currentDriver: null,
upcomingRaces: [ upcomingRaces: [
{ {
id: 'race-1', id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
track: 'Test Track', track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
isMyLeague: false,
}, },
], ],
topLeagues: [ leagueStandingsSummaries: [
{ {
id: 'league-1', leagueId: 'league-1',
name: 'Test League', leagueName: 'Test League',
description: 'Test Description', position: 1,
}, points: 100,
], totalDrivers: 20,
teams: [
{
id: 'team-1',
name: 'Test Team',
tag: 'TT',
}, },
], ],
feedSummary: { items: [] },
friends: [],
activeLeaguesCount: 1,
}; };
const result = HomeViewDataBuilder.build(homeDataDto); const result = HomeViewDataBuilder.build(homeDataDto);
@@ -38,130 +37,58 @@ describe('HomeViewDataBuilder', () => {
upcomingRaces: [ upcomingRaces: [
{ {
id: 'race-1', id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
track: 'Test Track', track: 'Test Track',
car: 'Test Car',
formattedDate: 'Mon, Jan 1, 2024',
}, },
], ],
topLeagues: [ topLeagues: [
{ {
id: 'league-1', id: 'league-1',
name: 'Test League', name: 'Test League',
description: 'Test Description', description: '',
},
],
teams: [
{
id: 'team-1',
name: 'Test Team',
tag: 'TT',
}, },
], ],
teams: [],
}); });
}); });
it('should handle empty arrays correctly', () => { it('should handle empty arrays correctly', () => {
const homeDataDto: HomeDataDTO = { const homeDataDto: DashboardOverviewDTO = {
isAlpha: false, currentDriver: null,
upcomingRaces: [], upcomingRaces: [],
topLeagues: [], leagueStandingsSummaries: [],
teams: [], feedSummary: { items: [] },
friends: [],
activeLeaguesCount: 0,
}; };
const result = HomeViewDataBuilder.build(homeDataDto); const result = HomeViewDataBuilder.build(homeDataDto);
expect(result).toEqual({ expect(result).toEqual({
isAlpha: false, isAlpha: true,
upcomingRaces: [], upcomingRaces: [],
topLeagues: [], topLeagues: [],
teams: [], teams: [],
}); });
}); });
it('should handle multiple items in arrays', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [
{ id: 'race-1', name: 'Race 1', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track 1' },
{ id: 'race-2', name: 'Race 2', scheduledAt: '2024-01-02T10:00:00Z', track: 'Track 2' },
],
topLeagues: [
{ id: 'league-1', name: 'League 1', description: 'Description 1' },
{ id: 'league-2', name: 'League 2', description: 'Description 2' },
],
teams: [
{ id: 'team-1', name: 'Team 1', tag: 'T1' },
{ id: 'team-2', name: 'Team 2', tag: 'T2' },
],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.upcomingRaces).toHaveLength(2);
expect(result.topLeagues).toHaveLength(2);
expect(result.teams).toHaveLength(2);
});
}); });
describe('data transformation', () => { describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.isAlpha).toBe(homeDataDto.isAlpha);
expect(result.upcomingRaces).toEqual(homeDataDto.upcomingRaces);
expect(result.topLeagues).toEqual(homeDataDto.topLeagues);
expect(result.teams).toEqual(homeDataDto.teams);
});
it('should not modify the input DTO', () => { it('should not modify the input DTO', () => {
const homeDataDto: HomeDataDTO = { const homeDataDto: DashboardOverviewDTO = {
isAlpha: true, currentDriver: null,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }], upcomingRaces: [{ id: 'race-1', track: 'Track', car: 'Car', scheduledAt: '2024-01-01T10:00:00Z', isMyLeague: false }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }], leagueStandingsSummaries: [{ leagueId: 'league-1', leagueName: 'League', position: 1, points: 10, totalDrivers: 10 }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }], feedSummary: { items: [] },
friends: [],
activeLeaguesCount: 1,
}; };
const originalDto = { ...homeDataDto }; const originalDto = JSON.parse(JSON.stringify(homeDataDto));
HomeViewDataBuilder.build(homeDataDto); HomeViewDataBuilder.build(homeDataDto);
expect(homeDataDto).toEqual(originalDto); expect(homeDataDto).toEqual(originalDto);
}); });
}); });
describe('edge cases', () => {
it('should handle false isAlpha value', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: false,
upcomingRaces: [],
topLeagues: [],
teams: [],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.isAlpha).toBe(false);
});
it('should handle null/undefined values in arrays', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.upcomingRaces[0].id).toBe('race-1');
expect(result.topLeagues[0].id).toBe('league-1');
expect(result.teams[0].id).toBe('team-1');
});
});
}); });

View File

@@ -1,32 +1,34 @@
import type { HomeViewData } from '@/templates/HomeTemplate'; 'use client';
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
/** import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
* HomeViewDataBuilder import type { HomeViewData } from '@/lib/view-data/HomeViewData';
* import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
* Transforms HomeDataDTO to HomeViewData. import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay';
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class HomeViewDataBuilder implements ViewDataBuilder<any, any> { export class HomeViewDataBuilder {
build(input: any): any {
return HomeViewDataBuilder.build(input);
}
static build(
/** /**
* Build HomeViewData from HomeDataDTO * Build HomeViewData from DashboardOverviewDTO
* *
* @param apiDto - The API DTO * @param apiDto - The API DTO
* @returns HomeViewData * @returns HomeViewData
*/ */
static build(apiDto: HomeDataDTO): HomeViewData { public static build(apiDto: DashboardOverviewDTO): HomeViewData {
return { return {
isAlpha: apiDto.isAlpha, isAlpha: true,
upcomingRaces: apiDto.upcomingRaces, upcomingRaces: (apiDto.upcomingRaces || []).map(race => ({
topLeagues: apiDto.topLeagues, id: race.id,
teams: apiDto.teams, track: race.track,
car: race.car,
formattedDate: DashboardDateDisplay.format(new Date(race.scheduledAt)).date,
})),
topLeagues: (apiDto.leagueStandingsSummaries || []).map(league => ({
id: league.leagueId,
name: league.leagueName,
description: '',
})),
teams: [],
}; };
} }
} }
HomeViewDataBuilder satisfies ViewDataBuilder<DashboardOverviewDTO, HomeViewData>;

View File

@@ -1,19 +1,17 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData'; 'use client';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData'; import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; type LeaderboardsInputDTO = {
drivers: { drivers: DriverLeaderboardItemDTO[] };
teams: GetTeamsLeaderboardOutputDTO;
}
export class LeaderboardsViewDataBuilder implements ViewDataBuilder<any, any> { export class LeaderboardsViewDataBuilder {
build(input: any): any { public static build(apiDto: LeaderboardsInputDTO): LeaderboardsViewData {
return LeaderboardsViewDataBuilder.build(input);
}
static build(
static build(
apiDto: { drivers: { drivers: DriverLeaderboardItemDTO[] }; teams: GetTeamsLeaderboardOutputDTO }
): LeaderboardsViewData {
return { return {
drivers: apiDto.drivers.drivers.map(driver => ({ drivers: apiDto.drivers.drivers.map(driver => ({
id: driver.id, id: driver.id,
@@ -45,3 +43,5 @@ export class LeaderboardsViewDataBuilder implements ViewDataBuilder<any, any> {
}; };
} }
} }
LeaderboardsViewDataBuilder satisfies ViewDataBuilder<LeaderboardsInputDTO, LeaderboardsViewData>;

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { LeagueCoverViewDataBuilder } from './LeagueCoverViewDataBuilder'; import { LeagueCoverViewDataBuilder } from './LeagueCoverViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import type { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
describe('LeagueCoverViewDataBuilder', () => { describe('LeagueCoverViewDataBuilder', () => {
describe('happy paths', () => { describe('happy paths', () => {

View File

@@ -1,25 +1,16 @@
/** 'use client';
* LeagueCoverViewDataBuilder
*
* Transforms MediaBinaryDTO into LeagueCoverViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import type { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
import { LeagueCoverViewData } from '@/lib/view-data/LeagueCoverViewData'; import type { LeagueCoverViewData } from '@/lib/view-data/LeagueCoverViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; export class LeagueCoverViewDataBuilder {
public static build(apiDto: MediaBinaryDTO): LeagueCoverViewData {
export class LeagueCoverViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LeagueCoverViewDataBuilder.build(input);
}
static build(
static build(apiDto: MediaBinaryDTO): LeagueCoverViewData {
return { return {
buffer: Buffer.from(apiDto.buffer).toString('base64'), buffer: Buffer.from(apiDto.buffer).toString('base64'),
contentType: apiDto.contentType, contentType: apiDto.contentType,
}; };
} }
} }
LeagueCoverViewDataBuilder satisfies ViewDataBuilder<MediaBinaryDTO, LeagueCoverViewData>;

View File

@@ -1,33 +1,32 @@
'use client';
import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO'; import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO'; import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO'; import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO'; import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO'; import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
import type { LeagueDetailViewData, LeagueInfoData, LiveRaceData, DriverSummaryData, SponsorInfo, NextRaceInfo, SeasonProgress, RecentResult } from '@/lib/view-data/LeagueDetailViewData'; import type { LeagueDetailViewData, LeagueInfoData, LiveRaceData, DriverSummaryData, SponsorInfo, NextRaceInfo, SeasonProgress, RecentResult } from '@/lib/view-data/LeagueDetailViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
/** type LeagueDetailInputDTO = {
* LeagueDetailViewDataBuilder league: LeagueWithCapacityAndScoringDTO;
* owner: GetDriverOutputDTO | null;
* Transforms API DTOs into LeagueDetailViewData for server-side rendering. scoringConfig: LeagueScoringConfigDTO | null;
* Deterministic; side-effect free; no HTTP calls. memberships: LeagueMembershipsDTO;
*/ races: RaceDTO[];
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; sponsors: Array<{
id: string;
name: string;
tier: string;
logoUrl?: string;
websiteUrl?: string;
tagline?: string;
}>;
}
export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> { export class LeagueDetailViewDataBuilder {
build(input: any): any { public static build(apiDto: LeagueDetailInputDTO): LeagueDetailViewData {
return LeagueDetailViewDataBuilder.build(input); const { league, owner, scoringConfig, memberships, races, sponsors } = apiDto;
}
static build(
static build(input: {
league: LeagueWithCapacityAndScoringDTO;
owner: GetDriverOutputDTO | null;
scoringConfig: LeagueScoringConfigDTO | null;
memberships: LeagueMembershipsDTO;
races: RaceDTO[];
sponsors: any[];
}): LeagueDetailViewData {
const { league, owner, scoringConfig, memberships, races, sponsors } = input;
// Calculate running races - using available fields from RaceDTO // Calculate running races - using available fields from RaceDTO
const runningRaces: LiveRaceData[] = races const runningRaces: LiveRaceData[] = races
@@ -44,31 +43,17 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
const membersCount = Array.isArray(memberships.members) ? memberships.members.length : 0; const membersCount = Array.isArray(memberships.members) ? memberships.members.length : 0;
// League overview wants total races, not just completed. // League overview wants total races, not just completed.
// (In seed/demo data many races are `status: running`, which should still count.)
const racesCount = races.length; const racesCount = races.length;
// Compute real avgSOF from races // Compute real avgSOF from races
const racesWithSOF = races.filter(r => { const racesWithSOF = races.filter(r => {
const sof = (r as any).strengthOfField; const sof = (r as RaceDTO & { strengthOfField?: number }).strengthOfField;
return typeof sof === 'number' && sof > 0; return typeof sof === 'number' && sof > 0;
}); });
const avgSOF = racesWithSOF.length > 0 const avgSOF = racesWithSOF.length > 0
? Math.round(racesWithSOF.reduce((sum, r) => sum + ((r as any).strengthOfField || 0), 0) / racesWithSOF.length) ? Math.round(racesWithSOF.reduce((sum, r) => sum + ((r as RaceDTO & { strengthOfField?: number }).strengthOfField || 0), 0) / racesWithSOF.length)
: null; : null;
if (process.env.NODE_ENV !== 'production') {
const race0 = races.length > 0 ? races[0] : null;
console.info(
'[LeagueDetailViewDataBuilder] leagueId=%s members=%d races=%d racesWithSOF=%d avgSOF=%s race0=%o',
league.id,
membersCount,
racesCount,
racesWithSOF.length,
String(avgSOF),
race0,
);
}
const info: LeagueInfoData = { const info: LeagueInfoData = {
name: league.name, name: league.name,
description: league.description || '', description: league.description || '',
@@ -111,7 +96,7 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
.map(m => ({ .map(m => ({
driverId: m.driverId, driverId: m.driverId,
driverName: m.driver.name, driverName: m.driver.name,
avatarUrl: (m.driver as any).avatarUrl || null, avatarUrl: (m.driver as GetDriverOutputDTO & { avatarUrl?: string }).avatarUrl || null,
rating: null, rating: null,
rank: null, rank: null,
roleBadgeText: 'Admin', roleBadgeText: 'Admin',
@@ -124,7 +109,7 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
.map(m => ({ .map(m => ({
driverId: m.driverId, driverId: m.driverId,
driverName: m.driver.name, driverName: m.driver.name,
avatarUrl: (m.driver as any).avatarUrl || null, avatarUrl: (m.driver as GetDriverOutputDTO & { avatarUrl?: string }).avatarUrl || null,
rating: null, rating: null,
rank: null, rank: null,
roleBadgeText: 'Steward', roleBadgeText: 'Steward',
@@ -137,7 +122,7 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
.map(m => ({ .map(m => ({
driverId: m.driverId, driverId: m.driverId,
driverName: m.driver.name, driverName: m.driver.name,
avatarUrl: (m.driver as any).avatarUrl || null, avatarUrl: (m.driver as GetDriverOutputDTO & { avatarUrl?: string }).avatarUrl || null,
rating: null, rating: null,
rank: null, rank: null,
roleBadgeText: 'Member', roleBadgeText: 'Member',
@@ -154,8 +139,8 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
id: r.id, id: r.id,
name: r.name, name: r.name,
date: r.date, date: r.date,
track: (r as any).track, track: (r as RaceDTO & { track?: string }).track || '',
car: (r as any).car, car: (r as RaceDTO & { car?: string }).car || '',
}))[0]; }))[0];
// Calculate season progress (completed races vs total races) // Calculate season progress (completed races vs total races)
@@ -179,8 +164,8 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
.map(r => ({ .map(r => ({
raceId: r.id, raceId: r.id,
raceName: r.name, raceName: r.name,
position: (r as any).position || 0, position: (r as RaceDTO & { position?: number }).position || 0,
points: (r as any).points || 0, points: (r as RaceDTO & { points?: number }).points || 0,
finishedAt: r.date, finishedAt: r.date,
})); }));
@@ -196,13 +181,15 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
adminSummaries, adminSummaries,
stewardSummaries, stewardSummaries,
memberSummaries, memberSummaries,
sponsorInsights: null, // Only for sponsor mode sponsorInsights: null,
nextRace, nextRace,
seasonProgress, seasonProgress,
recentResults, recentResults,
walletBalance: league.walletBalance, walletBalance: league.walletBalance ?? 0,
pendingProtestsCount: league.pendingProtestsCount, pendingProtestsCount: league.pendingProtestsCount ?? 0,
pendingJoinRequestsCount: league.pendingJoinRequestsCount, pendingJoinRequestsCount: league.pendingJoinRequestsCount ?? 0,
}; };
} }
} }
LeagueDetailViewDataBuilder satisfies ViewDataBuilder<LeagueDetailInputDTO, LeagueDetailViewData>;

View File

@@ -1,25 +1,14 @@
/** import type { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
* LeagueLogoViewDataBuilder import type { LeagueLogoViewData } from '@/lib/view-data/LeagueLogoViewData';
* import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
* Transforms MediaBinaryDTO into LeagueLogoViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; export class LeagueLogoViewDataBuilder {
import { LeagueLogoViewData } from '@/lib/view-data/LeagueLogoViewData'; public static build(apiDto: MediaBinaryDTO): LeagueLogoViewData {
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeagueLogoViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LeagueLogoViewDataBuilder.build(input);
}
static build(
static build(apiDto: MediaBinaryDTO): LeagueLogoViewData {
return { return {
buffer: Buffer.from(apiDto.buffer).toString('base64'), buffer: Buffer.from(apiDto.buffer).toString('base64'),
contentType: apiDto.contentType, contentType: apiDto.contentType,
}; };
} }
} }
LeagueLogoViewDataBuilder satisfies ViewDataBuilder<MediaBinaryDTO, LeagueLogoViewData>;

View File

@@ -2,26 +2,16 @@ import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMe
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO'; import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
import type { LeagueRosterAdminViewData, RosterMemberData, JoinRequestData } from '@/lib/view-data/LeagueRosterAdminViewData'; import type { LeagueRosterAdminViewData, RosterMemberData, JoinRequestData } from '@/lib/view-data/LeagueRosterAdminViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
/** type LeagueRosterAdminInputDTO = {
* LeagueRosterAdminViewDataBuilder leagueId: string;
* members: LeagueRosterMemberDTO[];
* Transforms API DTOs into LeagueRosterAdminViewData for server-side rendering. joinRequests: LeagueRosterJoinRequestDTO[];
* Deterministic; side-effect free; no HTTP calls. }
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeagueRosterAdminViewDataBuilder implements ViewDataBuilder<any, any> { export class LeagueRosterAdminViewDataBuilder {
build(input: any): any { public static build(input: LeagueRosterAdminInputDTO): LeagueRosterAdminViewData {
return LeagueRosterAdminViewDataBuilder.build(input);
}
static build(
static build(input: {
leagueId: string;
members: LeagueRosterMemberDTO[];
joinRequests: LeagueRosterJoinRequestDTO[];
}): LeagueRosterAdminViewData {
const { leagueId, members, joinRequests } = input; const { leagueId, members, joinRequests } = input;
// Transform members // Transform members
@@ -29,7 +19,7 @@ export class LeagueRosterAdminViewDataBuilder implements ViewDataBuilder<any, an
driverId: member.driverId, driverId: member.driverId,
driver: { driver: {
id: member.driverId, id: member.driverId,
name: member.driver?.name || 'Unknown Driver', name: (member.driver as { name?: string })?.name || 'Unknown Driver',
}, },
role: member.role, role: member.role,
joinedAt: member.joinedAt, joinedAt: member.joinedAt,
@@ -41,11 +31,11 @@ export class LeagueRosterAdminViewDataBuilder implements ViewDataBuilder<any, an
id: req.id, id: req.id,
driver: { driver: {
id: req.driverId, id: req.driverId,
name: 'Unknown Driver', // driver field is unknown type name: (req as { driver?: { name?: string } }).driver?.name || 'Unknown Driver',
}, },
requestedAt: req.requestedAt, requestedAt: req.requestedAt,
formattedRequestedAt: DateDisplay.formatShort(req.requestedAt), formattedRequestedAt: DateDisplay.formatShort(req.requestedAt),
message: req.message, message: req.message ?? undefined,
})); }));
return { return {
@@ -54,4 +44,6 @@ export class LeagueRosterAdminViewDataBuilder implements ViewDataBuilder<any, an
joinRequests: requests, joinRequests: requests,
}; };
} }
} }
LeagueRosterAdminViewDataBuilder satisfies ViewDataBuilder<LeagueRosterAdminInputDTO, LeagueRosterAdminViewData>;

View File

@@ -1,19 +1,24 @@
import { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData'; import type { LeagueScheduleViewData } from '@/lib/view-data/LeagueScheduleViewData';
import { LeagueScheduleApiDto } from '@/lib/types/tbd/LeagueScheduleApiDto'; import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeagueScheduleViewDataBuilder implements ViewDataBuilder<any, any> { export interface LeagueScheduleInputDTO {
build(input: any): any { apiDto: LeagueScheduleDTO;
return LeagueScheduleViewDataBuilder.build(input); currentDriverId?: string;
isAdmin?: boolean;
}
export class LeagueScheduleViewDataBuilder implements ViewDataBuilder<LeagueScheduleInputDTO, LeagueScheduleViewData> {
build(input: LeagueScheduleInputDTO): LeagueScheduleViewData {
return LeagueScheduleViewDataBuilder.build(input.apiDto, input.currentDriverId, input.isAdmin);
} }
static build( public static build(apiDto: LeagueScheduleDTO, currentDriverId?: string, isAdmin: boolean = false): LeagueScheduleViewData {
static build(apiDto: LeagueScheduleApiDto, currentDriverId?: string, isAdmin: boolean = false): LeagueScheduleViewData {
const now = new Date(); const now = new Date();
return { return {
leagueId: apiDto.leagueId, leagueId: (apiDto as any).leagueId || '',
races: apiDto.races.map((race) => { races: apiDto.races.map((race) => {
const scheduledAt = new Date(race.date); const scheduledAt = new Date(race.date);
const isPast = scheduledAt.getTime() <= now.getTime(); const isPast = scheduledAt.getTime() <= now.getTime();
@@ -23,12 +28,12 @@ export class LeagueScheduleViewDataBuilder implements ViewDataBuilder<any, any>
id: race.id, id: race.id,
name: race.name, name: race.name,
scheduledAt: race.date, scheduledAt: race.date,
track: race.track, track: race.track || '',
car: race.car, car: race.car || '',
sessionType: race.sessionType, sessionType: race.sessionType || 'race',
isPast, isPast,
isUpcoming, isUpcoming,
status: isPast ? 'completed' : 'scheduled', status: (race.status as any) || (isPast ? 'completed' : 'scheduled'),
// Registration info (would come from API in real implementation) // Registration info (would come from API in real implementation)
isUserRegistered: false, isUserRegistered: false,
canRegister: isUpcoming, canRegister: isUpcoming,

View File

@@ -1,5 +1,5 @@
import { LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto'; import type { LeagueSettingsDTO } from '@/lib/types/generated/LeagueSettingsDTO';
import { LeagueSettingsViewData } from '@/lib/view-data/LeagueSettingsViewData'; import type { LeagueSettingsViewData } from '@/lib/view-data/LeagueSettingsViewData';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -8,12 +8,13 @@ export class LeagueSettingsViewDataBuilder implements ViewDataBuilder<any, any>
return LeagueSettingsViewDataBuilder.build(input); return LeagueSettingsViewDataBuilder.build(input);
} }
static build( static build(apiDto: LeagueSettingsDTO): LeagueSettingsViewData {
static build(apiDto: LeagueSettingsApiDto): LeagueSettingsViewData {
return { return {
leagueId: apiDto.leagueId, league: (apiDto as any).league || { id: '', name: '', ownerId: '', createdAt: '' },
league: apiDto.league, config: (apiDto as any).config || {},
config: apiDto.config, presets: (apiDto as any).presets || [],
owner: (apiDto as any).owner || null,
members: (apiDto as any).members || [],
}; };
} }
} }

View File

@@ -10,7 +10,6 @@ export class LeagueSponsorshipsViewDataBuilder implements ViewDataBuilder<any, a
return LeagueSponsorshipsViewDataBuilder.build(input); return LeagueSponsorshipsViewDataBuilder.build(input);
} }
static build(
static build(apiDto: LeagueSponsorshipsApiDto): LeagueSponsorshipsViewData { static build(apiDto: LeagueSponsorshipsApiDto): LeagueSponsorshipsViewData {
return { return {
leagueId: apiDto.leagueId, leagueId: apiDto.leagueId,

View File

@@ -1,7 +1,6 @@
import { CurrencyDisplay } from '@/lib/display-objects/CurrencyDisplay'; import type { GetLeagueWalletOutputDTO } from '@/lib/types/generated/GetLeagueWalletOutputDTO';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import type { LeagueWalletViewData } from '@/lib/view-data/LeagueWalletViewData';
import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto'; import type { WalletTransactionViewData } from '@/lib/view-data/WalletTransactionViewData';
import { LeagueWalletTransactionViewData, LeagueWalletViewData } from '@/lib/view-data/LeagueWalletViewData';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -10,29 +9,29 @@ export class LeagueWalletViewDataBuilder implements ViewDataBuilder<any, any> {
return LeagueWalletViewDataBuilder.build(input); return LeagueWalletViewDataBuilder.build(input);
} }
static build( static build(apiDto: GetLeagueWalletOutputDTO): LeagueWalletViewData {
static build(apiDto: LeagueWalletApiDto): LeagueWalletViewData { const transactions: WalletTransactionViewData[] = apiDto.transactions.map(t => ({
const transactions: LeagueWalletTransactionViewData[] = apiDto.transactions.map(t => ({ id: t.id,
...t, type: t.type as any,
formattedAmount: CurrencyDisplay.format(t.amount, apiDto.currency), description: t.description,
amountColor: t.amount >= 0 ? 'green' : 'red', amount: t.amount,
formattedDate: DateDisplay.formatShort(t.createdAt), fee: t.fee,
statusColor: t.status === 'completed' ? 'green' : t.status === 'pending' ? 'yellow' : 'red', netAmount: t.netAmount,
typeColor: 'blue', date: (t as any).createdAt || (t as any).date || new Date().toISOString(),
status: t.status as any,
reference: t.reference,
})); }));
return { return {
leagueId: apiDto.leagueId,
balance: apiDto.balance, balance: apiDto.balance,
formattedBalance: CurrencyDisplay.format(apiDto.balance, apiDto.currency),
totalRevenue: apiDto.balance, // Mock
formattedTotalRevenue: CurrencyDisplay.format(apiDto.balance, apiDto.currency),
totalFees: 0, // Mock
formattedTotalFees: CurrencyDisplay.format(0, apiDto.currency),
pendingPayouts: 0, // Mock
formattedPendingPayouts: CurrencyDisplay.format(0, apiDto.currency),
currency: apiDto.currency, currency: apiDto.currency,
totalRevenue: apiDto.totalRevenue,
totalFees: apiDto.totalFees,
totalWithdrawals: apiDto.totalWithdrawals,
pendingPayouts: apiDto.pendingPayouts,
transactions, transactions,
canWithdraw: apiDto.canWithdraw,
withdrawalBlockReason: apiDto.withdrawalBlockReason,
}; };
} }
} }

View File

@@ -1,12 +1,6 @@
import type { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO'; import type { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO';
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData'; import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
/**
* LeaguesViewDataBuilder
*
* Transforms AllLeaguesWithCapacityAndScoringDTO (API DTO) into LeaguesViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeaguesViewDataBuilder implements ViewDataBuilder<any, any> { export class LeaguesViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -14,7 +8,6 @@ export class LeaguesViewDataBuilder implements ViewDataBuilder<any, any> {
return LeaguesViewDataBuilder.build(input); return LeaguesViewDataBuilder.build(input);
} }
static build(
static build(apiDto: AllLeaguesWithCapacityAndScoringDTO): LeaguesViewData { static build(apiDto: AllLeaguesWithCapacityAndScoringDTO): LeaguesViewData {
return { return {
leagues: apiDto.leagues.map((league) => ({ leagues: apiDto.leagues.map((league) => ({
@@ -36,7 +29,7 @@ export class LeaguesViewDataBuilder implements ViewDataBuilder<any, any> {
scoring: league.scoring ? { scoring: league.scoring ? {
gameId: league.scoring.gameId, gameId: league.scoring.gameId,
gameName: league.scoring.gameName, gameName: league.scoring.gameName,
primaryChampionshipType: league.scoring.primaryChampionshipType, primaryChampionshipType: league.scoring.primaryChampionshipType as any,
scoringPresetId: league.scoring.scoringPresetId, scoringPresetId: league.scoring.scoringPresetId,
scoringPresetName: league.scoring.scoringPresetName, scoringPresetName: league.scoring.scoringPresetName,
dropPolicySummary: league.scoring.dropPolicySummary, dropPolicySummary: league.scoring.dropPolicySummary,

View File

@@ -1,24 +1,9 @@
/** import type { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
* Login View Data Builder import type { LoginViewData } from '@/lib/view-data/LoginViewData';
* import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
* Transforms LoginPageDTO into ViewData for the login template.
* Deterministic, side-effect free, no business logic.
*/
import { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO'; export class LoginViewDataBuilder {
import { LoginViewData } from '../../view-data/LoginViewData'; public static build(apiDto: LoginPageDTO): LoginViewData {
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import { error } from 'console';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LoginViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LoginViewDataBuilder.build(input);
}
static build(
static build(apiDto: LoginPageDTO): LoginViewData {
return { return {
returnTo: apiDto.returnTo, returnTo: apiDto.returnTo,
hasInsufficientPermissions: apiDto.hasInsufficientPermissions, hasInsufficientPermissions: apiDto.hasInsufficientPermissions,
@@ -39,4 +24,6 @@ export class LoginViewDataBuilder implements ViewDataBuilder<any, any> {
submitError: undefined, submitError: undefined,
}; };
} }
} }
LoginViewDataBuilder satisfies ViewDataBuilder<LoginPageDTO, LoginViewData>;

View File

@@ -16,7 +16,6 @@ export class OnboardingViewDataBuilder implements ViewDataBuilder<any, any> {
return OnboardingViewDataBuilder.build(input); return OnboardingViewDataBuilder.build(input);
} }
static build(
static build(apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError>): Result<OnboardingPageViewData, PresentationError> { static build(apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError>): Result<OnboardingPageViewData, PresentationError> {
if (apiDto.isErr()) { if (apiDto.isErr()) {
return Result.err(apiDto.getError()); return Result.err(apiDto.getError());

View File

@@ -15,7 +15,6 @@ export class ProfileViewDataBuilder implements ViewDataBuilder<any, any> {
return ProfileViewDataBuilder.build(input); return ProfileViewDataBuilder.build(input);
} }
static build(
static build(apiDto: GetDriverProfileOutputDTO): ProfileViewData { static build(apiDto: GetDriverProfileOutputDTO): ProfileViewData {
const driver = apiDto.currentDriver; const driver = apiDto.currentDriver;

View File

@@ -1,33 +1,5 @@
import { ProtestDetailViewData } from '@/lib/view-data/ProtestDetailViewData'; import type { RaceProtestDTO } from '@/lib/types/generated/RaceProtestDTO';
import type { ProtestDetailViewData } from '@/lib/view-data/ProtestDetailViewData';
interface ProtestDetailApiDto {
id: string;
leagueId: string;
status: string;
submittedAt: string;
incident: {
lap: number;
description: string;
};
protestingDriver: {
id: string;
name: string;
};
accusedDriver: {
id: string;
name: string;
};
race: {
id: string;
name: string;
scheduledAt: string;
};
penaltyTypes: Array<{
type: string;
label: string;
description: string;
}>;
}
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -36,18 +8,20 @@ export class ProtestDetailViewDataBuilder implements ViewDataBuilder<any, any> {
return ProtestDetailViewDataBuilder.build(input); return ProtestDetailViewDataBuilder.build(input);
} }
static build( static build(apiDto: RaceProtestDTO): ProtestDetailViewData {
static build(apiDto: ProtestDetailApiDto): ProtestDetailViewData {
return { return {
protestId: apiDto.id, protestId: apiDto.id,
leagueId: apiDto.leagueId, leagueId: (apiDto as any).leagueId || '',
status: apiDto.status, status: apiDto.status,
submittedAt: apiDto.submittedAt, submittedAt: (apiDto as any).submittedAt || apiDto.filedAt,
incident: apiDto.incident, incident: {
protestingDriver: apiDto.protestingDriver, lap: (apiDto.incident as any)?.lap || 0,
accusedDriver: apiDto.accusedDriver, description: (apiDto.incident as any)?.description || '',
race: apiDto.race, },
penaltyTypes: apiDto.penaltyTypes, protestingDriver: (apiDto as any).protestingDriver || { id: apiDto.protestingDriverId, name: 'Unknown' },
accusedDriver: (apiDto as any).accusedDriver || { id: apiDto.accusedDriverId, name: 'Unknown' },
race: (apiDto as any).race || { id: '', name: '', scheduledAt: '' },
penaltyTypes: (apiDto as any).penaltyTypes || [],
}; };
} }
} }

View File

@@ -1,11 +1,6 @@
import { RaceDetailEntry, RaceDetailLeague, RaceDetailRace, RaceDetailRegistration, RaceDetailUserResult, RaceDetailViewData } from '@/lib/view-data/RaceDetailViewData'; import type { RaceDetailDTO } from '@/lib/types/generated/RaceDetailDTO';
import type { RaceDetailEntry, RaceDetailLeague, RaceDetailRace, RaceDetailRegistration, RaceDetailUserResult, RaceDetailViewData } from '@/lib/view-data/RaceDetailViewData';
/**
* Race Detail View Data Builder
*
* Transforms API DTO into ViewData for the race detail template.
* Deterministic, side-effect free.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> { export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -13,8 +8,7 @@ export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> {
return RaceDetailViewDataBuilder.build(input); return RaceDetailViewDataBuilder.build(input);
} }
static build( static build(apiDto: RaceDetailDTO): RaceDetailViewData {
static build(apiDto: any): RaceDetailViewData {
if (!apiDto || !apiDto.race) { if (!apiDto || !apiDto.race) {
return { return {
race: { race: {
@@ -36,11 +30,11 @@ export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> {
const race: RaceDetailRace = { const race: RaceDetailRace = {
id: apiDto.race.id, id: apiDto.race.id,
track: apiDto.race.track, track: apiDto.race.track || '',
car: apiDto.race.car, car: apiDto.race.car || '',
scheduledAt: apiDto.race.scheduledAt, scheduledAt: apiDto.race.scheduledAt,
status: apiDto.race.status as 'scheduled' | 'running' | 'completed' | 'cancelled', status: apiDto.race.status as any,
sessionType: apiDto.race.sessionType, sessionType: apiDto.race.sessionType || 'race',
}; };
const league: RaceDetailLeague | undefined = apiDto.league ? { const league: RaceDetailLeague | undefined = apiDto.league ? {
@@ -48,8 +42,8 @@ export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> {
name: apiDto.league.name, name: apiDto.league.name,
description: apiDto.league.description || undefined, description: apiDto.league.description || undefined,
settings: { settings: {
maxDrivers: apiDto.league.settings?.maxDrivers || 32, maxDrivers: (apiDto.league.settings as any)?.maxDrivers || 32,
qualifyingFormat: apiDto.league.settings?.qualifyingFormat || 'Open', qualifyingFormat: (apiDto.league.settings as any)?.qualifyingFormat || 'Open',
}, },
} : undefined; } : undefined;
@@ -83,7 +77,7 @@ export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> {
entryList, entryList,
registration, registration,
userResult, userResult,
canReopenRace: apiDto.canReopenRace || false, canReopenRace: (apiDto as any).canReopenRace || false,
}; };
} }
} }

View File

@@ -1,11 +1,6 @@
import { RaceResultsPenalty, RaceResultsResult, RaceResultsViewData } from '@/lib/view-data/RaceResultsViewData'; import type { RaceResultsDetailDTO } from '@/lib/types/generated/RaceResultsDetailDTO';
import type { RaceResultsPenalty, RaceResultsResult, RaceResultsViewData } from '@/lib/view-data/RaceResultsViewData';
/**
* Race Results View Data Builder
*
* Transforms API DTO into ViewData for the race results template.
* Deterministic, side-effect free.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class RaceResultsViewDataBuilder implements ViewDataBuilder<any, any> { export class RaceResultsViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -13,8 +8,7 @@ export class RaceResultsViewDataBuilder implements ViewDataBuilder<any, any> {
return RaceResultsViewDataBuilder.build(input); return RaceResultsViewDataBuilder.build(input);
} }
static build( static build(apiDto: RaceResultsDetailDTO): RaceResultsViewData {
static build(apiDto: unknown): RaceResultsViewData {
if (!apiDto) { if (!apiDto) {
return { return {
raceSOF: null, raceSOF: null,

View File

@@ -1,11 +1,6 @@
import { Driver, Penalty, Protest, RaceStewardingViewData } from '@/lib/view-data/RaceStewardingViewData'; import type { LeagueAdminProtestsDTO } from '@/lib/types/generated/LeagueAdminProtestsDTO';
import type { RaceStewardingViewData } from '@/lib/view-data/RaceStewardingViewData';
/**
* Race Stewarding View Data Builder
*
* Transforms API DTO into ViewData for the race stewarding template.
* Deterministic, side-effect free.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any> { export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -13,19 +8,14 @@ export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any>
return RaceStewardingViewDataBuilder.build(input); return RaceStewardingViewDataBuilder.build(input);
} }
static build( static build(apiDto: LeagueAdminProtestsDTO): RaceStewardingViewData {
static build(apiDto: unknown): RaceStewardingViewData {
if (!apiDto) { if (!apiDto) {
return { return {
race: null, race: null,
league: null, league: null,
pendingProtests: [], protests: [],
resolvedProtests: [],
penalties: [], penalties: [],
driverMap: {}, driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
}; };
} }
@@ -34,15 +24,20 @@ export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any>
const race = dto.race ? { const race = dto.race ? {
id: dto.race.id, id: dto.race.id,
track: dto.race.track, track: dto.race.track || '',
scheduledAt: dto.race.scheduledAt, scheduledAt: dto.race.scheduledAt,
status: dto.race.status || 'scheduled',
} : null; } : null;
const league = dto.league ? { const league = dto.league ? {
id: dto.league.id, id: dto.league.id,
name: dto.league.name || '',
} : null; } : null;
const pendingProtests: Protest[] = (dto.pendingProtests || []).map((p: any) => ({ const protests = [
...(dto.pendingProtests || []),
...(dto.resolvedProtests || []),
].map((p: any) => ({
id: p.id, id: p.id,
protestingDriverId: p.protestingDriverId, protestingDriverId: p.protestingDriverId,
accusedDriverId: p.accusedDriverId, accusedDriverId: p.accusedDriverId,
@@ -56,21 +51,7 @@ export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any>
decisionNotes: p.decisionNotes, decisionNotes: p.decisionNotes,
})); }));
const resolvedProtests: Protest[] = (dto.resolvedProtests || []).map((p: any) => ({ const penalties = (dto.penalties || []).map((p: any) => ({
id: p.id,
protestingDriverId: p.protestingDriverId,
accusedDriverId: p.accusedDriverId,
incident: {
lap: p.incident?.lap || 0,
description: p.incident?.description || '',
},
filedAt: p.filedAt,
status: p.status,
proofVideoUrl: p.proofVideoUrl,
decisionNotes: p.decisionNotes,
}));
const penalties: Penalty[] = (dto.penalties || []).map((p: any) => ({
id: p.id, id: p.id,
driverId: p.driverId, driverId: p.driverId,
type: p.type, type: p.type,
@@ -79,18 +60,14 @@ export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any>
notes: p.notes, notes: p.notes,
})); }));
const driverMap: Record<string, Driver> = dto.driverMap || {}; const driverMap = dto.driverMap || {};
return { return {
race, race,
league, league,
pendingProtests, protests,
resolvedProtests,
penalties, penalties,
driverMap, driverMap,
pendingCount: dto.pendingCount || pendingProtests.length,
resolvedCount: dto.resolvedCount || resolvedProtests.length,
penaltiesCount: dto.penaltiesCount || penalties.length,
}; };
} }
} }

View File

@@ -11,7 +11,6 @@ export class RacesViewDataBuilder implements ViewDataBuilder<any, any> {
return RacesViewDataBuilder.build(input); return RacesViewDataBuilder.build(input);
} }
static build(
static build(apiDto: RacesPageDataDTO): RacesViewData { static build(apiDto: RacesPageDataDTO): RacesViewData {
const now = new Date(); const now = new Date();
const races = apiDto.races.map((race): RaceViewData => { const races = apiDto.races.map((race): RaceViewData => {

View File

@@ -1,24 +1,9 @@
/** import type { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO';
* Reset Password View Data Builder import type { ResetPasswordViewData } from '@/lib/view-data/ResetPasswordViewData';
* import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
* Transforms ResetPasswordPageDTO into ViewData for the reset password template.
* Deterministic, side-effect free, no business logic.
*/
import { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO'; export class ResetPasswordViewDataBuilder {
import { ResetPasswordViewData } from '../../view-data/ResetPasswordViewData'; public static build(apiDto: ResetPasswordPageDTO): ResetPasswordViewData {
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import { error } from 'console';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class ResetPasswordViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return ResetPasswordViewDataBuilder.build(input);
}
static build(
static build(apiDto: ResetPasswordPageDTO): ResetPasswordViewData {
return { return {
token: apiDto.token, token: apiDto.token,
returnTo: apiDto.returnTo, returnTo: apiDto.returnTo,
@@ -37,4 +22,6 @@ export class ResetPasswordViewDataBuilder implements ViewDataBuilder<any, any> {
submitError: undefined, submitError: undefined,
}; };
} }
} }
ResetPasswordViewDataBuilder satisfies ViewDataBuilder<ResetPasswordPageDTO, ResetPasswordViewData>;

View File

@@ -1,14 +1,6 @@
import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO'; import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardViewData'; import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardViewData';
import { CurrencyDisplay } from '@/lib/display-objects/CurrencyDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
/**
* Sponsor Dashboard ViewData Builder
*
* Transforms SponsorDashboardDTO into ViewData for templates.
* Deterministic and side-effect free.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class SponsorDashboardViewDataBuilder implements ViewDataBuilder<any, any> { export class SponsorDashboardViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -16,32 +8,10 @@ export class SponsorDashboardViewDataBuilder implements ViewDataBuilder<any, any
return SponsorDashboardViewDataBuilder.build(input); return SponsorDashboardViewDataBuilder.build(input);
} }
static build(
static build(apiDto: SponsorDashboardDTO): SponsorDashboardViewData { static build(apiDto: SponsorDashboardDTO): SponsorDashboardViewData {
const totalInvestmentValue = apiDto.investment.activeSponsorships * 1000;
return { return {
sponsorId: (apiDto as any).sponsorId || '',
sponsorName: apiDto.sponsorName, sponsorName: apiDto.sponsorName,
totalImpressions: NumberDisplay.format(apiDto.metrics.impressions),
totalInvestment: CurrencyDisplay.format(totalInvestmentValue),
metrics: {
impressionsChange: apiDto.metrics.impressions > 1000 ? 15 : -5,
viewersChange: 8,
exposureChange: 12,
},
categoryData: {
leagues: { count: 2, countLabel: '2 active', impressions: 1500, impressionsLabel: '1,500' },
teams: { count: 1, countLabel: '1 active', impressions: 800, impressionsLabel: '800' },
drivers: { count: 3, countLabel: '3 active', impressions: 2200, impressionsLabel: '2,200' },
races: { count: 1, countLabel: '1 active', impressions: 500, impressionsLabel: '500' },
platform: { count: 0, countLabel: '0 active', impressions: 0, impressionsLabel: '0' },
},
sponsorships: apiDto.sponsorships,
activeSponsorships: apiDto.investment.activeSponsorships,
formattedTotalInvestment: CurrencyDisplay.format(totalInvestmentValue),
costPerThousandViews: CurrencyDisplay.format(50),
upcomingRenewals: [], // Mock empty for now
recentActivity: [], // Mock empty for now
}; };
} }
} }

View File

@@ -1,12 +1,5 @@
/** import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
* SponsorLogoViewDataBuilder import type { SponsorLogoViewData } from '@/lib/view-data/SponsorLogoViewData';
*
* Transforms MediaBinaryDTO into SponsorLogoViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { SponsorLogoViewData } from '@/lib/view-data/SponsorLogoViewData';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -15,11 +8,10 @@ export class SponsorLogoViewDataBuilder implements ViewDataBuilder<any, any> {
return SponsorLogoViewDataBuilder.build(input); return SponsorLogoViewDataBuilder.build(input);
} }
static build( static build(apiDto: GetMediaOutputDTO): SponsorLogoViewData {
static build(apiDto: MediaBinaryDTO): SponsorLogoViewData {
return { return {
buffer: Buffer.from(apiDto.buffer).toString('base64'), buffer: (apiDto as any).buffer ? Buffer.from((apiDto as any).buffer).toString('base64') : '',
contentType: apiDto.contentType, contentType: (apiDto as any).contentType || apiDto.type,
}; };
} }
} }

View File

@@ -8,7 +8,6 @@ export class SponsorshipRequestsViewDataBuilder implements ViewDataBuilder<any,
return SponsorshipRequestsViewDataBuilder.build(input); return SponsorshipRequestsViewDataBuilder.build(input);
} }
static build(
static build(apiDto: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData { static build(apiDto: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData {
return { return {
sections: [ sections: [

View File

@@ -1,14 +1,10 @@
import type { TeamDetailPageDto } from '@/lib/page-queries/TeamDetailPageQuery'; import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO';
import type { TeamDetailViewData, TeamDetailData, TeamMemberData, SponsorMetric, TeamTab } from '@/lib/view-data/TeamDetailViewData'; import type { TeamDetailViewData, TeamDetailData, TeamMemberData, SponsorMetric, TeamTab } from '@/lib/view-data/TeamDetailViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { MemberDisplay } from '@/lib/display-objects/MemberDisplay'; import { MemberDisplay } from '@/lib/display-objects/MemberDisplay';
import { LeagueDisplay } from '@/lib/display-objects/LeagueDisplay'; import { LeagueDisplay } from '@/lib/display-objects/LeagueDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
/**
* TeamDetailViewDataBuilder - Transforms TeamDetailPageDto into ViewData
* Deterministic; side-effect free; no HTTP calls
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> { export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -16,26 +12,25 @@ export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
return TeamDetailViewDataBuilder.build(input); return TeamDetailViewDataBuilder.build(input);
} }
static build( static build(apiDto: GetTeamDetailsOutputDTO): TeamDetailViewData {
static build(apiDto: TeamDetailPageDto): TeamDetailViewData {
const team: TeamDetailData = { const team: TeamDetailData = {
id: apiDto.team.id, id: apiDto.team.id,
name: apiDto.team.name, name: apiDto.team.name,
tag: apiDto.team.tag, tag: apiDto.team.tag,
description: apiDto.team.description, description: apiDto.team.description,
ownerId: apiDto.team.ownerId, ownerId: apiDto.team.ownerId,
leagues: apiDto.team.leagues, leagues: (apiDto.team as any).leagues || [],
createdAt: apiDto.team.createdAt, createdAt: apiDto.team.createdAt,
foundedDateLabel: apiDto.team.createdAt ? DateDisplay.formatMonthYear(apiDto.team.createdAt) : 'Unknown', foundedDateLabel: apiDto.team.createdAt ? DateDisplay.formatMonthYear(apiDto.team.createdAt) : 'Unknown',
specialization: apiDto.team.specialization, specialization: (apiDto.team as any).specialization || null,
region: apiDto.team.region, region: (apiDto.team as any).region || null,
languages: apiDto.team.languages, languages: (apiDto.team as any).languages || [],
category: apiDto.team.category, category: (apiDto.team as any).category || null,
membership: apiDto.team.membership, membership: (apiDto.team as any).membership || 'open',
canManage: apiDto.team.canManage, canManage: apiDto.canManage,
}; };
const memberships: TeamMemberData[] = apiDto.memberships.map((membership) => ({ const memberships: TeamMemberData[] = ((apiDto as any).memberships || []).map((membership: any) => ({
driverId: membership.driverId, driverId: membership.driverId,
driverName: membership.driverName, driverName: membership.driverName,
role: membership.role, role: membership.role,
@@ -46,7 +41,8 @@ export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
})); }));
// Calculate isAdmin based on current driver's role // Calculate isAdmin based on current driver's role
const currentDriverMembership = memberships.find(m => m.driverId === apiDto.currentDriverId); const currentDriverId = (apiDto as any).currentDriverId;
const currentDriverMembership = memberships.find(m => m.driverId === currentDriverId);
const isAdmin = currentDriverMembership?.role === 'owner' || currentDriverMembership?.role === 'manager'; const isAdmin = currentDriverMembership?.role === 'owner' || currentDriverMembership?.role === 'manager';
// Build sponsor metrics // Build sponsor metrics
@@ -89,7 +85,7 @@ export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
return { return {
team, team,
memberships, memberships,
currentDriverId: apiDto.currentDriverId, currentDriverId: currentDriverId || null,
isAdmin, isAdmin,
teamMetrics, teamMetrics,
tabs, tabs,

View File

@@ -1,4 +1,3 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData'; import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData';
@@ -9,7 +8,6 @@ export class TeamRankingsViewDataBuilder implements ViewDataBuilder<any, any> {
return TeamRankingsViewDataBuilder.build(input); return TeamRankingsViewDataBuilder.build(input);
} }
static build(
public static build(apiDto: GetTeamsLeaderboardOutputDTO): TeamRankingsViewData { public static build(apiDto: GetTeamsLeaderboardOutputDTO): TeamRankingsViewData {
const allTeams = apiDto.teams.map(t => ({ const allTeams = apiDto.teams.map(t => ({
...t, ...t,

View File

@@ -1,13 +1,9 @@
import type { TeamsPageDto } from '@/lib/page-queries/TeamsPageQuery'; import type { GetAllTeamsOutputDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO';
import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData'; import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO'; import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay'; import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
/**
* TeamsViewDataBuilder - Transforms TeamsPageDto into ViewData for TeamsTemplate
* Deterministic; side-effect free; no HTTP calls
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class TeamsViewDataBuilder implements ViewDataBuilder<any, any> { export class TeamsViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -15,23 +11,22 @@ export class TeamsViewDataBuilder implements ViewDataBuilder<any, any> {
return TeamsViewDataBuilder.build(input); return TeamsViewDataBuilder.build(input);
} }
static build( static build(apiDto: GetAllTeamsOutputDTO): TeamsViewData {
static build(apiDto: TeamsPageDto): TeamsViewData {
const teams: TeamSummaryData[] = apiDto.teams.map((team: TeamListItemDTO): TeamSummaryData => ({ const teams: TeamSummaryData[] = apiDto.teams.map((team: TeamListItemDTO): TeamSummaryData => ({
teamId: team.id, teamId: team.id,
teamName: team.name, teamName: team.name,
memberCount: team.memberCount, memberCount: team.memberCount,
logoUrl: team.logoUrl, logoUrl: team.logoUrl || '',
ratingLabel: RatingDisplay.format(team.rating), ratingLabel: RatingDisplay.format(team.rating),
ratingValue: team.rating || 0, ratingValue: team.rating || 0,
winsLabel: NumberDisplay.format(team.totalWins || 0), winsLabel: NumberDisplay.format(team.totalWins || 0),
racesLabel: NumberDisplay.format(team.totalRaces || 0), racesLabel: NumberDisplay.format(team.totalRaces || 0),
region: team.region, region: team.region || '',
isRecruiting: team.isRecruiting, isRecruiting: team.isRecruiting,
category: team.category, category: team.category || '',
performanceLevel: team.performanceLevel, performanceLevel: team.performanceLevel || '',
description: team.description, description: team.description || '',
countryCode: team.region, // Assuming region contains country code for now countryCode: team.region || '', // Assuming region contains country code for now
})); }));
return { teams }; return { teams };

View File

@@ -1,145 +1,25 @@
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
import type { DriverProfileDriverSummaryDTO } from '@/lib/types/generated/DriverProfileDriverSummaryDTO';
import type { DriverProfileStatsDTO } from '@/lib/types/generated/DriverProfileStatsDTO';
import type { DriverProfileFinishDistributionDTO } from '@/lib/types/generated/DriverProfileFinishDistributionDTO';
import type { DriverProfileTeamMembershipDTO } from '@/lib/types/generated/DriverProfileTeamMembershipDTO';
import type { DriverProfileSocialSummaryDTO } from '@/lib/types/generated/DriverProfileSocialSummaryDTO';
import type { DriverProfileExtendedProfileDTO } from '@/lib/types/generated/DriverProfileExtendedProfileDTO';
import { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel'; import { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel';
import type { import { ProfileViewData } from '@/lib/view-data/ProfileViewData';
DriverProfileDriverSummaryViewModel, import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder";
DriverProfileStatsViewModel,
DriverProfileFinishDistributionViewModel,
DriverProfileTeamMembershipViewModel,
DriverProfileSocialSummaryViewModel,
DriverProfileExtendedProfileViewModel,
} from '@/lib/view-models/DriverProfileViewModel';
/** /**
* DriverProfileViewModelBuilder * DriverProfileViewModelBuilder
* *
* Transforms GetDriverProfileOutputDTO into DriverProfileViewModel. * Transforms ProfileViewData into DriverProfileViewModel.
* Deterministic, side-effect free, no HTTP calls. * Deterministic, side-effect free, no HTTP calls.
*/ */
import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder";
export class DriverProfileViewModelBuilder implements ViewModelBuilder<any, any> { export class DriverProfileViewModelBuilder implements ViewModelBuilder<any, any> {
build(input: any): any { build(input: any): any {
return DriverProfileViewModelBuilder.build(input); return DriverProfileViewModelBuilder.build(input);
} }
static build(
/** /**
* Build ViewModel from API DTO * Build ViewModel from ViewData
* *
* @param apiDto - The API transport DTO * @param viewData - The template-ready ViewData
* @returns ViewModel ready for template * @returns ViewModel ready for client-side state
*/ */
static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewModel { static build(viewData: ProfileViewData): DriverProfileViewModel {
return new DriverProfileViewModel({ return new DriverProfileViewModel(viewData);
currentDriver: apiDto.currentDriver ? this.transformCurrentDriver(apiDto.currentDriver) : null,
stats: apiDto.stats ? this.transformStats(apiDto.stats) : null,
finishDistribution: apiDto.finishDistribution ? this.transformFinishDistribution(apiDto.finishDistribution) : null,
teamMemberships: apiDto.teamMemberships.map(m => this.transformTeamMembership(m)),
socialSummary: this.transformSocialSummary(apiDto.socialSummary),
extendedProfile: apiDto.extendedProfile ? this.transformExtendedProfile(apiDto.extendedProfile) : null,
});
}
private static transformCurrentDriver(dto: DriverProfileDriverSummaryDTO): DriverProfileDriverSummaryViewModel {
return {
id: dto.id,
name: dto.name,
country: dto.country,
avatarUrl: dto.avatarUrl || '', // Handle undefined
iracingId: dto.iracingId || null,
joinedAt: dto.joinedAt,
rating: dto.rating ?? null,
globalRank: dto.globalRank ?? null,
consistency: dto.consistency ?? null,
bio: dto.bio || null,
totalDrivers: dto.totalDrivers ?? null,
};
}
private static transformStats(dto: DriverProfileStatsDTO): DriverProfileStatsViewModel {
return {
totalRaces: dto.totalRaces,
wins: dto.wins,
podiums: dto.podiums,
dnfs: dto.dnfs,
avgFinish: dto.avgFinish ?? null,
bestFinish: dto.bestFinish ?? null,
worstFinish: dto.worstFinish ?? null,
finishRate: dto.finishRate ?? null,
winRate: dto.winRate ?? null,
podiumRate: dto.podiumRate ?? null,
percentile: dto.percentile ?? null,
rating: dto.rating ?? null,
consistency: dto.consistency ?? null,
overallRank: dto.overallRank ?? null,
};
}
private static transformFinishDistribution(dto: DriverProfileFinishDistributionDTO): DriverProfileFinishDistributionViewModel {
return {
totalRaces: dto.totalRaces,
wins: dto.wins,
podiums: dto.podiums,
topTen: dto.topTen,
dnfs: dto.dnfs,
other: dto.other,
};
}
private static transformTeamMembership(dto: DriverProfileTeamMembershipDTO): DriverProfileTeamMembershipViewModel {
return {
teamId: dto.teamId,
teamName: dto.teamName,
teamTag: dto.teamTag || null,
role: dto.role,
joinedAt: dto.joinedAt,
isCurrent: dto.isCurrent,
};
}
private static transformSocialSummary(dto: DriverProfileSocialSummaryDTO): DriverProfileSocialSummaryViewModel {
return {
friendsCount: dto.friendsCount,
friends: dto.friends.map(f => ({
id: f.id,
name: f.name,
country: f.country,
avatarUrl: f.avatarUrl || '', // Handle undefined
})),
};
}
private static transformExtendedProfile(dto: DriverProfileExtendedProfileDTO): DriverProfileExtendedProfileViewModel {
return {
socialHandles: dto.socialHandles.map(h => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
platform: h.platform as any, // Type assertion - assuming valid platform
handle: h.handle,
url: h.url,
})),
achievements: dto.achievements.map(a => ({
id: a.id,
title: a.title,
description: a.description,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
icon: a.icon as any, // Type assertion - assuming valid icon
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rarity: a.rarity as any, // Type assertion - assuming valid rarity
earnedAt: a.earnedAt,
})),
racingStyle: dto.racingStyle,
favoriteTrack: dto.favoriteTrack,
favoriteCar: dto.favoriteCar,
timezone: dto.timezone,
availableHours: dto.availableHours,
lookingForTeam: dto.lookingForTeam,
openToRequests: dto.openToRequests,
};
} }
} }

View File

@@ -1,5 +1,5 @@
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
import { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel'; import { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
import { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
/** /**
* DriversViewModelBuilder * DriversViewModelBuilder
@@ -14,10 +14,7 @@ export class DriversViewModelBuilder implements ViewModelBuilder<any, any> {
return DriversViewModelBuilder.build(input); return DriversViewModelBuilder.build(input);
} }
static build( static build(viewData: LeaderboardsViewData): DriverLeaderboardViewModel {
static build(apiDto: DriversLeaderboardDTO): DriverLeaderboardViewModel { return new DriverLeaderboardViewModel(viewData);
return new DriverLeaderboardViewModel({
drivers: apiDto.drivers,
});
} }
} }

View File

@@ -9,24 +9,6 @@ export class LeagueSummaryViewModelBuilder implements ViewModelBuilder<any, any>
} }
static build(league: LeaguesViewData['leagues'][number]): LeagueSummaryViewModel { static build(league: LeaguesViewData['leagues'][number]): LeagueSummaryViewModel {
return { return new LeagueSummaryViewModel(league as any);
id: league.id,
name: league.name,
description: league.description ?? '',
logoUrl: league.logoUrl,
ownerId: league.ownerId,
createdAt: league.createdAt,
maxDrivers: league.maxDrivers,
usedDriverSlots: league.usedDriverSlots,
maxTeams: league.maxTeams ?? 0,
usedTeamSlots: league.usedTeamSlots ?? 0,
structureSummary: league.structureSummary,
timingSummary: league.timingSummary,
category: league.category ?? undefined,
scoring: league.scoring ? {
...league.scoring,
primaryChampionshipType: league.scoring.primaryChampionshipType as 'driver' | 'team' | 'nations' | 'trophy',
} : undefined,
};
} }
} }

View File

@@ -16,12 +16,11 @@ export class OnboardingViewModelBuilder implements ViewModelBuilder<any, any> {
return OnboardingViewModelBuilder.build(input); return OnboardingViewModelBuilder.build(input);
} }
static build(
static build(apiDto: { isAlreadyOnboarded: boolean }): Result<OnboardingViewModel, DomainError> { static build(apiDto: { isAlreadyOnboarded: boolean }): Result<OnboardingViewModel, DomainError> {
try { try {
return Result.ok({ return Result.ok(new OnboardingViewModel({
isAlreadyOnboarded: apiDto.isAlreadyOnboarded || false, isAlreadyOnboarded: apiDto.isAlreadyOnboarded || false,
}); }));
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to build ViewModel'; const errorMessage = error instanceof Error ? error.message : 'Failed to build ViewModel';
return Result.err({ type: 'unknown', message: errorMessage }); return Result.err({ type: 'unknown', message: errorMessage });

View File

@@ -12,17 +12,22 @@
* - Must be named *ViewDataBuilder * - Must be named *ViewDataBuilder
* - Must have 'use client' directive * - Must have 'use client' directive
* - Must implement static build() method * - Must implement static build() method
* - Must use 'satisfies' for static type enforcement
*/ */
import { JsonValue } from '../types/primitives';
import { ViewData } from '../view-data/ViewData'; import { ViewData } from '../view-data/ViewData';
export interface ViewDataBuilder<TDTO extends JsonValue, TViewData extends ViewData> { /**
/** * ViewData Builder Contract (Static)
* Transform DTO into ViewData *
* * TDTO is constrained to object to ensure it is a serializable API DTO.
* @param dto - API Transport DTO (JSON-serializable) *
* @returns ViewData for template * Usage:
*/ * export class MyViewDataBuilder {
build(dto: TDTO): TViewData; * static build(apiDto: MyDTO): MyViewData { ... }
* }
* MyViewDataBuilder satisfies ViewDataBuilder<MyDTO, MyViewData>;
*/
export interface ViewDataBuilder<TDTO extends object, TViewData extends ViewData> {
build(apiDto: TDTO): TViewData;
} }

View File

@@ -12,17 +12,21 @@
* - Must be named *ViewModelBuilder * - Must be named *ViewModelBuilder
* - Must have 'use client' directive * - Must have 'use client' directive
* - Must implement static build() method * - Must implement static build() method
* - Must use 'satisfies' for static type enforcement
*/ */
import { ViewData } from '../view-data/ViewData'; import { ViewData } from '../view-data/ViewData';
import { ViewModel } from '../view-models/ViewModel'; import { ViewModel } from '../view-models/ViewModel';
/**
* ViewModel Builder Contract (Static)
*
* Usage:
* export class MyViewModelBuilder {
* static build(viewData: MyViewData): MyViewModel { ... }
* }
* MyViewModelBuilder satisfies ViewModelBuilder<MyViewData, MyViewModel>;
*/
export interface ViewModelBuilder<TViewData extends ViewData, TViewModel extends ViewModel> { export interface ViewModelBuilder<TViewData extends ViewData, TViewModel extends ViewModel> {
/**
* Transform ViewData into ViewModel
*
* @param viewData - ViewData (JSON-serializable template-ready data)
* @returns ViewModel
*/
build(viewData: TViewData): TViewModel; build(viewData: TViewData): TViewModel;
} }

View File

@@ -1,3 +1,5 @@
import { JsonValue } from "../types/primitives";
/** /**
* Base interface for ViewData objects * Base interface for ViewData objects
* *
@@ -8,9 +10,8 @@
* architectural rule is that these must be plain JSON objects. * architectural rule is that these must be plain JSON objects.
*/ */
export interface ViewData { export interface ViewData {
[key: string]: any; [key: string]: JsonValue;
} }
/** /**
* Helper type to ensure a type is ViewData-compatible * Helper type to ensure a type is ViewData-compatible
* *

View File

@@ -29,9 +29,9 @@
export abstract class ViewModel { export abstract class ViewModel {
/** /**
* Optional: Validate the ViewModel state * Optional: Validate the ViewModel state
* *
* Can be used to ensure the ViewModel is in a valid state * Can be used to ensure the ViewModel is in a valid state
* before a Presenter converts it to ViewData. * before it is used by the UI.
*/ */
validate?(): boolean; validate?(): boolean;
} }

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34 * Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */

Some files were not shown because too many files have changed in this diff Show More