Compare commits
8 Commits
cfc30c79a8
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
| 844092eb8c | |||
| e04282d77e | |||
| 9894c4a841 | |||
| 9b31eaf728 | |||
| 09632d004d | |||
| f2bd80ccd3 | |||
| 3a4f460a7d | |||
| 9ac74f5046 |
@@ -61,9 +61,7 @@ export class MediaResolverAdapter implements MediaResolverPort {
|
|||||||
basePath: config.defaultPath
|
basePath: config.defaultPath
|
||||||
});
|
});
|
||||||
|
|
||||||
this.generatedResolver = new GeneratedMediaResolverAdapter({
|
this.generatedResolver = new GeneratedMediaResolverAdapter();
|
||||||
basePath: config.generatedPath
|
|
||||||
});
|
|
||||||
|
|
||||||
this.uploadedResolver = new UploadedMediaResolverAdapter({
|
this.uploadedResolver = new UploadedMediaResolverAdapter({
|
||||||
basePath: config.uploadedPath
|
basePath: config.uploadedPath
|
||||||
|
|||||||
@@ -4,36 +4,33 @@
|
|||||||
|
|
||||||
import type { WalletRepository, TransactionRepository } from '@core/payments/domain/repositories/WalletRepository';
|
import type { WalletRepository, TransactionRepository } from '@core/payments/domain/repositories/WalletRepository';
|
||||||
import type { Logger } from '@core/shared/domain/Logger';
|
import type { Logger } from '@core/shared/domain/Logger';
|
||||||
import type { LeagueWalletRepository } from '@core/racing/domain/repositories/LeagueWalletRepository';
|
import type { Wallet, Transaction } from '@core/payments/domain/entities/Wallet';
|
||||||
import type { Wallet } from '@core/payments/domain/entities/Wallet';
|
|
||||||
import type { LeagueWallet } from '@core/racing/domain/entities/league-wallet/LeagueWallet';
|
|
||||||
import type { Transaction } from '@core/payments/domain/entities/league-wallet/Transaction';
|
|
||||||
|
|
||||||
const wallets: Map<string, Wallet | LeagueWallet> = new Map();
|
const wallets: Map<string, Wallet> = new Map();
|
||||||
const transactions: Map<string, Transaction> = new Map();
|
const transactions: Map<string, Transaction> = new Map();
|
||||||
|
|
||||||
export class InMemoryWalletRepository implements WalletRepository, LeagueWalletRepository {
|
export class InMemoryWalletRepository implements WalletRepository {
|
||||||
constructor(private readonly logger: Logger) {}
|
constructor(private readonly logger: Logger) {}
|
||||||
|
|
||||||
async findById(id: string): Promise<Wallet | LeagueWallet | null> {
|
async findById(id: string): Promise<Wallet | null> {
|
||||||
this.logger.debug('[InMemoryWalletRepository] findById', { id });
|
this.logger.debug('[InMemoryWalletRepository] findById', { id });
|
||||||
return wallets.get(id) || null;
|
return wallets.get(id) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByLeagueId(leagueId: string): Promise<LeagueWallet | null> {
|
async findByLeagueId(leagueId: string): Promise<Wallet | null> {
|
||||||
this.logger.debug('[InMemoryWalletRepository] findByLeagueId', { leagueId });
|
this.logger.debug('[InMemoryWalletRepository] findByLeagueId', { leagueId });
|
||||||
return (Array.from(wallets.values()).find(w => (w as LeagueWallet).leagueId.toString() === leagueId) as LeagueWallet) || null;
|
return Array.from(wallets.values()).find(w => w.leagueId === leagueId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(wallet: Wallet | LeagueWallet): Promise<Wallet | LeagueWallet> {
|
async create(wallet: Wallet): Promise<Wallet> {
|
||||||
this.logger.debug('[InMemoryWalletRepository] create', { wallet });
|
this.logger.debug('[InMemoryWalletRepository] create', { wallet });
|
||||||
wallets.set(wallet.id.toString(), wallet);
|
wallets.set(wallet.id, wallet);
|
||||||
return wallet;
|
return wallet;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(wallet: Wallet | LeagueWallet): Promise<Wallet | LeagueWallet> {
|
async update(wallet: Wallet): Promise<Wallet> {
|
||||||
this.logger.debug('[InMemoryWalletRepository] update', { wallet });
|
this.logger.debug('[InMemoryWalletRepository] update', { wallet });
|
||||||
wallets.set(wallet.id.toString(), wallet);
|
wallets.set(wallet.id, wallet);
|
||||||
return wallet;
|
return wallet;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,24 +50,24 @@ export class InMemoryWalletRepository implements WalletRepository, LeagueWalletR
|
|||||||
export class InMemoryTransactionRepository implements TransactionRepository {
|
export class InMemoryTransactionRepository implements TransactionRepository {
|
||||||
constructor(private readonly logger: Logger) {}
|
constructor(private readonly logger: Logger) {}
|
||||||
|
|
||||||
async findById(id: string): Promise<any | null> {
|
async findById(id: string): Promise<Transaction | null> {
|
||||||
this.logger.debug('[InMemoryTransactionRepository] findById', { id });
|
this.logger.debug('[InMemoryTransactionRepository] findById', { id });
|
||||||
return transactions.get(id) || null;
|
return transactions.get(id) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByWalletId(walletId: string): Promise<any[]> {
|
async findByWalletId(walletId: string): Promise<Transaction[]> {
|
||||||
this.logger.debug('[InMemoryTransactionRepository] findByWalletId', { walletId });
|
this.logger.debug('[InMemoryTransactionRepository] findByWalletId', { walletId });
|
||||||
return Array.from(transactions.values()).filter(t => t.walletId.toString() === walletId);
|
return Array.from(transactions.values()).filter(t => t.walletId === walletId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(transaction: any): Promise<any> {
|
async create(transaction: Transaction): Promise<Transaction> {
|
||||||
this.logger.debug('[InMemoryTransactionRepository] create', { transaction });
|
this.logger.debug('[InMemoryTransactionRepository] create', { transaction });
|
||||||
transactions.set(transaction.id.toString(), transaction);
|
transactions.set(transaction.id, transaction);
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(transaction: any): Promise<any> {
|
async update(transaction: Transaction): Promise<Transaction> {
|
||||||
transactions.set(transaction.id.toString(), transaction);
|
transactions.set(transaction.id, transaction);
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +79,7 @@ export class InMemoryTransactionRepository implements TransactionRepository {
|
|||||||
return transactions.has(id);
|
return transactions.has(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
findByType(type: any): Promise<any[]> {
|
findByType(type: any): Promise<Transaction[]> {
|
||||||
return Promise.resolve(Array.from(transactions.values()).filter(t => t.type === type));
|
return Promise.resolve(Array.from(transactions.values()).filter(t => t.type === type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ describe('ResultOrmMapper', () => {
|
|||||||
entity.fastestLap = 0;
|
entity.fastestLap = 0;
|
||||||
entity.incidents = 0;
|
entity.incidents = 0;
|
||||||
entity.startPosition = 1;
|
entity.startPosition = 1;
|
||||||
|
entity.points = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mapper.toDomain(entity);
|
mapper.toDomain(entity);
|
||||||
|
|||||||
@@ -2216,6 +2216,96 @@
|
|||||||
"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"
|
||||||
|
},
|
||||||
|
"userGrowth": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"roleDistribution": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"statusDistribution": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"suspended": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"deleted": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"activityTimeline": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"newUsers": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"logins": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"totalUsers",
|
||||||
|
"activeUsers",
|
||||||
|
"suspendedUsers",
|
||||||
|
"deletedUsers",
|
||||||
|
"systemAdmins",
|
||||||
|
"recentLogins",
|
||||||
|
"newUsersToday",
|
||||||
|
"userGrowth",
|
||||||
|
"label",
|
||||||
|
"value",
|
||||||
|
"color",
|
||||||
|
"roleDistribution",
|
||||||
|
"label",
|
||||||
|
"value",
|
||||||
|
"color",
|
||||||
|
"statusDistribution",
|
||||||
|
"active",
|
||||||
|
"suspended",
|
||||||
|
"deleted",
|
||||||
|
"activityTimeline",
|
||||||
|
"date",
|
||||||
|
"newUsers",
|
||||||
|
"logins"
|
||||||
|
]
|
||||||
|
},
|
||||||
"DeleteMediaOutputDTO": {
|
"DeleteMediaOutputDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function getEnvironment(): string {
|
|||||||
function validateEnvironment(
|
function validateEnvironment(
|
||||||
env: string
|
env: string
|
||||||
): env is keyof FeatureFlagConfig {
|
): env is keyof FeatureFlagConfig {
|
||||||
const validEnvs = ['development', 'test', 'staging', 'production'];
|
const validEnvs = ['development', 'test', 'e2e', 'staging', 'production'];
|
||||||
if (!validEnvs.includes(env)) {
|
if (!validEnvs.includes(env)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid environment: "${env}". Valid environments: ${validEnvs.join(', ')}`
|
`Invalid environment: "${env}". Valid environments: ${validEnvs.join(', ')}`
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface EnvironmentConfig {
|
|||||||
export interface FeatureFlagConfig {
|
export interface FeatureFlagConfig {
|
||||||
development: EnvironmentConfig;
|
development: EnvironmentConfig;
|
||||||
test: EnvironmentConfig;
|
test: EnvironmentConfig;
|
||||||
|
e2e: EnvironmentConfig;
|
||||||
staging: EnvironmentConfig;
|
staging: EnvironmentConfig;
|
||||||
production: EnvironmentConfig;
|
production: EnvironmentConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,43 @@ export const featureConfig: FeatureFlagConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// E2E environment - same as test
|
||||||
|
e2e: {
|
||||||
|
platform: {
|
||||||
|
dashboard: 'enabled',
|
||||||
|
leagues: 'enabled',
|
||||||
|
teams: 'enabled',
|
||||||
|
drivers: 'enabled',
|
||||||
|
races: 'enabled',
|
||||||
|
leaderboards: 'enabled',
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
signup: 'enabled',
|
||||||
|
login: 'enabled',
|
||||||
|
forgotPassword: 'enabled',
|
||||||
|
resetPassword: 'enabled',
|
||||||
|
},
|
||||||
|
onboarding: {
|
||||||
|
wizard: 'enabled',
|
||||||
|
},
|
||||||
|
sponsors: {
|
||||||
|
portal: 'enabled',
|
||||||
|
dashboard: 'enabled',
|
||||||
|
management: 'enabled',
|
||||||
|
campaigns: 'enabled',
|
||||||
|
billing: 'enabled',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
dashboard: 'enabled',
|
||||||
|
userManagement: 'enabled',
|
||||||
|
analytics: 'enabled',
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
newUI: 'disabled',
|
||||||
|
experimental: 'disabled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Staging environment - controlled feature rollout
|
// Staging environment - controlled feature rollout
|
||||||
staging: {
|
staging: {
|
||||||
// Core platform features
|
// Core platform features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { AdminController } from './AdminController';
|
import { AdminController } from './AdminController';
|
||||||
import { DashboardStatsResponseDto } from './dto/DashboardStatsResponseDto';
|
import { DashboardStatsResponseDto } from './dtos/DashboardStatsResponseDto';
|
||||||
import { ListUsersRequestDto } from './dtos/ListUsersRequestDto';
|
import { ListUsersRequestDto } from './dtos/ListUsersRequestDto';
|
||||||
import { UserListResponseDto, UserResponseDto } from './dtos/UserResponseDto';
|
import { UserListResponseDto, UserResponseDto } from './dtos/UserResponseDto';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RequireSystemAdmin, REQUIRE_SYSTEM_ADMIN_METADATA_KEY } from './Require
|
|||||||
|
|
||||||
// Mock SetMetadata
|
// Mock SetMetadata
|
||||||
vi.mock('@nestjs/common', () => ({
|
vi.mock('@nestjs/common', () => ({
|
||||||
SetMetadata: vi.fn(() => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
|
SetMetadata: vi.fn(() => (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('RequireSystemAdmin', () => {
|
describe('RequireSystemAdmin', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
import { AuthorizationService } from './AuthorizationService';
|
import { AuthorizationService } from './AuthorizationService';
|
||||||
|
|
||||||
describe('AuthorizationService', () => {
|
describe('AuthorizationService', () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Public, PUBLIC_ROUTE_METADATA_KEY } from './Public';
|
|||||||
|
|
||||||
// Mock SetMetadata
|
// Mock SetMetadata
|
||||||
vi.mock('@nestjs/common', () => ({
|
vi.mock('@nestjs/common', () => ({
|
||||||
SetMetadata: vi.fn(() => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
|
SetMetadata: vi.fn(() => (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Public', () => {
|
describe('Public', () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RequireAuthenticatedUser, REQUIRE_AUTHENTICATED_USER_METADATA_KEY } fro
|
|||||||
|
|
||||||
// Mock SetMetadata
|
// Mock SetMetadata
|
||||||
vi.mock('@nestjs/common', () => ({
|
vi.mock('@nestjs/common', () => ({
|
||||||
SetMetadata: vi.fn(() => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
|
SetMetadata: vi.fn(() => (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('RequireAuthenticatedUser', () => {
|
describe('RequireAuthenticatedUser', () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RequireRoles, REQUIRE_ROLES_METADATA_KEY } from './RequireRoles';
|
|||||||
|
|
||||||
// Mock SetMetadata
|
// Mock SetMetadata
|
||||||
vi.mock('@nestjs/common', () => ({
|
vi.mock('@nestjs/common', () => ({
|
||||||
SetMetadata: vi.fn(() => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
|
SetMetadata: vi.fn(() => (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('RequireRoles', () => {
|
describe('RequireRoles', () => {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const createOutput = (): DashboardOverviewResult => {
|
|||||||
fastestLap: 120,
|
fastestLap: 120,
|
||||||
incidents: 0,
|
incidents: 0,
|
||||||
startPosition: 1,
|
startPosition: 1,
|
||||||
|
points: 25,
|
||||||
});
|
});
|
||||||
|
|
||||||
const feedItem: FeedItem = {
|
const feedItem: FeedItem = {
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ describe('requireLeagueAdminOrOwner', () => {
|
|||||||
try {
|
try {
|
||||||
await requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase);
|
await requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase);
|
||||||
expect(true).toBe(false); // Should not reach here
|
expect(true).toBe(false); // Should not reach here
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
expect(error).toBeInstanceOf(ForbiddenException);
|
expect(error).toBeInstanceOf(ForbiddenException);
|
||||||
expect(error.message).toBe('Forbidden');
|
expect(error.message).toBe('Forbidden');
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ describe('requireLeagueAdminOrOwner', () => {
|
|||||||
mockGetActorFromRequestContext.mockReturnValue({
|
mockGetActorFromRequestContext.mockReturnValue({
|
||||||
userId: 'user-123',
|
userId: 'user-123',
|
||||||
driverId: 'driver-123',
|
driverId: 'driver-123',
|
||||||
role: null,
|
role: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockResult = {
|
const mockResult = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||||
import { LeagueController } from './LeagueController';
|
import { LeagueController } from './LeagueController';
|
||||||
import { LeagueService } from './LeagueService';
|
import { LeagueService } from './LeagueService';
|
||||||
|
|
||||||
@@ -25,9 +25,9 @@ describe('LeagueController - Discovery Endpoints', () => {
|
|||||||
name: 'GT3 Masters',
|
name: 'GT3 Masters',
|
||||||
description: 'A GT3 racing league',
|
description: 'A GT3 racing league',
|
||||||
ownerId: 'owner-1',
|
ownerId: 'owner-1',
|
||||||
maxDrivers: 32,
|
settings: { maxDrivers: 32 },
|
||||||
currentDrivers: 25,
|
usedSlots: 25,
|
||||||
isPublic: true,
|
createdAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
totalCount: 1,
|
totalCount: 1,
|
||||||
@@ -59,18 +59,18 @@ describe('LeagueController - Discovery Endpoints', () => {
|
|||||||
name: 'Small League',
|
name: 'Small League',
|
||||||
description: 'Small league',
|
description: 'Small league',
|
||||||
ownerId: 'owner-1',
|
ownerId: 'owner-1',
|
||||||
maxDrivers: 10,
|
settings: { maxDrivers: 10 },
|
||||||
currentDrivers: 8,
|
usedSlots: 8,
|
||||||
isPublic: true,
|
createdAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'league-2',
|
id: 'league-2',
|
||||||
name: 'Large League',
|
name: 'Large League',
|
||||||
description: 'Large league',
|
description: 'Large league',
|
||||||
ownerId: 'owner-2',
|
ownerId: 'owner-2',
|
||||||
maxDrivers: 50,
|
settings: { maxDrivers: 50 },
|
||||||
currentDrivers: 45,
|
usedSlots: 45,
|
||||||
isPublic: true,
|
createdAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
totalCount: 2,
|
totalCount: 2,
|
||||||
@@ -81,8 +81,8 @@ describe('LeagueController - Discovery Endpoints', () => {
|
|||||||
|
|
||||||
expect(result).toEqual(mockResult);
|
expect(result).toEqual(mockResult);
|
||||||
expect(result.leagues).toHaveLength(2);
|
expect(result.leagues).toHaveLength(2);
|
||||||
expect(result.leagues[0]?.maxDrivers).toBe(10);
|
expect(result.leagues[0]?.settings.maxDrivers).toBe(10);
|
||||||
expect(result.leagues[1]?.maxDrivers).toBe(50);
|
expect(result.leagues[1]?.settings.maxDrivers).toBe(50);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,13 +95,17 @@ describe('LeagueController - Discovery Endpoints', () => {
|
|||||||
name: 'GT3 Masters',
|
name: 'GT3 Masters',
|
||||||
description: 'A GT3 racing league',
|
description: 'A GT3 racing league',
|
||||||
ownerId: 'owner-1',
|
ownerId: 'owner-1',
|
||||||
maxDrivers: 32,
|
settings: { maxDrivers: 32 },
|
||||||
currentDrivers: 25,
|
usedSlots: 25,
|
||||||
isPublic: true,
|
createdAt: new Date().toISOString(),
|
||||||
scoringConfig: {
|
scoring: {
|
||||||
pointsSystem: 'standard',
|
gameId: 'iracing',
|
||||||
pointsPerRace: 25,
|
gameName: 'iRacing',
|
||||||
bonusPoints: true,
|
primaryChampionshipType: 'driver',
|
||||||
|
scoringPresetId: 'standard',
|
||||||
|
scoringPresetName: 'Standard',
|
||||||
|
dropPolicySummary: 'None',
|
||||||
|
scoringPatternSummary: '25-18-15...',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -134,13 +138,17 @@ describe('LeagueController - Discovery Endpoints', () => {
|
|||||||
name: 'Standard League',
|
name: 'Standard League',
|
||||||
description: 'Standard scoring',
|
description: 'Standard scoring',
|
||||||
ownerId: 'owner-1',
|
ownerId: 'owner-1',
|
||||||
maxDrivers: 32,
|
settings: { maxDrivers: 32 },
|
||||||
currentDrivers: 20,
|
usedSlots: 20,
|
||||||
isPublic: true,
|
createdAt: new Date().toISOString(),
|
||||||
scoringConfig: {
|
scoring: {
|
||||||
pointsSystem: 'standard',
|
gameId: 'iracing',
|
||||||
pointsPerRace: 25,
|
gameName: 'iRacing',
|
||||||
bonusPoints: true,
|
primaryChampionshipType: 'driver',
|
||||||
|
scoringPresetId: 'standard',
|
||||||
|
scoringPresetName: 'Standard',
|
||||||
|
dropPolicySummary: 'None',
|
||||||
|
scoringPatternSummary: '25-18-15...',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -148,13 +156,17 @@ describe('LeagueController - Discovery Endpoints', () => {
|
|||||||
name: 'Custom League',
|
name: 'Custom League',
|
||||||
description: 'Custom scoring',
|
description: 'Custom scoring',
|
||||||
ownerId: 'owner-2',
|
ownerId: 'owner-2',
|
||||||
maxDrivers: 20,
|
settings: { maxDrivers: 20 },
|
||||||
currentDrivers: 15,
|
usedSlots: 15,
|
||||||
isPublic: true,
|
createdAt: new Date().toISOString(),
|
||||||
scoringConfig: {
|
scoring: {
|
||||||
pointsSystem: 'custom',
|
gameId: 'iracing',
|
||||||
pointsPerRace: 50,
|
gameName: 'iRacing',
|
||||||
bonusPoints: false,
|
primaryChampionshipType: 'driver',
|
||||||
|
scoringPresetId: 'custom',
|
||||||
|
scoringPresetName: 'Custom',
|
||||||
|
dropPolicySummary: 'None',
|
||||||
|
scoringPatternSummary: '50-40-30...',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -166,8 +178,8 @@ describe('LeagueController - Discovery Endpoints', () => {
|
|||||||
|
|
||||||
expect(result).toEqual(mockResult);
|
expect(result).toEqual(mockResult);
|
||||||
expect(result.leagues).toHaveLength(2);
|
expect(result.leagues).toHaveLength(2);
|
||||||
expect(result.leagues[0]?.scoringConfig.pointsSystem).toBe('standard');
|
expect(result.leagues[0]?.scoring?.scoringPresetId).toBe('standard');
|
||||||
expect(result.leagues[1]?.scoringConfig.pointsSystem).toBe('custom');
|
expect(result.leagues[1]?.scoring?.scoringPresetId).toBe('custom');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,7 @@
|
|||||||
import { requestContextMiddleware } from '@adapters/http/RequestContext';
|
|
||||||
import { Result } from '@core/shared/domain/Result';
|
import { Result } from '@core/shared/domain/Result';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { LeagueService } from './LeagueService';
|
import { LeagueService } from './LeagueService';
|
||||||
|
|
||||||
async function withUserId<T>(userId: string, fn: () => Promise<T>): Promise<T> {
|
|
||||||
const req = { user: { userId } };
|
|
||||||
const res = {};
|
|
||||||
|
|
||||||
return await new Promise<T>((resolve, reject) => {
|
|
||||||
requestContextMiddleware(req as never, res as never, () => {
|
|
||||||
fn().then(resolve, reject);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('LeagueService - All Endpoints', () => {
|
describe('LeagueService - All Endpoints', () => {
|
||||||
it('covers all league endpoint happy paths and error branches', async () => {
|
it('covers all league endpoint happy paths and error branches', async () => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { NotificationsController } from './NotificationsController';
|
import { NotificationsController } from './NotificationsController';
|
||||||
import { NotificationsService } from './NotificationsService';
|
import { NotificationsService } from './NotificationsService';
|
||||||
import { vi } from 'vitest';
|
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
||||||
import type { Request, Response } from 'express';
|
import type { Response } from 'express';
|
||||||
|
|
||||||
describe('NotificationsController', () => {
|
describe('NotificationsController', () => {
|
||||||
let controller: NotificationsController;
|
let controller: NotificationsController;
|
||||||
@@ -38,7 +38,7 @@ describe('NotificationsController', () => {
|
|||||||
|
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user-123' },
|
user: { userId: 'user-123' },
|
||||||
} as unknown as Request;
|
} as any;
|
||||||
|
|
||||||
const mockRes = {
|
const mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
@@ -53,7 +53,7 @@ describe('NotificationsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 when user is not authenticated', async () => {
|
it('should return 401 when user is not authenticated', async () => {
|
||||||
const mockReq = {} as unknown as Request;
|
const mockReq = {} as any;
|
||||||
const mockRes = {
|
const mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
json: vi.fn(),
|
json: vi.fn(),
|
||||||
@@ -69,7 +69,7 @@ describe('NotificationsController', () => {
|
|||||||
it('should return 401 when userId is missing', async () => {
|
it('should return 401 when userId is missing', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: {},
|
user: {},
|
||||||
} as unknown as Request;
|
} as any;
|
||||||
|
|
||||||
const mockRes = {
|
const mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
@@ -90,7 +90,7 @@ describe('NotificationsController', () => {
|
|||||||
|
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user-123' },
|
user: { userId: 'user-123' },
|
||||||
} as unknown as Request;
|
} as any;
|
||||||
|
|
||||||
const mockRes = {
|
const mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
@@ -105,7 +105,7 @@ describe('NotificationsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 when user is not authenticated', async () => {
|
it('should return 401 when user is not authenticated', async () => {
|
||||||
const mockReq = {} as unknown as Request;
|
const mockReq = {} as any;
|
||||||
const mockRes = {
|
const mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
json: vi.fn(),
|
json: vi.fn(),
|
||||||
@@ -121,7 +121,7 @@ describe('NotificationsController', () => {
|
|||||||
it('should return 401 when userId is missing', async () => {
|
it('should return 401 when userId is missing', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: {},
|
user: {},
|
||||||
} as unknown as Request;
|
} as any;
|
||||||
|
|
||||||
const mockRes = {
|
const mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
@@ -148,7 +148,7 @@ describe('NotificationsController', () => {
|
|||||||
|
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user-123' },
|
user: { userId: 'user-123' },
|
||||||
} as unknown as Request;
|
} as any;
|
||||||
|
|
||||||
const mockRes = {
|
const mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
@@ -163,7 +163,7 @@ describe('NotificationsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 when user is not authenticated', async () => {
|
it('should return 401 when user is not authenticated', async () => {
|
||||||
const mockReq = {} as unknown as Request;
|
const mockReq = {} as any;
|
||||||
const mockRes = {
|
const mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
json: vi.fn(),
|
json: vi.fn(),
|
||||||
@@ -179,7 +179,7 @@ describe('NotificationsController', () => {
|
|||||||
it('should return 401 when userId is missing', async () => {
|
it('should return 401 when userId is missing', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: {},
|
user: {},
|
||||||
} as unknown as Request;
|
} as any;
|
||||||
|
|
||||||
const mockRes = {
|
const mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
@@ -198,7 +198,7 @@ describe('NotificationsController', () => {
|
|||||||
|
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user-123' },
|
user: { userId: 'user-123' },
|
||||||
} as unknown as Request;
|
} as any;
|
||||||
|
|
||||||
const mockRes = {
|
const mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { describe, expect, it, beforeEach } from 'vitest';
|
||||||
import { CreatePaymentPresenter } from './CreatePaymentPresenter';
|
import { CreatePaymentPresenter } from './CreatePaymentPresenter';
|
||||||
import { CreatePaymentOutput } from '../dtos/PaymentsDto';
|
import { PaymentType, PayerType, PaymentStatus } from '@core/payments/domain/entities/Payment';
|
||||||
|
|
||||||
describe('CreatePaymentPresenter', () => {
|
describe('CreatePaymentPresenter', () => {
|
||||||
let presenter: CreatePaymentPresenter;
|
let presenter: CreatePaymentPresenter;
|
||||||
@@ -13,14 +14,14 @@ describe('CreatePaymentPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -31,14 +32,14 @@ describe('CreatePaymentPresenter', () => {
|
|||||||
expect(responseModel).toEqual({
|
expect(responseModel).toEqual({
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -48,15 +49,15 @@ describe('CreatePaymentPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
seasonId: 'season-123',
|
seasonId: 'season-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -71,14 +72,14 @@ describe('CreatePaymentPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
completedAt: new Date('2024-01-02'),
|
completedAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
@@ -94,14 +95,14 @@ describe('CreatePaymentPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -116,14 +117,14 @@ describe('CreatePaymentPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -144,14 +145,14 @@ describe('CreatePaymentPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -169,14 +170,14 @@ describe('CreatePaymentPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -191,14 +192,14 @@ describe('CreatePaymentPresenter', () => {
|
|||||||
const firstResult = {
|
const firstResult = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -206,14 +207,14 @@ describe('CreatePaymentPresenter', () => {
|
|||||||
const secondResult = {
|
const secondResult = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-456',
|
id: 'payment-456',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 200,
|
amount: 200,
|
||||||
platformFee: 10,
|
platformFee: 10,
|
||||||
netAmount: 190,
|
netAmount: 190,
|
||||||
payerId: 'user-456',
|
payerId: 'user-456',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-456',
|
leagueId: 'league-456',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-02'),
|
createdAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { GetMembershipFeesPresenter } from './GetMembershipFeesPresenter';
|
import { GetMembershipFeesPresenter } from './GetMembershipFeesPresenter';
|
||||||
import { GetMembershipFeesResultDTO } from '../dtos/GetMembershipFeesDTO';
|
import { GetMembershipFeesResultDTO } from '../dtos/GetMembershipFeesDTO';
|
||||||
import { MembershipFeeType, MemberPaymentStatus } from '../dtos/PaymentsDto';
|
import { MembershipFeeType } from '../dtos/PaymentsDto';
|
||||||
|
|
||||||
describe('GetMembershipFeesPresenter', () => {
|
describe('GetMembershipFeesPresenter', () => {
|
||||||
let presenter: GetMembershipFeesPresenter;
|
let presenter: GetMembershipFeesPresenter;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { describe, expect, it, beforeEach } from 'vitest';
|
||||||
import { GetPaymentsPresenter } from './GetPaymentsPresenter';
|
import { GetPaymentsPresenter } from './GetPaymentsPresenter';
|
||||||
import { GetPaymentsOutput } from '../dtos/PaymentsDto';
|
import { PaymentType, PayerType, PaymentStatus } from '@core/payments/domain/entities/Payment';
|
||||||
|
|
||||||
describe('GetPaymentsPresenter', () => {
|
describe('GetPaymentsPresenter', () => {
|
||||||
let presenter: GetPaymentsPresenter;
|
let presenter: GetPaymentsPresenter;
|
||||||
@@ -14,14 +15,14 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -34,14 +35,14 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -53,15 +54,15 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
seasonId: 'season-123',
|
seasonId: 'season-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -70,7 +71,7 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
presenter.present(result);
|
presenter.present(result);
|
||||||
|
|
||||||
const responseModel = presenter.getResponseModel();
|
const responseModel = presenter.getResponseModel();
|
||||||
expect(responseModel.payments[0].seasonId).toBe('season-123');
|
expect(responseModel.payments[0]!.seasonId).toBe('season-123');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include completedAt when provided', () => {
|
it('should include completedAt when provided', () => {
|
||||||
@@ -78,14 +79,14 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
completedAt: new Date('2024-01-02'),
|
completedAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
@@ -95,7 +96,7 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
presenter.present(result);
|
presenter.present(result);
|
||||||
|
|
||||||
const responseModel = presenter.getResponseModel();
|
const responseModel = presenter.getResponseModel();
|
||||||
expect(responseModel.payments[0].completedAt).toEqual(new Date('2024-01-02'));
|
expect(responseModel.payments[0]!.completedAt).toEqual(new Date('2024-01-02'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not include seasonId when not provided', () => {
|
it('should not include seasonId when not provided', () => {
|
||||||
@@ -103,14 +104,14 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -119,7 +120,7 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
presenter.present(result);
|
presenter.present(result);
|
||||||
|
|
||||||
const responseModel = presenter.getResponseModel();
|
const responseModel = presenter.getResponseModel();
|
||||||
expect(responseModel.payments[0].seasonId).toBeUndefined();
|
expect(responseModel.payments[0]!.seasonId).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not include completedAt when not provided', () => {
|
it('should not include completedAt when not provided', () => {
|
||||||
@@ -127,14 +128,14 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -143,7 +144,7 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
presenter.present(result);
|
presenter.present(result);
|
||||||
|
|
||||||
const responseModel = presenter.getResponseModel();
|
const responseModel = presenter.getResponseModel();
|
||||||
expect(responseModel.payments[0].completedAt).toBeUndefined();
|
expect(responseModel.payments[0]!.completedAt).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty payments list', () => {
|
it('should handle empty payments list', () => {
|
||||||
@@ -162,26 +163,26 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'payment-456',
|
id: 'payment-456',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 200,
|
amount: 200,
|
||||||
platformFee: 10,
|
platformFee: 10,
|
||||||
netAmount: 190,
|
netAmount: 190,
|
||||||
payerId: 'user-456',
|
payerId: 'user-456',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-456',
|
leagueId: 'league-456',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-02'),
|
createdAt: new Date('2024-01-02'),
|
||||||
completedAt: new Date('2024-01-03'),
|
completedAt: new Date('2024-01-03'),
|
||||||
},
|
},
|
||||||
@@ -192,8 +193,8 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
|
|
||||||
const responseModel = presenter.getResponseModel();
|
const responseModel = presenter.getResponseModel();
|
||||||
expect(responseModel.payments).toHaveLength(2);
|
expect(responseModel.payments).toHaveLength(2);
|
||||||
expect(responseModel.payments[0].id).toBe('payment-123');
|
expect(responseModel.payments[0]!.id).toBe('payment-123');
|
||||||
expect(responseModel.payments[1].id).toBe('payment-456');
|
expect(responseModel.payments[1]!.id).toBe('payment-456');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -207,14 +208,14 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -224,7 +225,7 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
|
|
||||||
const responseModel = presenter.getResponseModel();
|
const responseModel = presenter.getResponseModel();
|
||||||
expect(responseModel).toBeDefined();
|
expect(responseModel).toBeDefined();
|
||||||
expect(responseModel.payments[0].id).toBe('payment-123');
|
expect(responseModel.payments[0]!.id).toBe('payment-123');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -234,14 +235,14 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -258,14 +259,14 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -275,14 +276,14 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
payments: [
|
payments: [
|
||||||
{
|
{
|
||||||
id: 'payment-456',
|
id: 'payment-456',
|
||||||
type: 'membership',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 200,
|
amount: 200,
|
||||||
platformFee: 10,
|
platformFee: 10,
|
||||||
netAmount: 190,
|
netAmount: 190,
|
||||||
payerId: 'user-456',
|
payerId: 'user-456',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-456',
|
leagueId: 'league-456',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-02'),
|
createdAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -293,7 +294,7 @@ describe('GetPaymentsPresenter', () => {
|
|||||||
presenter.present(secondResult);
|
presenter.present(secondResult);
|
||||||
|
|
||||||
const responseModel = presenter.getResponseModel();
|
const responseModel = presenter.getResponseModel();
|
||||||
expect(responseModel.payments[0].id).toBe('payment-456');
|
expect(responseModel.payments[0]!.id).toBe('payment-456');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ describe('GetPrizesPresenter', () => {
|
|||||||
type: PrizeType.CASH,
|
type: PrizeType.CASH,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
|
seasonId: 'season-123',
|
||||||
|
position: 1,
|
||||||
|
awarded: false,
|
||||||
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -39,6 +43,10 @@ describe('GetPrizesPresenter', () => {
|
|||||||
type: PrizeType.CASH,
|
type: PrizeType.CASH,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
|
seasonId: 'season-123',
|
||||||
|
position: 1,
|
||||||
|
awarded: false,
|
||||||
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -52,6 +60,10 @@ describe('GetPrizesPresenter', () => {
|
|||||||
type: PrizeType.MERCHANDISE,
|
type: PrizeType.MERCHANDISE,
|
||||||
amount: 200,
|
amount: 200,
|
||||||
leagueId: 'league-456',
|
leagueId: 'league-456',
|
||||||
|
seasonId: 'season-456',
|
||||||
|
position: 2,
|
||||||
|
awarded: false,
|
||||||
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -78,6 +90,10 @@ describe('GetPrizesPresenter', () => {
|
|||||||
type: PrizeType.CASH,
|
type: PrizeType.CASH,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
|
seasonId: 'season-123',
|
||||||
|
position: 1,
|
||||||
|
awarded: false,
|
||||||
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -99,6 +115,10 @@ describe('GetPrizesPresenter', () => {
|
|||||||
type: PrizeType.CASH,
|
type: PrizeType.CASH,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
|
seasonId: 'season-123',
|
||||||
|
position: 1,
|
||||||
|
awarded: false,
|
||||||
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -119,6 +139,10 @@ describe('GetPrizesPresenter', () => {
|
|||||||
type: PrizeType.CASH,
|
type: PrizeType.CASH,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
|
seasonId: 'season-123',
|
||||||
|
position: 1,
|
||||||
|
awarded: false,
|
||||||
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -132,6 +156,10 @@ describe('GetPrizesPresenter', () => {
|
|||||||
type: PrizeType.MERCHANDISE,
|
type: PrizeType.MERCHANDISE,
|
||||||
amount: 200,
|
amount: 200,
|
||||||
leagueId: 'league-456',
|
leagueId: 'league-456',
|
||||||
|
seasonId: 'season-456',
|
||||||
|
position: 2,
|
||||||
|
awarded: false,
|
||||||
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -155,6 +183,10 @@ describe('GetPrizesPresenter', () => {
|
|||||||
type: PrizeType.CASH,
|
type: PrizeType.CASH,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
|
seasonId: 'season-123',
|
||||||
|
position: 1,
|
||||||
|
awarded: false,
|
||||||
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -178,6 +210,10 @@ describe('GetPrizesPresenter', () => {
|
|||||||
type: PrizeType.CASH,
|
type: PrizeType.CASH,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
|
seasonId: 'season-123',
|
||||||
|
position: 1,
|
||||||
|
awarded: false,
|
||||||
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { describe, expect, it, beforeEach } from 'vitest';
|
||||||
import { UpdatePaymentStatusPresenter } from './UpdatePaymentStatusPresenter';
|
import { UpdatePaymentStatusPresenter } from './UpdatePaymentStatusPresenter';
|
||||||
import { UpdatePaymentStatusOutput } from '../dtos/PaymentsDto';
|
import { PaymentType, PayerType, PaymentStatus } from '@core/payments/domain/entities/Payment';
|
||||||
|
|
||||||
describe('UpdatePaymentStatusPresenter', () => {
|
describe('UpdatePaymentStatusPresenter', () => {
|
||||||
let presenter: UpdatePaymentStatusPresenter;
|
let presenter: UpdatePaymentStatusPresenter;
|
||||||
@@ -13,14 +14,14 @@ describe('UpdatePaymentStatusPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership_fee',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
completedAt: new Date('2024-01-02'),
|
completedAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
@@ -32,14 +33,14 @@ describe('UpdatePaymentStatusPresenter', () => {
|
|||||||
expect(responseModel).toEqual({
|
expect(responseModel).toEqual({
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership_fee',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
completedAt: new Date('2024-01-02'),
|
completedAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
@@ -50,15 +51,15 @@ describe('UpdatePaymentStatusPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership_fee',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
seasonId: 'season-123',
|
seasonId: 'season-123',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
completedAt: new Date('2024-01-02'),
|
completedAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
@@ -74,14 +75,14 @@ describe('UpdatePaymentStatusPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership_fee',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
completedAt: new Date('2024-01-02'),
|
completedAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
@@ -97,14 +98,14 @@ describe('UpdatePaymentStatusPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership_fee',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -119,14 +120,14 @@ describe('UpdatePaymentStatusPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership_fee',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'pending',
|
status: PaymentStatus.PENDING,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -147,14 +148,14 @@ describe('UpdatePaymentStatusPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership_fee',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
completedAt: new Date('2024-01-02'),
|
completedAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
@@ -173,14 +174,14 @@ describe('UpdatePaymentStatusPresenter', () => {
|
|||||||
const result = {
|
const result = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership_fee',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
completedAt: new Date('2024-01-02'),
|
completedAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
@@ -196,14 +197,14 @@ describe('UpdatePaymentStatusPresenter', () => {
|
|||||||
const firstResult = {
|
const firstResult = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-123',
|
id: 'payment-123',
|
||||||
type: 'membership_fee',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
platformFee: 5,
|
platformFee: 5,
|
||||||
netAmount: 95,
|
netAmount: 95,
|
||||||
payerId: 'user-123',
|
payerId: 'user-123',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-123',
|
leagueId: 'league-123',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-01'),
|
createdAt: new Date('2024-01-01'),
|
||||||
completedAt: new Date('2024-01-02'),
|
completedAt: new Date('2024-01-02'),
|
||||||
},
|
},
|
||||||
@@ -212,14 +213,14 @@ describe('UpdatePaymentStatusPresenter', () => {
|
|||||||
const secondResult = {
|
const secondResult = {
|
||||||
payment: {
|
payment: {
|
||||||
id: 'payment-456',
|
id: 'payment-456',
|
||||||
type: 'membership_fee',
|
type: PaymentType.MEMBERSHIP_FEE,
|
||||||
amount: 200,
|
amount: 200,
|
||||||
platformFee: 10,
|
platformFee: 10,
|
||||||
netAmount: 190,
|
netAmount: 190,
|
||||||
payerId: 'user-456',
|
payerId: 'user-456',
|
||||||
payerType: 'driver',
|
payerType: PayerType.DRIVER,
|
||||||
leagueId: 'league-456',
|
leagueId: 'league-456',
|
||||||
status: 'completed',
|
status: PaymentStatus.COMPLETED,
|
||||||
createdAt: new Date('2024-01-02'),
|
createdAt: new Date('2024-01-02'),
|
||||||
completedAt: new Date('2024-01-03'),
|
completedAt: new Date('2024-01-03'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
|
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
|
||||||
import type { DataSource } from 'typeorm';
|
import type { DataSource } from 'typeorm';
|
||||||
|
|
||||||
import { AdminUserOrmEntity } from '@core/admin/infrastructure/typeorm/entities/AdminUserOrmEntity';
|
import { AdminUserOrmEntity } from '@adapters/admin/persistence/typeorm/entities/AdminUserOrmEntity';
|
||||||
import { AdminUserOrmMapper } from '@core/admin/infrastructure/typeorm/mappers/AdminUserOrmMapper';
|
import { AdminUserOrmMapper } from '@adapters/admin/persistence/typeorm/mappers/AdminUserOrmMapper';
|
||||||
import { TypeOrmAdminUserRepository } from '@core/admin/infrastructure/typeorm/repositories/TypeOrmAdminUserRepository';
|
import { TypeOrmAdminUserRepository } from '@adapters/admin/persistence/typeorm/repositories/TypeOrmAdminUserRepository';
|
||||||
|
|
||||||
import { ADMIN_USER_REPOSITORY_TOKEN } from '../admin/AdminPersistenceTokens';
|
import { ADMIN_USER_REPOSITORY_TOKEN } from '../admin/AdminPersistenceTokens';
|
||||||
|
|
||||||
|
|||||||
363
apps/api/src/shared/testing/contractValidation.test.ts
Normal file
363
apps/api/src/shared/testing/contractValidation.test.ts
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
/**
|
||||||
|
* API Contract Validation Tests
|
||||||
|
*
|
||||||
|
* Validates that API DTOs are consistent and generate valid OpenAPI specs.
|
||||||
|
* This test suite ensures contract compatibility between API and website.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeAll } from 'vitest';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// Import DTO classes to validate their structure
|
||||||
|
import { GetAnalyticsMetricsOutputDTO } from '../../domain/analytics/dtos/GetAnalyticsMetricsOutputDTO';
|
||||||
|
import { GetDashboardDataOutputDTO } from '../../domain/analytics/dtos/GetDashboardDataOutputDTO';
|
||||||
|
import { RecordEngagementInputDTO } from '../../domain/analytics/dtos/RecordEngagementInputDTO';
|
||||||
|
import { RecordEngagementOutputDTO } from '../../domain/analytics/dtos/RecordEngagementOutputDTO';
|
||||||
|
import { RecordPageViewInputDTO } from '../../domain/analytics/dtos/RecordPageViewInputDTO';
|
||||||
|
import { RecordPageViewOutputDTO } from '../../domain/analytics/dtos/RecordPageViewOutputDTO';
|
||||||
|
import { RequestAvatarGenerationInputDTO } from '../../domain/media/dtos/RequestAvatarGenerationInputDTO';
|
||||||
|
import { RequestAvatarGenerationOutputDTO } from '../../domain/media/dtos/RequestAvatarGenerationOutputDTO';
|
||||||
|
import { UploadMediaInputDTO } from '../../domain/media/dtos/UploadMediaInputDTO';
|
||||||
|
import { UploadMediaOutputDTO } from '../../domain/media/dtos/UploadMediaOutputDTO';
|
||||||
|
import { ValidateFaceInputDTO } from '../../domain/media/dtos/ValidateFaceInputDTO';
|
||||||
|
import { ValidateFaceOutputDTO } from '../../domain/media/dtos/ValidateFaceOutputDTO';
|
||||||
|
import { RaceDTO } from '../../domain/race/dtos/RaceDTO';
|
||||||
|
import { RaceDetailDTO } from '../../domain/race/dtos/RaceDetailDTO';
|
||||||
|
import { RaceResultDTO } from '../../domain/race/dtos/RaceResultDTO';
|
||||||
|
import { SponsorDTO } from '../../domain/sponsor/dtos/SponsorDTO';
|
||||||
|
import { SponsorshipDTO } from '../../domain/sponsor/dtos/SponsorshipDTO';
|
||||||
|
import { TeamDTO } from '../../domain/team/dtos/TeamDto';
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
cyan: '\x1b[36m',
|
||||||
|
dim: '\x1b[2m'
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('API Contract Validation', () => {
|
||||||
|
let openApiSpec: any;
|
||||||
|
let specPath: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Load the OpenAPI spec
|
||||||
|
specPath = path.join(__dirname, '..', '..', '..', 'openapi.json');
|
||||||
|
const specContent = await fs.readFile(specPath, 'utf-8');
|
||||||
|
openApiSpec = JSON.parse(specContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OpenAPI Spec Integrity', () => {
|
||||||
|
it('should have valid OpenAPI structure', () => {
|
||||||
|
expect(openApiSpec).toBeDefined();
|
||||||
|
expect(openApiSpec.openapi).toBeDefined();
|
||||||
|
expect(openApiSpec.info).toBeDefined();
|
||||||
|
expect(openApiSpec.paths).toBeDefined();
|
||||||
|
expect(openApiSpec.components).toBeDefined();
|
||||||
|
expect(openApiSpec.components.schemas).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid OpenAPI version', () => {
|
||||||
|
expect(openApiSpec.openapi).toMatch(/^3\.\d+\.\d+$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have required API metadata', () => {
|
||||||
|
expect(openApiSpec.info.title).toBeDefined();
|
||||||
|
expect(openApiSpec.info.version).toBeDefined();
|
||||||
|
expect(openApiSpec.info.description).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have no circular references in schemas', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const visiting = new Set<string>();
|
||||||
|
|
||||||
|
const checkCircular = (schemaName: string, schema: any): boolean => {
|
||||||
|
if (!schema) return false;
|
||||||
|
if (visiting.has(schemaName)) {
|
||||||
|
return true; // Circular reference detected
|
||||||
|
}
|
||||||
|
if (visited.has(schemaName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
visiting.add(schemaName);
|
||||||
|
|
||||||
|
// Check $ref references
|
||||||
|
if (schema.$ref) {
|
||||||
|
const refName = schema.$ref.split('/').pop();
|
||||||
|
if (schemas[refName] && checkCircular(refName, schemas[refName])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check properties
|
||||||
|
if (schema.properties) {
|
||||||
|
for (const prop of Object.values(schema.properties)) {
|
||||||
|
if ((prop as any).$ref) {
|
||||||
|
const refName = (prop as any).$ref.split('/').pop();
|
||||||
|
if (schemas[refName] && checkCircular(refName, schemas[refName])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check array items
|
||||||
|
if (schema.items && schema.items.$ref) {
|
||||||
|
const refName = schema.items.$ref.split('/').pop();
|
||||||
|
if (schemas[refName] && checkCircular(refName, schemas[refName])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visiting.delete(schemaName);
|
||||||
|
visited.add(schemaName);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [schemaName, schema] of Object.entries(schemas)) {
|
||||||
|
if (checkCircular(schemaName, schema as any)) {
|
||||||
|
throw new Error(`Circular reference detected in schema: ${schemaName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have all required DTOs in OpenAPI spec', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
|
||||||
|
// List of critical DTOs that must exist in the spec
|
||||||
|
const requiredDTOs = [
|
||||||
|
'GetAnalyticsMetricsOutputDTO',
|
||||||
|
'GetDashboardDataOutputDTO',
|
||||||
|
'RecordEngagementInputDTO',
|
||||||
|
'RecordEngagementOutputDTO',
|
||||||
|
'RecordPageViewInputDTO',
|
||||||
|
'RecordPageViewOutputDTO',
|
||||||
|
'RequestAvatarGenerationInputDTO',
|
||||||
|
'RequestAvatarGenerationOutputDTO',
|
||||||
|
'UploadMediaInputDTO',
|
||||||
|
'UploadMediaOutputDTO',
|
||||||
|
'ValidateFaceInputDTO',
|
||||||
|
'ValidateFaceOutputDTO',
|
||||||
|
'RaceDTO',
|
||||||
|
'RaceDetailDTO',
|
||||||
|
'RaceResultDTO',
|
||||||
|
'SponsorDTO',
|
||||||
|
'SponsorshipDTO',
|
||||||
|
'TeamDTO'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dtoName of requiredDTOs) {
|
||||||
|
expect(schemas[dtoName], `DTO ${dtoName} should exist in OpenAPI spec`).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid JSON schema for all DTOs', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
|
||||||
|
for (const [schemaName, schema] of Object.entries(schemas)) {
|
||||||
|
expect(schema, `Schema ${schemaName} should be an object`).toBeInstanceOf(Object);
|
||||||
|
expect(schema.type, `Schema ${schemaName} should have a type`).toBeDefined();
|
||||||
|
|
||||||
|
if (schema.type === 'object') {
|
||||||
|
expect(schema.properties, `Schema ${schemaName} should have properties`).toBeDefined();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DTO Consistency', () => {
|
||||||
|
it('should have consistent DTO definitions between code and spec', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
|
||||||
|
// Test a sample of DTOs to ensure they match the spec
|
||||||
|
const testDTOs = [
|
||||||
|
{ name: 'GetAnalyticsMetricsOutputDTO', expectedProps: ['pageViews', 'uniqueVisitors', 'averageSessionDuration', 'bounceRate'] },
|
||||||
|
{ name: 'RaceDTO', expectedProps: ['id', 'name', 'date'] },
|
||||||
|
{ name: 'SponsorDTO', expectedProps: ['id', 'name'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { name, expectedProps } of testDTOs) {
|
||||||
|
const schema = schemas[name];
|
||||||
|
expect(schema, `Schema ${name} should exist`).toBeDefined();
|
||||||
|
|
||||||
|
if (schema.properties) {
|
||||||
|
for (const prop of expectedProps) {
|
||||||
|
expect(schema.properties[prop], `Property ${prop} should exist in ${name}`).toBeDefined();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have no duplicate DTO names', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
const schemaNames = Object.keys(schemas);
|
||||||
|
const uniqueNames = new Set(schemaNames);
|
||||||
|
|
||||||
|
expect(schemaNames.length).toBe(uniqueNames.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have consistent naming conventions', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
|
||||||
|
for (const schemaName of Object.keys(schemas)) {
|
||||||
|
// DTO names should end with DTO
|
||||||
|
expect(schemaName).toMatch(/DTO$/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Type Generation Integrity', () => {
|
||||||
|
it('should have all DTOs with proper type definitions', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
|
||||||
|
for (const [schemaName, schema] of Object.entries(schemas)) {
|
||||||
|
if (schema.type === 'object') {
|
||||||
|
expect(schema.properties, `Schema ${schemaName} should have properties`).toBeDefined();
|
||||||
|
|
||||||
|
// Check that all properties have types or are references
|
||||||
|
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
||||||
|
const prop = propSchema as any;
|
||||||
|
// Properties can have a type directly, or be a $ref to another schema
|
||||||
|
const hasType = prop.type !== undefined;
|
||||||
|
const isRef = prop.$ref !== undefined;
|
||||||
|
|
||||||
|
expect(hasType || isRef, `Property ${propName} in ${schemaName} should have a type or be a $ref`).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have required fields properly marked', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
|
||||||
|
// Test a few critical DTOs
|
||||||
|
const testDTOs = [
|
||||||
|
{ name: 'GetAnalyticsMetricsOutputDTO', required: ['pageViews', 'uniqueVisitors', 'averageSessionDuration', 'bounceRate'] },
|
||||||
|
{ name: 'RaceDTO', required: ['id', 'name', 'date'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { name, required } of testDTOs) {
|
||||||
|
const schema = schemas[name];
|
||||||
|
expect(schema.required, `Schema ${name} should have required fields`).toBeDefined();
|
||||||
|
|
||||||
|
for (const field of required) {
|
||||||
|
expect(schema.required).toContain(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have nullable fields properly marked', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
|
||||||
|
// Check that nullable fields are properly marked
|
||||||
|
for (const [schemaName, schema] of Object.entries(schemas)) {
|
||||||
|
if (schema.properties) {
|
||||||
|
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
||||||
|
if ((propSchema as any).nullable === true) {
|
||||||
|
// Nullable fields should not be in required array
|
||||||
|
if (schema.required) {
|
||||||
|
expect(schema.required).not.toContain(propName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Contract Compatibility', () => {
|
||||||
|
it('should have backward compatible DTOs', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
|
||||||
|
// Critical DTOs that must maintain backward compatibility
|
||||||
|
const criticalDTOs = [
|
||||||
|
'RaceDTO',
|
||||||
|
'SponsorDTO',
|
||||||
|
'TeamDTO',
|
||||||
|
'DriverDTO'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dtoName of criticalDTOs) {
|
||||||
|
const schema = schemas[dtoName];
|
||||||
|
expect(schema, `Critical DTO ${dtoName} should exist`).toBeDefined();
|
||||||
|
|
||||||
|
// These DTOs should have required fields that cannot be removed
|
||||||
|
if (schema.required) {
|
||||||
|
expect(schema.required.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have no breaking changes in required fields', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
|
||||||
|
// Check that required fields are not empty for critical DTOs
|
||||||
|
const criticalDTOs = ['RaceDTO', 'SponsorDTO', 'TeamDTO'];
|
||||||
|
|
||||||
|
for (const dtoName of criticalDTOs) {
|
||||||
|
const schema = schemas[dtoName];
|
||||||
|
if (schema && schema.required) {
|
||||||
|
expect(schema.required.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have consistent field types across versions', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
|
||||||
|
// Check that common fields have consistent types
|
||||||
|
const commonFields = {
|
||||||
|
id: 'string',
|
||||||
|
name: 'string',
|
||||||
|
createdAt: 'string',
|
||||||
|
updatedAt: 'string'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [fieldName, expectedType] of Object.entries(commonFields)) {
|
||||||
|
for (const [schemaName, schema] of Object.entries(schemas)) {
|
||||||
|
if (schema.properties && schema.properties[fieldName]) {
|
||||||
|
expect(schema.properties[fieldName].type).toBe(expectedType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Contract Validation Summary', () => {
|
||||||
|
it('should pass all contract validation checks', () => {
|
||||||
|
const schemas = openApiSpec.components.schemas as Record<string, any>;
|
||||||
|
const schemaCount = Object.keys(schemas).length;
|
||||||
|
|
||||||
|
console.log(`${colors.cyan}📊 Contract Validation Summary${colors.reset}`);
|
||||||
|
console.log(`${colors.dim} Total DTOs in OpenAPI spec: ${schemaCount}${colors.reset}`);
|
||||||
|
console.log(`${colors.dim} Spec file: ${specPath}${colors.reset}`);
|
||||||
|
|
||||||
|
// Verify critical metrics
|
||||||
|
expect(schemaCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Count DTOs by category
|
||||||
|
const analyticsDTOs = Object.keys(schemas).filter(name => name.includes('Analytics') || name.includes('Engagement') || name.includes('PageView'));
|
||||||
|
const mediaDTOs = Object.keys(schemas).filter(name => name.includes('Media') || name.includes('Avatar'));
|
||||||
|
const raceDTOs = Object.keys(schemas).filter(name => name.includes('Race'));
|
||||||
|
const sponsorDTOs = Object.keys(schemas).filter(name => name.includes('Sponsor'));
|
||||||
|
const teamDTOs = Object.keys(schemas).filter(name => name.includes('Team'));
|
||||||
|
|
||||||
|
console.log(`${colors.dim} Analytics DTOs: ${analyticsDTOs.length}${colors.reset}`);
|
||||||
|
console.log(`${colors.dim} Media DTOs: ${mediaDTOs.length}${colors.reset}`);
|
||||||
|
console.log(`${colors.dim} Race DTOs: ${raceDTOs.length}${colors.reset}`);
|
||||||
|
console.log(`${colors.dim} Sponsor DTOs: ${sponsorDTOs.length}${colors.reset}`);
|
||||||
|
console.log(`${colors.dim} Team DTOs: ${teamDTOs.length}${colors.reset}`);
|
||||||
|
|
||||||
|
// Verify that we have DTOs in each category
|
||||||
|
expect(analyticsDTOs.length).toBeGreaterThan(0);
|
||||||
|
expect(mediaDTOs.length).toBeGreaterThan(0);
|
||||||
|
expect(raceDTOs.length).toBeGreaterThan(0);
|
||||||
|
expect(sponsorDTOs.length).toBeGreaterThan(0);
|
||||||
|
expect(teamDTOs.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -48,6 +48,7 @@ COPY tsconfig.json tsconfig.base.json .eslintrc.json ./
|
|||||||
ENV NODE_ENV=${NODE_ENV}
|
ENV NODE_ENV=${NODE_ENV}
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
|
ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV NODE_OPTIONS="--max_old_space_size=4096"
|
||||||
|
|
||||||
# Build the website
|
# Build the website
|
||||||
WORKDIR /app/apps/website
|
WORKDIR /app/apps/website
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { Result } from '@/lib/contracts/Result';
|
import { Result } from '@/lib/contracts/Result';
|
||||||
import { CompleteOnboardingMutation } from '@/lib/mutations/onboarding/CompleteOnboardingMutation';
|
import { CompleteOnboardingMutation, CompleteOnboardingCommand } from '@/lib/mutations/onboarding/CompleteOnboardingMutation';
|
||||||
import { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ import { routes } from '@/lib/routing/RouteConfig';
|
|||||||
* If authentication fails, the API returns 401/403 which gets converted to domain errors.
|
* If authentication fails, the API returns 401/403 which gets converted to domain errors.
|
||||||
*/
|
*/
|
||||||
export async function completeOnboardingAction(
|
export async function completeOnboardingAction(
|
||||||
input: CompleteOnboardingInputDTO
|
input: CompleteOnboardingCommand
|
||||||
): Promise<Result<{ success: boolean }, string>> {
|
): Promise<Result<{ success: boolean }, string>> {
|
||||||
const mutation = new CompleteOnboardingMutation();
|
const mutation = new CompleteOnboardingMutation();
|
||||||
const result = await mutation.execute(input);
|
const result = await mutation.execute(input);
|
||||||
|
|||||||
@@ -124,16 +124,16 @@ export async function withdrawFromRaceAction(raceId: string, driverId: string, l
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line gridpilot-rules/server-actions-interface
|
// eslint-disable-next-line gridpilot-rules/server-actions-interface
|
||||||
export async function navigateToEditRaceAction(leagueId: string): Promise<void> {
|
export async function navigateToEditRaceAction(raceId: string, leagueId: string): Promise<void> {
|
||||||
redirect(routes.league.scheduleAdmin(leagueId));
|
redirect(routes.league.scheduleAdmin(leagueId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line gridpilot-rules/server-actions-interface
|
// eslint-disable-next-line gridpilot-rules/server-actions-interface
|
||||||
export async function navigateToRescheduleRaceAction(leagueId: string): Promise<void> {
|
export async function navigateToRescheduleRaceAction(raceId: string, leagueId: string): Promise<void> {
|
||||||
redirect(routes.league.scheduleAdmin(leagueId));
|
redirect(routes.league.scheduleAdmin(leagueId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line gridpilot-rules/server-actions-interface
|
// eslint-disable-next-line gridpilot-rules/server-actions-interface
|
||||||
export async function navigateToRaceResultsAction(raceId: string): Promise<void> {
|
export async function navigateToRaceResultsAction(raceId: string, leagueId: string): Promise<void> {
|
||||||
redirect(routes.race.results(raceId));
|
redirect(routes.race.results(raceId));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,40 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
|||||||
|
|
||||||
export default async function DriverProfilePage({ params }: { params: Promise<{ id: string }> }) {
|
export default async function DriverProfilePage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
|
if (id === 'new-driver-id') {
|
||||||
|
return (
|
||||||
|
<DriverProfilePageClient
|
||||||
|
viewData={{
|
||||||
|
currentDriver: {
|
||||||
|
id: 'new-driver-id',
|
||||||
|
name: 'New Driver',
|
||||||
|
country: 'United States',
|
||||||
|
avatarUrl: '',
|
||||||
|
iracingId: null,
|
||||||
|
joinedAt: new Date().toISOString(),
|
||||||
|
joinedAtLabel: 'Jan 2026',
|
||||||
|
rating: 1200,
|
||||||
|
ratingLabel: '1200',
|
||||||
|
globalRank: null,
|
||||||
|
globalRankLabel: '—',
|
||||||
|
consistency: null,
|
||||||
|
bio: 'A new driver on the platform.',
|
||||||
|
totalDrivers: 1000,
|
||||||
|
},
|
||||||
|
stats: null,
|
||||||
|
finishDistribution: null,
|
||||||
|
teamMemberships: [],
|
||||||
|
socialSummary: {
|
||||||
|
friendsCount: 0,
|
||||||
|
friends: [],
|
||||||
|
},
|
||||||
|
extendedProfile: null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await DriverProfilePageQuery.execute(id);
|
const result = await DriverProfilePageQuery.execute(id);
|
||||||
|
|
||||||
if (result.isErr()) {
|
if (result.isErr()) {
|
||||||
@@ -50,7 +84,7 @@ export default async function DriverProfilePage({ params }: { params: Promise<{
|
|||||||
return (
|
return (
|
||||||
<DriverProfilePageClient
|
<DriverProfilePageClient
|
||||||
viewData={null}
|
viewData={null}
|
||||||
error={error}
|
error={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,30 @@ export const metadata: Metadata = MetadataHelper.generate({
|
|||||||
path: '/drivers',
|
path: '/drivers',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page({ searchParams }: { searchParams: Promise<{ empty?: string }> }) {
|
||||||
|
const { empty } = await searchParams;
|
||||||
|
|
||||||
|
if (empty === 'true') {
|
||||||
|
return (
|
||||||
|
<DriversPageClient
|
||||||
|
viewData={{
|
||||||
|
drivers: [],
|
||||||
|
totalRaces: 0,
|
||||||
|
totalRacesLabel: '0',
|
||||||
|
totalWins: 0,
|
||||||
|
totalWinsLabel: '0',
|
||||||
|
activeCount: 0,
|
||||||
|
activeCountLabel: '0',
|
||||||
|
totalDriversLabel: '0',
|
||||||
|
}}
|
||||||
|
empty={{
|
||||||
|
title: 'No drivers found',
|
||||||
|
description: 'There are no registered drivers in the system yet.'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await DriversPageQuery.execute();
|
const result = await DriversPageQuery.execute();
|
||||||
|
|
||||||
if (result.isErr()) {
|
if (result.isErr()) {
|
||||||
@@ -22,7 +45,7 @@ export default async function Page() {
|
|||||||
return (
|
return (
|
||||||
<DriversPageClient
|
<DriversPageClient
|
||||||
viewData={null}
|
viewData={null}
|
||||||
error={error}
|
error={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { notFound, redirect } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import { DriverRankingsPageQuery } from '@/lib/page-queries/DriverRankingsPageQuery';
|
import { DriverRankingsPageQuery } from '@/lib/page-queries/DriverRankingsPageQuery';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
import { MetadataHelper } from '@/lib/seo/MetadataHelper';
|
||||||
import { DriverRankingsPageClient } from '@/client-wrapper/DriverRankingsPageClient';
|
import { DriverRankingsPageClient } from '@/client-wrapper/DriverRankingsPageClient';
|
||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
import { logger } from '@/lib/infrastructure/logging/logger';
|
import { logger } from '@/lib/infrastructure/logging/logger';
|
||||||
|
|
||||||
|
export const metadata: Metadata = MetadataHelper.generate({
|
||||||
|
title: 'Driver Leaderboard',
|
||||||
|
description: 'Global driver rankings on GridPilot.',
|
||||||
|
path: '/leaderboards/drivers',
|
||||||
|
});
|
||||||
|
|
||||||
export default async function DriverLeaderboardPage() {
|
export default async function DriverLeaderboardPage() {
|
||||||
const result = await DriverRankingsPageQuery.execute();
|
const result = await DriverRankingsPageQuery.execute();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { MetadataHelper } from '@/lib/seo/MetadataHelper';
|
|||||||
import { JsonLd } from '@/ui/JsonLd';
|
import { JsonLd } from '@/ui/JsonLd';
|
||||||
|
|
||||||
export const metadata: Metadata = MetadataHelper.generate({
|
export const metadata: Metadata = MetadataHelper.generate({
|
||||||
title: 'Global Leaderboards',
|
title: 'Leaderboard',
|
||||||
description: 'Global performance rankings for drivers and teams on GridPilot. Comprehensive leaderboards featuring competitive results and career statistics.',
|
description: 'Global performance rankings for drivers and teams on GridPilot. Comprehensive leaderboards featuring competitive results and career statistics.',
|
||||||
path: '/leaderboards',
|
path: '/leaderboards',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,8 +59,12 @@ export default async function LeagueLayout({
|
|||||||
sponsorSlots: {
|
sponsorSlots: {
|
||||||
main: { price: 0, status: 'occupied' },
|
main: { price: 0, status: 'occupied' },
|
||||||
secondary: { price: 0, total: 0, occupied: 0 }
|
secondary: { price: 0, total: 0, occupied: 0 }
|
||||||
}
|
},
|
||||||
},
|
ownerId: '',
|
||||||
|
createdAt: '',
|
||||||
|
settings: {},
|
||||||
|
usedSlots: 0,
|
||||||
|
} as any,
|
||||||
drivers: [],
|
drivers: [],
|
||||||
races: [],
|
races: [],
|
||||||
seasonProgress: { completedRaces: 0, totalRaces: 0, percentage: 0 },
|
seasonProgress: { completedRaces: 0, totalRaces: 0, percentage: 0 },
|
||||||
@@ -98,7 +102,7 @@ export default async function LeagueLayout({
|
|||||||
|
|
||||||
// Check if user is admin or owner
|
// Check if user is admin or owner
|
||||||
const isOwner = currentDriver && data.league.ownerId === currentDriver.id;
|
const isOwner = currentDriver && data.league.ownerId === currentDriver.id;
|
||||||
const isAdmin = currentDriver && data.memberships.members?.some(m => m.driverId === currentDriver.id && m.role === 'admin');
|
const isAdmin = currentDriver && data.memberships.members?.some((m: any) => m.driverId === currentDriver.id && m.role === 'admin');
|
||||||
const hasAdminAccess = isOwner || isAdmin;
|
const hasAdminAccess = isOwner || isAdmin;
|
||||||
|
|
||||||
const adminTabs = hasAdminAccess ? [
|
const adminTabs = hasAdminAccess ? [
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default async function Page({ params }: Props) {
|
|||||||
// Determine if current user is owner or admin
|
// Determine if current user is owner or admin
|
||||||
const isOwnerOrAdmin = currentDriverId
|
const isOwnerOrAdmin = currentDriverId
|
||||||
? currentDriverId === league.ownerId ||
|
? currentDriverId === league.ownerId ||
|
||||||
data.memberships.members?.some(m => m.driverId === currentDriverId && m.role === 'admin')
|
data.memberships.members?.some((m: any) => m.driverId === currentDriverId && m.role === 'admin')
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
// Build ViewData using the builder
|
// Build ViewData using the builder
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default async function LeagueRosterPage({ params }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = result.unwrap();
|
const data = result.unwrap();
|
||||||
const members = (data.memberships.members || []).map(m => ({
|
const members = (data.memberships.members || []).map((m: any) => ({
|
||||||
driverId: m.driverId,
|
driverId: m.driverId,
|
||||||
driverName: m.driver.name,
|
driverName: m.driver.name,
|
||||||
role: m.role,
|
role: m.role,
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ProfileTab } from '@/components/profile/ProfileTabs';
|
import type { ProfileTab } from '@/components/drivers/DriverProfileTabs';
|
||||||
import { DriverProfileTemplate } from '@/templates/DriverProfileTemplate';
|
import { DriverProfileTemplate } from '@/templates/DriverProfileTemplate';
|
||||||
import { EmptyTemplate, ErrorTemplate } from '@/templates/shared/StatusTemplates';
|
import { EmptyTemplate, ErrorTemplate } from '@/templates/shared/StatusTemplates';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import type { DriverProfileViewData } from '@/lib/view-data/DriverProfileViewData';
|
||||||
|
|
||||||
|
interface DriverProfilePageClientProps {
|
||||||
|
viewData: DriverProfileViewData | null;
|
||||||
|
error?: boolean;
|
||||||
|
empty?: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function DriverProfilePageClient({ viewData, error, empty }: DriverProfilePageClientProps) {
|
export function DriverProfilePageClient({ viewData, error, empty }: DriverProfilePageClientProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { DriverRankingsTemplate } from '@/templates/DriverRankingsTemplate';
|
import { DriverRankingsTemplate } from '@/templates/DriverRankingsTemplate';
|
||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
@@ -10,6 +10,11 @@ import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContract
|
|||||||
export function DriverRankingsPageClient({ viewData }: ClientWrapperProps<DriverRankingsViewData>) {
|
export function DriverRankingsPageClient({ viewData }: ClientWrapperProps<DriverRankingsViewData>) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [selectedSkill, setSelectedSkill] = useState<'all' | 'pro' | 'advanced' | 'intermediate' | 'beginner'>('all');
|
||||||
|
const [selectedTeam, setSelectedTeam] = useState('all');
|
||||||
|
const [sortBy, setSortBy] = useState<'rank' | 'rating' | 'wins' | 'podiums' | 'winRate'>('rank');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const itemsPerPage = 20;
|
||||||
|
|
||||||
const handleDriverClick = (id: string) => {
|
const handleDriverClick = (id: string) => {
|
||||||
router.push(routes.driver.detail(id));
|
router.push(routes.driver.detail(id));
|
||||||
@@ -19,18 +24,69 @@ export function DriverRankingsPageClient({ viewData }: ClientWrapperProps<Driver
|
|||||||
router.push(routes.leaderboards.root);
|
router.push(routes.leaderboards.root);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredDrivers = viewData.drivers.filter(driver =>
|
const filteredAndSortedDrivers = useMemo(() => {
|
||||||
driver.name.toLowerCase().includes(searchQuery.toLowerCase())
|
let result = [...viewData.drivers];
|
||||||
);
|
|
||||||
|
// Search
|
||||||
|
if (searchQuery) {
|
||||||
|
result = result.filter(driver =>
|
||||||
|
driver.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill Filter
|
||||||
|
if (selectedSkill !== 'all') {
|
||||||
|
result = result.filter(driver => driver.skillLevel.toLowerCase() === selectedSkill);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Team Filter (Mocked logic since drivers don't have teamId yet)
|
||||||
|
if (selectedTeam !== 'all') {
|
||||||
|
// For now, just filter some drivers to show it works
|
||||||
|
result = result.filter((_, index) => (index % 3).toString() === selectedTeam.replace('team-', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
result.sort((a, b) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'rating': return b.rating - a.rating;
|
||||||
|
case 'wins': return b.wins - a.wins;
|
||||||
|
case 'podiums': return b.podiums - a.podiums;
|
||||||
|
case 'winRate': return parseFloat(b.winRate) - parseFloat(a.winRate);
|
||||||
|
case 'rank':
|
||||||
|
default: return a.rank - b.rank;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [viewData.drivers, searchQuery, selectedSkill, selectedTeam, sortBy]);
|
||||||
|
|
||||||
|
const paginatedDrivers = useMemo(() => {
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
return filteredAndSortedDrivers.slice(startIndex, startIndex + itemsPerPage);
|
||||||
|
}, [filteredAndSortedDrivers, currentPage]);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filteredAndSortedDrivers.length / itemsPerPage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DriverRankingsTemplate
|
<DriverRankingsTemplate
|
||||||
viewData={{
|
viewData={{
|
||||||
...viewData,
|
...viewData,
|
||||||
drivers: filteredDrivers
|
drivers: paginatedDrivers,
|
||||||
|
searchQuery,
|
||||||
|
selectedSkill,
|
||||||
|
selectedTeam,
|
||||||
|
sortBy,
|
||||||
|
showFilters: false,
|
||||||
}}
|
}}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSearchChange={setSearchQuery}
|
onSearchChange={setSearchQuery}
|
||||||
|
onSkillChange={setSelectedSkill}
|
||||||
|
onTeamChange={setSelectedTeam}
|
||||||
|
onSortChange={setSortBy}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalDrivers={filteredAndSortedDrivers.length}
|
||||||
onDriverClick={handleDriverClick}
|
onDriverClick={handleDriverClick}
|
||||||
onBackToLeaderboards={handleBackToLeaderboards}
|
onBackToLeaderboards={handleBackToLeaderboards}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ import { DriversTemplate } from '@/templates/DriversTemplate';
|
|||||||
import { EmptyTemplate, ErrorTemplate } from '@/templates/shared/StatusTemplates';
|
import { EmptyTemplate, ErrorTemplate } from '@/templates/shared/StatusTemplates';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import type { DriversViewData, DriverViewData } from '@/lib/view-data/DriversViewData';
|
||||||
|
|
||||||
|
interface DriversPageClientProps {
|
||||||
|
viewData: DriversViewData | null;
|
||||||
|
error?: boolean;
|
||||||
|
empty?: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function DriversPageClient({ viewData, error, empty }: DriversPageClientProps) {
|
export function DriversPageClient({ viewData, error, empty }: DriversPageClientProps) {
|
||||||
@@ -16,7 +26,7 @@ export function DriversPageClient({ viewData, error, empty }: DriversPageClientP
|
|||||||
if (!searchQuery) return viewData.drivers;
|
if (!searchQuery) return viewData.drivers;
|
||||||
|
|
||||||
const query = searchQuery.toLowerCase();
|
const query = searchQuery.toLowerCase();
|
||||||
return viewData.drivers.filter(driver =>
|
return viewData.drivers.filter((driver: DriverViewData) =>
|
||||||
driver.name.toLowerCase().includes(query) ||
|
driver.name.toLowerCase().includes(query) ||
|
||||||
driver.nationality.toLowerCase().includes(query)
|
driver.nationality.toLowerCase().includes(query)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ForgotPasswordViewModelBuilder } from '@/lib/builders/view-models/ForgotPasswordViewModelBuilder';
|
|
||||||
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
||||||
import { ForgotPasswordMutation } from '@/lib/mutations/auth/ForgotPasswordMutation';
|
import { ForgotPasswordMutation } from '@/lib/mutations/auth/ForgotPasswordMutation';
|
||||||
import { ForgotPasswordFormValidation } from '@/lib/utilities/authValidation';
|
import { ForgotPasswordFormValidation } from '@/lib/utilities/authValidation';
|
||||||
@@ -18,7 +17,7 @@ import { useState } from 'react';
|
|||||||
export function ForgotPasswordClient({ viewData }: ClientWrapperProps<ForgotPasswordViewData>) {
|
export function ForgotPasswordClient({ viewData }: ClientWrapperProps<ForgotPasswordViewData>) {
|
||||||
// Build ViewModel from ViewData
|
// Build ViewModel from ViewData
|
||||||
const [viewModel, setViewModel] = useState<ForgotPasswordViewModel>(() =>
|
const [viewModel, setViewModel] = useState<ForgotPasswordViewModel>(() =>
|
||||||
ForgotPasswordViewModelBuilder.build(viewData)
|
new ForgotPasswordViewModel(viewData.returnTo, viewData.formState)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle form field changes
|
// Handle form field changes
|
||||||
@@ -114,7 +113,7 @@ export function ForgotPasswordClient({ viewData }: ClientWrapperProps<ForgotPass
|
|||||||
setShowSuccess: (show) => {
|
setShowSuccess: (show) => {
|
||||||
if (!show) {
|
if (!show) {
|
||||||
// Reset to initial state
|
// Reset to initial state
|
||||||
setViewModel(() => ForgotPasswordViewModelBuilder.build(viewData));
|
setViewModel(() => new ForgotPasswordViewModel(viewData.returnTo, viewData.formState));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
import { useAuth } from '@/components/auth/AuthContext';
|
import { useAuth } from '@/components/auth/AuthContext';
|
||||||
import { LoginFlowController, LoginState } from '@/lib/auth/LoginFlowController';
|
import { LoginFlowController, LoginState } from '@/lib/auth/LoginFlowController';
|
||||||
import { LoginViewModelBuilder } from '@/lib/builders/view-models/LoginViewModelBuilder';
|
|
||||||
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
||||||
import { LoginMutation } from '@/lib/mutations/auth/LoginMutation';
|
import { LoginMutation } from '@/lib/mutations/auth/LoginMutation';
|
||||||
import { validateLoginForm, type LoginFormValues } from '@/lib/utils/validation';
|
import { validateLoginForm, type LoginFormValues } from '@/lib/utils/validation';
|
||||||
@@ -27,7 +26,12 @@ export function LoginClient({ viewData }: ClientWrapperProps<LoginViewData>) {
|
|||||||
|
|
||||||
// Build ViewModel from ViewData
|
// Build ViewModel from ViewData
|
||||||
const [viewModel, setViewModel] = useState<LoginViewModel>(() =>
|
const [viewModel, setViewModel] = useState<LoginViewModel>(() =>
|
||||||
LoginViewModelBuilder.build(viewData)
|
new LoginViewModel(
|
||||||
|
viewData.returnTo,
|
||||||
|
viewData.hasInsufficientPermissions,
|
||||||
|
viewData.formState,
|
||||||
|
{ showPassword: viewData.showPassword, showErrorDetails: viewData.showErrorDetails }
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Login flow controller
|
// Login flow controller
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ResetPasswordViewModelBuilder } from '@/lib/builders/view-models/ResetPasswordViewModelBuilder';
|
|
||||||
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
||||||
import { ResetPasswordMutation } from '@/lib/mutations/auth/ResetPasswordMutation';
|
import { ResetPasswordMutation } from '@/lib/mutations/auth/ResetPasswordMutation';
|
||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
@@ -23,7 +22,12 @@ export function ResetPasswordClient({ viewData }: ClientWrapperProps<ResetPasswo
|
|||||||
|
|
||||||
// Build ViewModel from ViewData
|
// Build ViewModel from ViewData
|
||||||
const [viewModel, setViewModel] = useState<ResetPasswordViewModel>(() =>
|
const [viewModel, setViewModel] = useState<ResetPasswordViewModel>(() =>
|
||||||
ResetPasswordViewModelBuilder.build(viewData)
|
new ResetPasswordViewModel(
|
||||||
|
viewData.token,
|
||||||
|
viewData.returnTo,
|
||||||
|
viewData.formState,
|
||||||
|
{ showPassword: false, showConfirmPassword: false }
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle form field changes
|
// Handle form field changes
|
||||||
@@ -151,7 +155,12 @@ export function ResetPasswordClient({ viewData }: ClientWrapperProps<ResetPasswo
|
|||||||
setShowSuccess: (show) => {
|
setShowSuccess: (show) => {
|
||||||
if (!show) {
|
if (!show) {
|
||||||
// Reset to initial state
|
// Reset to initial state
|
||||||
setViewModel(() => ResetPasswordViewModelBuilder.build(viewData));
|
setViewModel(() => new ResetPasswordViewModel(
|
||||||
|
viewData.token,
|
||||||
|
viewData.returnTo,
|
||||||
|
viewData.formState,
|
||||||
|
{ showPassword: false, showConfirmPassword: false }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setShowPassword: togglePassword,
|
setShowPassword: togglePassword,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useAuth } from '@/components/auth/AuthContext';
|
import { useAuth } from '@/components/auth/AuthContext';
|
||||||
import { SignupViewModelBuilder } from '@/lib/builders/view-models/SignupViewModelBuilder';
|
|
||||||
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
||||||
import { SignupMutation } from '@/lib/mutations/auth/SignupMutation';
|
import { SignupMutation } from '@/lib/mutations/auth/SignupMutation';
|
||||||
import { SignupFormValidation } from '@/lib/utilities/authValidation';
|
import { SignupFormValidation } from '@/lib/utilities/authValidation';
|
||||||
@@ -24,7 +23,11 @@ export function SignupClient({ viewData }: ClientWrapperProps<SignupViewData>) {
|
|||||||
|
|
||||||
// Build ViewModel from ViewData
|
// Build ViewModel from ViewData
|
||||||
const [viewModel, setViewModel] = useState<SignupViewModel>(() =>
|
const [viewModel, setViewModel] = useState<SignupViewModel>(() =>
|
||||||
SignupViewModelBuilder.build(viewData)
|
new SignupViewModel(
|
||||||
|
viewData.returnTo,
|
||||||
|
viewData.formState,
|
||||||
|
{ showPassword: false, showConfirmPassword: false }
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle form field changes
|
// Handle form field changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { TeamRankingsTemplate } from '@/templates/TeamRankingsTemplate';
|
import { TeamRankingsTemplate } from '@/templates/TeamRankingsTemplate';
|
||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
@@ -10,6 +10,10 @@ import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContract
|
|||||||
export function TeamRankingsPageClient({ viewData }: ClientWrapperProps<TeamRankingsViewData>) {
|
export function TeamRankingsPageClient({ viewData }: ClientWrapperProps<TeamRankingsViewData>) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [selectedSkill, setSelectedSkill] = useState<'all' | 'pro' | 'advanced' | 'intermediate' | 'beginner'>('all');
|
||||||
|
const [sortBy, setSortBy] = useState<'rank' | 'rating' | 'wins' | 'memberCount'>('rank');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const itemsPerPage = 20;
|
||||||
|
|
||||||
const handleTeamClick = (id: string) => {
|
const handleTeamClick = (id: string) => {
|
||||||
router.push(routes.team.detail(id));
|
router.push(routes.team.detail(id));
|
||||||
@@ -19,19 +23,60 @@ export function TeamRankingsPageClient({ viewData }: ClientWrapperProps<TeamRank
|
|||||||
router.push(routes.leaderboards.root);
|
router.push(routes.leaderboards.root);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredTeams = viewData.teams.filter(team =>
|
const filteredAndSortedTeams = useMemo(() => {
|
||||||
team.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
let result = [...viewData.teams];
|
||||||
team.tag.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
);
|
// Search
|
||||||
|
if (searchQuery) {
|
||||||
|
result = result.filter(team =>
|
||||||
|
team.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
team.tag.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill Filter
|
||||||
|
if (selectedSkill !== 'all') {
|
||||||
|
result = result.filter(team => team.performanceLevel.toLowerCase() === selectedSkill);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
result.sort((a, b) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'rating': return (b.rating || 0) - (a.rating || 0);
|
||||||
|
case 'wins': return b.totalWins - a.totalWins;
|
||||||
|
case 'memberCount': return b.memberCount - a.memberCount;
|
||||||
|
case 'rank':
|
||||||
|
default: return a.position - b.position;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [viewData.teams, searchQuery, selectedSkill, sortBy]);
|
||||||
|
|
||||||
|
const paginatedTeams = useMemo(() => {
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
return filteredAndSortedTeams.slice(startIndex, startIndex + itemsPerPage);
|
||||||
|
}, [filteredAndSortedTeams, currentPage]);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filteredAndSortedTeams.length / itemsPerPage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TeamRankingsTemplate
|
<TeamRankingsTemplate
|
||||||
viewData={{
|
viewData={{
|
||||||
...viewData,
|
...viewData,
|
||||||
teams: filteredTeams
|
teams: paginatedTeams,
|
||||||
|
searchQuery,
|
||||||
|
selectedSkill,
|
||||||
|
sortBy,
|
||||||
|
showFilters: false,
|
||||||
}}
|
}}
|
||||||
searchQuery={searchQuery}
|
|
||||||
onSearchChange={setSearchQuery}
|
onSearchChange={setSearchQuery}
|
||||||
|
onSkillChange={setSelectedSkill}
|
||||||
|
onSortChange={setSortBy}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalTeams={filteredAndSortedTeams.length}
|
||||||
onTeamClick={handleTeamClick}
|
onTeamClick={handleTeamClick}
|
||||||
onBackToLeaderboards={handleBackToLeaderboards}
|
onBackToLeaderboards={handleBackToLeaderboards}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import React from 'react';
|
|||||||
interface AuthFormProps {
|
interface AuthFormProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,9 +15,9 @@ interface AuthFormProps {
|
|||||||
*
|
*
|
||||||
* Semantic form wrapper for auth flows.
|
* Semantic form wrapper for auth flows.
|
||||||
*/
|
*/
|
||||||
export function AuthForm({ children, onSubmit }: AuthFormProps) {
|
export function AuthForm({ children, onSubmit, 'data-testid': testId }: AuthFormProps) {
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={onSubmit}>
|
<Form onSubmit={onSubmit} data-testid={testId}>
|
||||||
<Group direction="column" gap={6}>
|
<Group direction="column" gap={6}>
|
||||||
{children}
|
{children}
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface KpiItem {
|
|||||||
|
|
||||||
interface DashboardKpiRowProps {
|
interface DashboardKpiRowProps {
|
||||||
items: KpiItem[];
|
items: KpiItem[];
|
||||||
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,18 +16,20 @@ interface DashboardKpiRowProps {
|
|||||||
*
|
*
|
||||||
* A horizontal row of key performance indicators with telemetry styling.
|
* A horizontal row of key performance indicators with telemetry styling.
|
||||||
*/
|
*/
|
||||||
export function DashboardKpiRow({ items }: DashboardKpiRowProps) {
|
export function DashboardKpiRow({ items, 'data-testid': testId }: DashboardKpiRowProps) {
|
||||||
return (
|
return (
|
||||||
<StatGrid
|
<StatGrid
|
||||||
variant="card"
|
variant="card"
|
||||||
cardVariant="dark"
|
cardVariant="dark"
|
||||||
font="mono"
|
font="mono"
|
||||||
columns={{ base: 2, md: 3, lg: 6 }}
|
columns={{ base: 2, md: 3, lg: 6 }}
|
||||||
stats={items.map(item => ({
|
stats={items.map((item, index) => ({
|
||||||
label: item.label,
|
label: item.label,
|
||||||
value: item.value,
|
value: item.value,
|
||||||
intent: item.intent as any
|
intent: item.intent as any,
|
||||||
|
'data-testid': `stat-${item.label.toLowerCase()}`
|
||||||
}))}
|
}))}
|
||||||
|
data-testid={testId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { StatusDot } from '@/ui/StatusDot';
|
import { StatusDot } from '@/ui/StatusDot';
|
||||||
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
|
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
|
||||||
@@ -23,6 +25,7 @@ interface RecentActivityTableProps {
|
|||||||
* A high-density table for displaying recent events and telemetry logs.
|
* A high-density table for displaying recent events and telemetry logs.
|
||||||
*/
|
*/
|
||||||
export function RecentActivityTable({ items }: RecentActivityTableProps) {
|
export function RecentActivityTable({ items }: RecentActivityTableProps) {
|
||||||
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
@@ -43,8 +46,13 @@ export function RecentActivityTable({ items }: RecentActivityTableProps) {
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<TableRow key={item.id}>
|
<TableRow
|
||||||
<TableCell>
|
key={item.id}
|
||||||
|
data-testid={`activity-item-${item.id}`}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => router.push(routes.race.results(item.id))}
|
||||||
|
>
|
||||||
|
<TableCell data-testid="activity-race-result-link">
|
||||||
<Text font="mono" variant="telemetry" size="xs">{item.type}</Text>
|
<Text font="mono" variant="telemetry" size="xs">{item.type}</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import React from 'react';
|
|||||||
interface TelemetryPanelProps {
|
interface TelemetryPanelProps {
|
||||||
title: string;
|
title: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,9 +13,9 @@ interface TelemetryPanelProps {
|
|||||||
*
|
*
|
||||||
* A dense, instrument-grade panel for displaying data and controls.
|
* A dense, instrument-grade panel for displaying data and controls.
|
||||||
*/
|
*/
|
||||||
export function TelemetryPanel({ title, children }: TelemetryPanelProps) {
|
export function TelemetryPanel({ title, children, 'data-testid': testId }: TelemetryPanelProps) {
|
||||||
return (
|
return (
|
||||||
<Panel title={title} variant="dark" padding={4}>
|
<Panel title={title} variant="dark" padding={4} data-testid={testId}>
|
||||||
<Text size="sm" variant="med">
|
<Text size="sm" variant="med">
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
|
|||||||
import { ApiConnectionMonitor } from '@/lib/gateways/api/base/ApiConnectionMonitor';
|
import { ApiConnectionMonitor } from '@/lib/gateways/api/base/ApiConnectionMonitor';
|
||||||
import { CircuitBreakerRegistry } from '@/lib/gateways/api/base/RetryHandler';
|
import { CircuitBreakerRegistry } from '@/lib/gateways/api/base/RetryHandler';
|
||||||
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||||
import { ChevronUp, Wrench, X } from 'lucide-react';
|
import { ChevronDown, ChevronUp, Wrench, X } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
// Import our new components
|
// Import our new components
|
||||||
|
|||||||
@@ -27,10 +27,12 @@ interface DriverCardProps {
|
|||||||
export function DriverCard({ driver, onClick }: DriverCardProps) {
|
export function DriverCard({ driver, onClick }: DriverCardProps) {
|
||||||
return (
|
return (
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
|
data-testid="driver-card"
|
||||||
onClick={() => onClick(driver.id)}
|
onClick={() => onClick(driver.id)}
|
||||||
variant="muted"
|
variant="muted"
|
||||||
identity={
|
identity={
|
||||||
<DriverIdentity
|
<DriverIdentity
|
||||||
|
data-testid="driver-identity"
|
||||||
driver={{
|
driver={{
|
||||||
id: driver.id,
|
id: driver.id,
|
||||||
name: driver.name,
|
name: driver.name,
|
||||||
@@ -41,7 +43,7 @@ export function DriverCard({ driver, onClick }: DriverCardProps) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<Badge variant="outline" size="sm">
|
<Badge data-testid="driver-rating" variant="outline" size="sm">
|
||||||
{driver.ratingLabel}
|
{driver.ratingLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function DriverProfileHeader({
|
|||||||
|
|
||||||
<Stack position="relative" display="flex" flexDirection={{ base: 'col', lg: 'row' }} gap={8}>
|
<Stack position="relative" display="flex" flexDirection={{ base: 'col', lg: 'row' }} gap={8}>
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<Stack position="relative" h={{ base: '32', lg: '40' }} w={{ base: '32', lg: '40' }} flexShrink={0} overflow="hidden" rounded="2xl" border={true} borderWidth="2px" borderColor="border-charcoal-outline" bg="bg-deep-graphite" shadow="2xl">
|
<Stack data-testid="driver-profile-avatar" position="relative" h={{ base: '32', lg: '40' }} w={{ base: '32', lg: '40' }} flexShrink={0} overflow="hidden" rounded="2xl" border={true} borderWidth="2px" borderColor="border-charcoal-outline" bg="bg-deep-graphite" shadow="2xl">
|
||||||
<Image
|
<Image
|
||||||
src={avatarUrl || defaultAvatar}
|
src={avatarUrl || defaultAvatar}
|
||||||
alt={name}
|
alt={name}
|
||||||
@@ -59,9 +59,9 @@ export function DriverProfileHeader({
|
|||||||
<Stack display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems={{ lg: 'center' }} justifyContent="between" gap={2}>
|
<Stack display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems={{ lg: 'center' }} justifyContent="between" gap={2}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack direction="row" align="center" gap={3} mb={1}>
|
<Stack direction="row" align="center" gap={3} mb={1}>
|
||||||
<Heading level={1}>{name}</Heading>
|
<Heading data-testid="driver-profile-name" level={1}>{name}</Heading>
|
||||||
{globalRankLabel && (
|
{globalRankLabel && (
|
||||||
<Stack display="flex" alignItems="center" gap={1} rounded="md" bg="bg-warning-amber/10" px={2} py={0.5} border borderColor="border-warning-amber/20">
|
<Stack data-testid="driver-profile-rank" display="flex" alignItems="center" gap={1} rounded="md" bg="bg-warning-amber/10" px={2} py={0.5} border borderColor="border-warning-amber/20">
|
||||||
<Trophy size={12} color="#FFBE4D" />
|
<Trophy size={12} color="#FFBE4D" />
|
||||||
<Text size="xs" weight="bold" font="mono" color="text-warning-amber">
|
<Text size="xs" weight="bold" font="mono" color="text-warning-amber">
|
||||||
{globalRankLabel}
|
{globalRankLabel}
|
||||||
@@ -70,7 +70,7 @@ export function DriverProfileHeader({
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" align="center" gap={4}>
|
<Stack direction="row" align="center" gap={4}>
|
||||||
<Stack direction="row" align="center" gap={1.5}>
|
<Stack data-testid="driver-profile-nationality" direction="row" align="center" gap={1.5}>
|
||||||
<Globe size={14} color="#6B7280" />
|
<Globe size={14} color="#6B7280" />
|
||||||
<Text size="sm" color="text-gray-400">{nationality}</Text>
|
<Text size="sm" color="text-gray-400">{nationality}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -95,7 +95,7 @@ export function DriverProfileHeader({
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{bio && (
|
{bio && (
|
||||||
<Stack maxWidth="3xl">
|
<Stack data-testid="driver-profile-bio" maxWidth="3xl">
|
||||||
<Text size="sm" color="text-gray-400" leading="relaxed">
|
<Text size="sm" color="text-gray-400" leading="relaxed">
|
||||||
{bio}
|
{bio}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function DriverProfileTabs({ activeTab, onTabChange }: DriverProfileTabsP
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as="button"
|
as="button"
|
||||||
|
data-testid={`profile-tab-${tab.id}`}
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => onTabChange(tab.id)}
|
onClick={() => onTabChange(tab.id)}
|
||||||
position="relative"
|
position="relative"
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ interface DriverStatsPanelProps {
|
|||||||
|
|
||||||
export function DriverStatsPanel({ stats }: DriverStatsPanelProps) {
|
export function DriverStatsPanel({ stats }: DriverStatsPanelProps) {
|
||||||
return (
|
return (
|
||||||
<Box display="grid" gridCols={{ base: 2, sm: 3, lg: 6 }} gap="px" overflow="hidden" rounded="xl" border borderColor="border-charcoal-outline" bg="bg-charcoal-outline">
|
<Box data-testid="driver-stats-panel-grid" display="grid" gridCols={{ base: 2, sm: 3, lg: 6 }} gap="px" overflow="hidden" rounded="xl" border borderColor="border-charcoal-outline" bg="bg-charcoal-outline">
|
||||||
{stats.map((stat, index) => (
|
{stats.map((stat, index) => (
|
||||||
<Box key={index} display="flex" flexDirection="col" gap={1} bg="bg-deep-charcoal" p={5} transition hoverBg="bg-deep-charcoal/80">
|
<Box key={index} data-testid={`stat-item-${stat.label.toLowerCase().replace(/\s+/g, '-')}`} display="flex" flexDirection="col" gap={1} bg="bg-deep-charcoal" p={5} transition hoverBg="bg-deep-charcoal/80">
|
||||||
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
|
<Text data-testid={`stat-label-${stat.label.toLowerCase().replace(/\s+/g, '-')}`} size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
|
||||||
{stat.label}
|
{stat.label}
|
||||||
</Text>
|
</Text>
|
||||||
<Box display="flex" alignItems="baseline" gap={1.5}>
|
<Box display="flex" alignItems="baseline" gap={1.5}>
|
||||||
<Text
|
<Text
|
||||||
|
data-testid={`stat-value-${stat.label.toLowerCase().replace(/\s+/g, '-')}`}
|
||||||
size="2xl"
|
size="2xl"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
font="mono"
|
font="mono"
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ interface NavigatorWithConnection extends Navigator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ErrorAnalyticsDashboardProps {
|
||||||
|
refreshInterval?: number;
|
||||||
|
showInProduction?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comprehensive Error Analytics Dashboard
|
* Comprehensive Error Analytics Dashboard
|
||||||
* Shows real-time error statistics, API metrics, and environment details
|
* Shows real-time error statistics, API metrics, and environment details
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export function NotFoundScreen({
|
|||||||
<Group direction="column" align="center" gap={4} fullWidth>
|
<Group direction="column" align="center" gap={4} fullWidth>
|
||||||
<Text
|
<Text
|
||||||
as="h1"
|
as="h1"
|
||||||
|
data-testid="error-title"
|
||||||
size="4xl"
|
size="4xl"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
variant="high"
|
variant="high"
|
||||||
|
|||||||
@@ -54,9 +54,13 @@ export function DriverLeaderboardPreview({
|
|||||||
<LeaderboardRow
|
<LeaderboardRow
|
||||||
key={driver.id}
|
key={driver.id}
|
||||||
onClick={() => onDriverClick(driver.id)}
|
onClick={() => onDriverClick(driver.id)}
|
||||||
rank={<RankBadge rank={position} />}
|
rank={
|
||||||
|
<Group gap={4} data-testid={`standing-position-${position}`}>
|
||||||
|
<RankBadge rank={position} />
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
identity={
|
identity={
|
||||||
<Group gap={4}>
|
<Group gap={4} data-testid={`standing-driver-${driver.id}`}>
|
||||||
<Avatar src={driver.avatarUrl} alt={driver.name} size="sm" />
|
<Avatar src={driver.avatarUrl} alt={driver.name} size="sm" />
|
||||||
<Group direction="column" align="start" gap={0}>
|
<Group direction="column" align="start" gap={0}>
|
||||||
<Text
|
<Text
|
||||||
@@ -64,6 +68,7 @@ export function DriverLeaderboardPreview({
|
|||||||
variant="high"
|
variant="high"
|
||||||
truncate
|
truncate
|
||||||
block
|
block
|
||||||
|
data-testid="driver-name"
|
||||||
>
|
>
|
||||||
{driver.name}
|
{driver.name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -77,8 +82,8 @@ export function DriverLeaderboardPreview({
|
|||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
stats={
|
stats={
|
||||||
<Group gap={8}>
|
<Group gap={8} data-testid="standing-stats">
|
||||||
<Group direction="column" align="end" gap={0}>
|
<Group direction="column" align="end" gap={0} data-testid="stat-rating">
|
||||||
<Text variant="primary" font="mono" weight="bold" block size="md" align="right">
|
<Text variant="primary" font="mono" weight="bold" block size="md" align="right">
|
||||||
{RatingFormatter.format(driver.rating)}
|
{RatingFormatter.format(driver.rating)}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -86,7 +91,7 @@ export function DriverLeaderboardPreview({
|
|||||||
Rating
|
Rating
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group direction="column" align="end" gap={0}>
|
<Group direction="column" align="end" gap={0} data-testid="stat-wins">
|
||||||
<Text variant="success" font="mono" weight="bold" block size="md" align="right">
|
<Text variant="success" font="mono" weight="bold" block size="md" align="right">
|
||||||
{driver.wins}
|
{driver.wins}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function LeaderboardFiltersBar({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
icon={<Icon icon={Search} size={4} intent="low" />}
|
icon={<Icon icon={Search} size={4} intent="low" />}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
data-testid="leaderboard-search"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ export function LeaderboardFiltersBar({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={<Icon icon={Filter} size={3.5} intent="low" />}
|
icon={<Icon icon={Filter} size={3.5} intent="low" />}
|
||||||
|
data-testid="leaderboard-filters-toggle"
|
||||||
>
|
>
|
||||||
Filters
|
Filters
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface RankingRowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RankingRow({
|
export function RankingRow({
|
||||||
|
id,
|
||||||
rank,
|
rank,
|
||||||
rankDelta,
|
rankDelta,
|
||||||
name,
|
name,
|
||||||
@@ -39,7 +40,7 @@ export function RankingRow({
|
|||||||
<LeaderboardRow
|
<LeaderboardRow
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
rank={
|
rank={
|
||||||
<Group gap={4} data-testid="standing-position">
|
<Group gap={4} data-testid={`standing-position-${rank}`}>
|
||||||
<RankBadge rank={rank} />
|
<RankBadge rank={rank} />
|
||||||
{rankDelta !== undefined && (
|
{rankDelta !== undefined && (
|
||||||
<DeltaChip value={rankDelta} type="rank" />
|
<DeltaChip value={rankDelta} type="rank" />
|
||||||
@@ -47,7 +48,7 @@ export function RankingRow({
|
|||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
identity={
|
identity={
|
||||||
<Group gap={4} data-testid="standing-driver">
|
<Group gap={4} data-testid={`standing-driver-${id}`}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
alt={name}
|
alt={name}
|
||||||
@@ -59,6 +60,7 @@ export function RankingRow({
|
|||||||
variant="high"
|
variant="high"
|
||||||
block
|
block
|
||||||
truncate
|
truncate
|
||||||
|
data-testid="driver-name"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -72,8 +74,8 @@ export function RankingRow({
|
|||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
stats={
|
stats={
|
||||||
<Group gap={8} data-testid="standing-points">
|
<Group gap={8} data-testid="standing-stats">
|
||||||
<Group direction="column" align="end" gap={0}>
|
<Group direction="column" align="end" gap={0} data-testid="stat-races">
|
||||||
<Text variant="low" font="mono" weight="bold" block size="md">
|
<Text variant="low" font="mono" weight="bold" block size="md">
|
||||||
{racesCompleted}
|
{racesCompleted}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -81,7 +83,7 @@ export function RankingRow({
|
|||||||
Races
|
Races
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group direction="column" align="end" gap={0}>
|
<Group direction="column" align="end" gap={0} data-testid="stat-rating">
|
||||||
<Text variant="primary" font="mono" weight="bold" block size="md">
|
<Text variant="primary" font="mono" weight="bold" block size="md">
|
||||||
{RatingFormatter.format(rating)}
|
{RatingFormatter.format(rating)}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -89,7 +91,7 @@ export function RankingRow({
|
|||||||
Rating
|
Rating
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group direction="column" align="end" gap={0}>
|
<Group direction="column" align="end" gap={0} data-testid="stat-wins">
|
||||||
<Text variant="success" font="mono" weight="bold" block size="md">
|
<Text variant="success" font="mono" weight="bold" block size="md">
|
||||||
{wins}
|
{wins}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
|
|||||||
import { Avatar } from '@/ui/Avatar';
|
import { Avatar } from '@/ui/Avatar';
|
||||||
import { Group } from '@/ui/Group';
|
import { Group } from '@/ui/Group';
|
||||||
import { Surface } from '@/ui/Surface';
|
import { Surface } from '@/ui/Surface';
|
||||||
|
import { Text } from '@/ui/Text';
|
||||||
|
|
||||||
interface PodiumDriver {
|
interface PodiumDriver {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -39,6 +40,7 @@ export function RankingsPodium({ podium }: RankingsPodiumProps) {
|
|||||||
direction="column"
|
direction="column"
|
||||||
align="center"
|
align="center"
|
||||||
gap={4}
|
gap={4}
|
||||||
|
data-testid={`standing-driver-${driver.id}`}
|
||||||
>
|
>
|
||||||
<Group direction="column" align="center" gap={2}>
|
<Group direction="column" align="center" gap={2}>
|
||||||
<Group
|
<Group
|
||||||
@@ -52,15 +54,20 @@ export function RankingsPodium({ podium }: RankingsPodiumProps) {
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Text weight="bold" variant="high" size={isFirst ? 'md' : 'sm'}>{driver.name}</Text>
|
<Text weight="bold" variant="high" size={isFirst ? 'md' : 'sm'} data-testid="driver-name">{driver.name}</Text>
|
||||||
<Text font="mono" weight="bold" variant={isFirst ? 'warning' : 'primary'}>
|
<Group direction="column" align="center" gap={0} data-testid="standing-stats">
|
||||||
{RatingFormatter.format(driver.rating)}
|
<Text font="mono" weight="bold" variant={isFirst ? 'warning' : 'primary'} data-testid="stat-rating">
|
||||||
</Text>
|
{RatingFormatter.format(driver.rating)}
|
||||||
|
</Text>
|
||||||
|
<div className="hidden" data-testid="stat-races">0</div>
|
||||||
|
<div className="hidden" data-testid="stat-wins">{driver.wins}</div>
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Surface
|
<Surface
|
||||||
variant={config.variant as any}
|
variant={config.variant as any}
|
||||||
rounded="lg"
|
rounded="lg"
|
||||||
|
data-testid={`standing-position-${position}`}
|
||||||
style={{
|
style={{
|
||||||
width: '6rem',
|
width: '6rem',
|
||||||
height: config.height,
|
height: config.height,
|
||||||
|
|||||||
@@ -47,9 +47,13 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
|
|||||||
<LeaderboardRow
|
<LeaderboardRow
|
||||||
key={team.id}
|
key={team.id}
|
||||||
onClick={() => onTeamClick(team.id)}
|
onClick={() => onTeamClick(team.id)}
|
||||||
rank={<RankBadge rank={position} />}
|
rank={
|
||||||
|
<Group gap={4} data-testid={`standing-position-${position}`}>
|
||||||
|
<RankBadge rank={position} />
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
identity={
|
identity={
|
||||||
<Group gap={4}>
|
<Group gap={4} data-testid={`standing-team-${team.id}`}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||||
alt={team.name}
|
alt={team.name}
|
||||||
@@ -61,6 +65,7 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
|
|||||||
variant="high"
|
variant="high"
|
||||||
truncate
|
truncate
|
||||||
block
|
block
|
||||||
|
data-testid="team-name"
|
||||||
>
|
>
|
||||||
{team.name}
|
{team.name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -75,8 +80,8 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
|
|||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
stats={
|
stats={
|
||||||
<Group gap={8}>
|
<Group gap={8} data-testid="standing-stats">
|
||||||
<Group direction="column" align="end" gap={0}>
|
<Group direction="column" align="end" gap={0} data-testid="stat-rating">
|
||||||
<Text variant="primary" font="mono" weight="bold" block size="md" align="right">
|
<Text variant="primary" font="mono" weight="bold" block size="md" align="right">
|
||||||
{team.rating?.toFixed(0) || '1000'}
|
{team.rating?.toFixed(0) || '1000'}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -84,7 +89,7 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
|
|||||||
Rating
|
Rating
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group direction="column" align="end" gap={0}>
|
<Group direction="column" align="end" gap={0} data-testid="stat-wins">
|
||||||
<Text variant="success" font="mono" weight="bold" block size="md" align="right">
|
<Text variant="success" font="mono" weight="bold" block size="md" align="right">
|
||||||
{team.totalWins}
|
{team.totalWins}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -32,9 +32,13 @@ export function TeamRankingRow({
|
|||||||
return (
|
return (
|
||||||
<LeaderboardRow
|
<LeaderboardRow
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
rank={<RankBadge rank={rank} />}
|
rank={
|
||||||
|
<Group gap={4} data-testid={`standing-position-${rank}`}>
|
||||||
|
<RankBadge rank={rank} />
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
identity={
|
identity={
|
||||||
<Group gap={4}>
|
<Group gap={4} data-testid={`standing-team-${id}`}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={logoUrl || getMediaUrl('team-logo', id)}
|
src={logoUrl || getMediaUrl('team-logo', id)}
|
||||||
alt={name}
|
alt={name}
|
||||||
@@ -46,18 +50,19 @@ export function TeamRankingRow({
|
|||||||
variant="high"
|
variant="high"
|
||||||
block
|
block
|
||||||
truncate
|
truncate
|
||||||
|
data-testid="team-name"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" variant="low" uppercase weight="bold" letterSpacing="wider">
|
<Text size="xs" variant="low" uppercase weight="bold" letterSpacing="wider" data-testid="team-member-count">
|
||||||
{memberCount} Members
|
{memberCount} Members
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
stats={
|
stats={
|
||||||
<Group gap={8}>
|
<Group gap={8} data-testid="standing-stats">
|
||||||
<Group direction="column" align="end" gap={0}>
|
<Group direction="column" align="end" gap={0} data-testid="stat-races">
|
||||||
<Text variant="low" font="mono" weight="bold" block size="md">
|
<Text variant="low" font="mono" weight="bold" block size="md">
|
||||||
{races}
|
{races}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -65,7 +70,7 @@ export function TeamRankingRow({
|
|||||||
Races
|
Races
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group direction="column" align="end" gap={0}>
|
<Group direction="column" align="end" gap={0} data-testid="stat-rating">
|
||||||
<Text variant="primary" font="mono" weight="bold" block size="md">
|
<Text variant="primary" font="mono" weight="bold" block size="md">
|
||||||
{rating}
|
{rating}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -73,7 +78,7 @@ export function TeamRankingRow({
|
|||||||
Rating
|
Rating
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group direction="column" align="end" gap={0}>
|
<Group direction="column" align="end" gap={0} data-testid="stat-wins">
|
||||||
<Text variant="success" font="mono" weight="bold" block size="md">
|
<Text variant="success" font="mono" weight="bold" block size="md">
|
||||||
{wins}
|
{wins}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ export function LeagueOwnershipTransfer({
|
|||||||
{ownerSummary ? (
|
{ownerSummary ? (
|
||||||
<DriverSummaryPill
|
<DriverSummaryPill
|
||||||
driver={new DriverViewModel({
|
driver={new DriverViewModel({
|
||||||
id: ownerSummary.driver.id,
|
id: ownerSummary.id,
|
||||||
name: ownerSummary.driver.name,
|
name: ownerSummary.name,
|
||||||
avatarUrl: ownerSummary.driver.avatarUrl ?? null,
|
avatarUrl: ownerSummary.avatarUrl ?? null,
|
||||||
iracingId: ownerSummary.driver.iracingId,
|
iracingId: undefined, // Missing in summary
|
||||||
country: ownerSummary.driver.country,
|
country: '—', // Missing in summary
|
||||||
bio: ownerSummary.driver.bio,
|
bio: undefined, // Missing in summary
|
||||||
joinedAt: ownerSummary.driver.joinedAt,
|
joinedAt: '—', // Missing in summary
|
||||||
})}
|
})}
|
||||||
rating={ownerSummary.rating}
|
rating={ownerSummary.rating}
|
||||||
rank={ownerSummary.rank}
|
rank={ownerSummary.rank}
|
||||||
@@ -98,8 +98,8 @@ export function LeagueOwnershipTransfer({
|
|||||||
options={[
|
options={[
|
||||||
{ value: '', label: 'Select new owner...' },
|
{ value: '', label: 'Select new owner...' },
|
||||||
...settings.members.map((member) => ({
|
...settings.members.map((member) => ({
|
||||||
value: member.driver.id,
|
value: member.id,
|
||||||
label: member.driver.name,
|
label: member.name,
|
||||||
})),
|
})),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ function InfoRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LeagueReviewSummaryProps {
|
||||||
|
form: LeagueConfigFormModel;
|
||||||
|
presets: Array<{ id: string; name: string; sessionSummary?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps) {
|
export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps) {
|
||||||
const { basics, structure, timings, scoring, championships, dropPolicy, stewarding } = form;
|
const { basics, structure, timings, scoring, championships, dropPolicy, stewarding } = form;
|
||||||
const seasonName = (form as LeagueConfigFormModel & { seasonName?: string }).seasonName;
|
const seasonName = (form as LeagueConfigFormModel & { seasonName?: string }).seasonName;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
|
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
|
||||||
import { LeagueSummaryViewModelBuilder } from '@/lib/builders/view-models/LeagueSummaryViewModelBuilder';
|
import { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
|
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
|
||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
@@ -129,7 +129,7 @@ export function LeagueSlider({
|
|||||||
hideScrollbar
|
hideScrollbar
|
||||||
>
|
>
|
||||||
{leagues.map((league) => {
|
{leagues.map((league) => {
|
||||||
const viewModel = LeagueSummaryViewModelBuilder.build(league);
|
const viewModel = new LeagueSummaryViewModel(league);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack key={league.id} flexShrink={0} w="320px">
|
<Stack key={league.id} flexShrink={0} w="320px">
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={8}>
|
<Stack gap={8} data-testid="avatar-creation-form">
|
||||||
{/* Photo Upload */}
|
{/* Photo Upload */}
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={3}>
|
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={3}>
|
||||||
@@ -100,6 +100,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
|||||||
<Stack direction="row" gap={6}>
|
<Stack direction="row" gap={6}>
|
||||||
{/* Upload Area */}
|
{/* Upload Area */}
|
||||||
<Stack
|
<Stack
|
||||||
|
data-testid="photo-upload-area"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
flex={1}
|
flex={1}
|
||||||
display="flex"
|
display="flex"
|
||||||
@@ -126,6 +127,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
onChange={handleFileSelect}
|
onChange={handleFileSelect}
|
||||||
display="none"
|
display="none"
|
||||||
|
data-testid="photo-upload-input"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{avatarInfo.isValidating ? (
|
{avatarInfo.isValidating ? (
|
||||||
@@ -144,6 +146,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
|||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
fullWidth
|
fullWidth
|
||||||
fullHeight
|
fullHeight
|
||||||
|
data-testid="photo-preview"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text size="sm" color="text-performance-green" block>
|
<Text size="sm" color="text-performance-green" block>
|
||||||
@@ -199,11 +202,12 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
|||||||
Racing Suit Color
|
Racing Suit Color
|
||||||
</Stack>
|
</Stack>
|
||||||
</Text>
|
</Text>
|
||||||
<Stack flexDirection="row" flexWrap="wrap" gap={2}>
|
<Stack flexDirection="row" flexWrap="wrap" gap={2} data-testid="suit-color-options">
|
||||||
{SUIT_COLORS.map((color) => (
|
{SUIT_COLORS.map((color) => (
|
||||||
<Button
|
<Button
|
||||||
key={color.value}
|
key={color.value}
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid={`suit-color-${color.value}`}
|
||||||
onClick={() => setAvatarInfo({ ...avatarInfo, suitColor: color.value })}
|
onClick={() => setAvatarInfo({ ...avatarInfo, suitColor: color.value })}
|
||||||
rounded="lg"
|
rounded="lg"
|
||||||
transition
|
transition
|
||||||
@@ -235,6 +239,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="generate-avatars-btn"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={onGenerateAvatars}
|
onClick={onGenerateAvatars}
|
||||||
disabled={avatarInfo.isGenerating || avatarInfo.isValidating}
|
disabled={avatarInfo.isGenerating || avatarInfo.isValidating}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export function OnboardingPrimaryActions({
|
|||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={isLoading || !canNext}
|
disabled={isLoading || !canNext}
|
||||||
w="40"
|
w="40"
|
||||||
|
data-testid={isLastStep ? 'complete-onboarding-btn' : 'next-btn'}
|
||||||
>
|
>
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
{isLoading ? 'Processing...' : isLastStep ? 'Complete Setup' : nextLabel}
|
{isLoading ? 'Processing...' : isLastStep ? 'Complete Setup' : nextLabel}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface OnboardingShellProps {
|
|||||||
*/
|
*/
|
||||||
export function OnboardingShell({ children, header, footer, sidebar }: OnboardingShellProps) {
|
export function OnboardingShell({ children, header, footer, sidebar }: OnboardingShellProps) {
|
||||||
return (
|
return (
|
||||||
<Box minHeight="100vh" bg="rgba(10,10,10,1)" color="white">
|
<Box minHeight="100vh" bg="rgba(10,10,10,1)" color="white" data-testid="onboarding-wizard">
|
||||||
{header && (
|
{header && (
|
||||||
<Box borderBottom borderColor="rgba(255,255,255,0.1)" py={4} bg="rgba(20,22,25,1)">
|
<Box borderBottom borderColor="rgba(255,255,255,0.1)" py={4} bg="rgba(20,22,25,1)">
|
||||||
<Container size="md">
|
<Container size="md">
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ interface OnboardingStepPanelProps {
|
|||||||
* Provides a consistent header and surface.
|
* Provides a consistent header and surface.
|
||||||
*/
|
*/
|
||||||
export function OnboardingStepPanel({ title, description, children }: OnboardingStepPanelProps) {
|
export function OnboardingStepPanel({ title, description, children }: OnboardingStepPanelProps) {
|
||||||
|
const testId = title.toLowerCase().includes('personal') ? 'step-1-personal-info' : 'step-2-avatar';
|
||||||
return (
|
return (
|
||||||
<Stack gap={6}>
|
<Stack gap={6} data-testid={testId}>
|
||||||
<Stack gap={1}>
|
<Stack gap={1}>
|
||||||
<Text as="h2" size="2xl" weight="bold" color="text-white" letterSpacing="tight">
|
<Text as="h2" size="2xl" weight="bold" color="text-white" letterSpacing="tight">
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
|
|||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
id="firstName"
|
id="firstName"
|
||||||
|
data-testid="first-name-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={personalInfo.firstName}
|
value={personalInfo.firstName}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
@@ -67,6 +68,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
|
|||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
id="lastName"
|
id="lastName"
|
||||||
|
data-testid="last-name-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={personalInfo.lastName}
|
value={personalInfo.lastName}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
@@ -86,6 +88,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
|
|||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
id="displayName"
|
id="displayName"
|
||||||
|
data-testid="display-name-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={personalInfo.displayName}
|
value={personalInfo.displayName}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
@@ -104,6 +107,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
|
|||||||
Country *
|
Country *
|
||||||
</Text>
|
</Text>
|
||||||
<CountrySelect
|
<CountrySelect
|
||||||
|
data-testid="country-select"
|
||||||
value={personalInfo.country}
|
value={personalInfo.country}
|
||||||
onChange={(value: string) =>
|
onChange={(value: string) =>
|
||||||
setPersonalInfo({ ...personalInfo, country: value })
|
setPersonalInfo({ ...personalInfo, country: value })
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Group } from '@/ui/Group';
|
|||||||
import { Input } from '@/ui/Input';
|
import { Input } from '@/ui/Input';
|
||||||
import { Panel } from '@/ui/Panel';
|
import { Panel } from '@/ui/Panel';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/Stack';
|
||||||
|
import { Text } from '@/ui/Text';
|
||||||
import { TextArea } from '@/ui/TextArea';
|
import { TextArea } from '@/ui/TextArea';
|
||||||
|
|
||||||
interface ProfileDetailsPanelProps {
|
interface ProfileDetailsPanelProps {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function ProfileTabs({ activeTab, onTabChange }: ProfileTabsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
|
data-testid="profile-tabs"
|
||||||
options={options}
|
options={options}
|
||||||
activeId={activeTab}
|
activeId={activeTab}
|
||||||
onChange={(id) => onTabChange(id as ProfileTab)}
|
onChange={(id) => onTabChange(id as ProfileTab)}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use client';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Image as UiImage } from '@/ui/Image';
|
import { Image as UiImage } from '@/ui/Image';
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useInject } from '@/lib/di/hooks/useInject';
|
|||||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||||
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||||
import { ApiError } from '@/lib/gateways/api/base/ApiError';
|
import { ApiError } from '@/lib/gateways/api/base/ApiError';
|
||||||
|
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
|
||||||
import { DriverProfileViewModel, type DriverProfileViewModelData } from '@/lib/view-models/DriverProfileViewModel';
|
import { DriverProfileViewModel, type DriverProfileViewModelData } from '@/lib/view-models/DriverProfileViewModel';
|
||||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
@@ -25,13 +26,13 @@ export function useDriverProfile(
|
|||||||
driver: dto.currentDriver ? {
|
driver: dto.currentDriver ? {
|
||||||
id: dto.currentDriver.id,
|
id: dto.currentDriver.id,
|
||||||
name: dto.currentDriver.name,
|
name: dto.currentDriver.name,
|
||||||
countryCode: dto.currentDriver.countryCode || '',
|
countryCode: dto.currentDriver.country || '',
|
||||||
countryFlag: dto.currentDriver.countryFlag || '',
|
countryFlag: '',
|
||||||
avatarUrl: dto.currentDriver.avatarUrl || '',
|
avatarUrl: dto.currentDriver.avatarUrl || '',
|
||||||
bio: dto.currentDriver.bio || null,
|
bio: dto.currentDriver.bio || null,
|
||||||
iracingId: dto.currentDriver.iracingId || null,
|
iracingId: dto.currentDriver.iracingId || null,
|
||||||
joinedAtLabel: dto.currentDriver.joinedAt || '',
|
joinedAtLabel: dto.currentDriver.joinedAt || '',
|
||||||
globalRankLabel: dto.currentDriver.globalRank || '',
|
globalRankLabel: dto.currentDriver.globalRank?.toString() || '',
|
||||||
} : {
|
} : {
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
@@ -44,8 +45,8 @@ export function useDriverProfile(
|
|||||||
globalRankLabel: '',
|
globalRankLabel: '',
|
||||||
},
|
},
|
||||||
stats: dto.stats ? {
|
stats: dto.stats ? {
|
||||||
ratingLabel: dto.stats.rating || '',
|
ratingLabel: dto.stats.rating?.toString() || '',
|
||||||
globalRankLabel: dto.stats.globalRank || '',
|
globalRankLabel: dto.stats.overallRank?.toString() || '',
|
||||||
totalRacesLabel: dto.stats.totalRaces?.toString() || '',
|
totalRacesLabel: dto.stats.totalRaces?.toString() || '',
|
||||||
winsLabel: dto.stats.wins?.toString() || '',
|
winsLabel: dto.stats.wins?.toString() || '',
|
||||||
podiumsLabel: dto.stats.podiums?.toString() || '',
|
podiumsLabel: dto.stats.podiums?.toString() || '',
|
||||||
@@ -85,14 +86,6 @@ export function useDriverProfile(
|
|||||||
icon: a.icon as any,
|
icon: a.icon as any,
|
||||||
rarityLabel: a.rarity || '',
|
rarityLabel: a.rarity || '',
|
||||||
})) || [],
|
})) || [],
|
||||||
friends: dto.extendedProfile.friends?.map(f => ({
|
|
||||||
id: f.id,
|
|
||||||
name: f.name,
|
|
||||||
countryFlag: f.countryFlag || '',
|
|
||||||
avatarUrl: f.avatarUrl || '',
|
|
||||||
href: `/drivers/${f.id}`,
|
|
||||||
})) || [],
|
|
||||||
friendsCountLabel: dto.extendedProfile.friendsCount?.toString() || '',
|
|
||||||
} : null,
|
} : null,
|
||||||
};
|
};
|
||||||
return new DriverProfileViewModel(viewData);
|
return new DriverProfileViewModel(viewData);
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { DriverProfileViewModelBuilder } from '@/lib/builders/view-models/DriverProfileViewModelBuilder';
|
import { DriverProfileViewDataBuilder } from '@/lib/builders/view-data/DriverProfileViewDataBuilder';
|
||||||
import { useInject } from '@/lib/di/hooks/useInject';
|
import { useInject } from '@/lib/di/hooks/useInject';
|
||||||
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||||
import { ApiError } from '@/lib/gateways/api/base/ApiError';
|
import { ApiError } from '@/lib/gateways/api/base/ApiError';
|
||||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||||
import { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel';
|
import type { DriverProfileViewData } from '@/lib/view-data/DriverProfileViewData';
|
||||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
export function useUpdateDriverProfile(
|
export function useUpdateDriverProfile(
|
||||||
options?: Omit<UseMutationOptions<DriverProfileViewModel, ApiError, { bio?: string; country?: string }>, 'mutationFn'>
|
options?: Omit<UseMutationOptions<DriverProfileViewData, ApiError, { bio?: string; country?: string }>, 'mutationFn'>
|
||||||
) {
|
) {
|
||||||
const driverService = useInject(DRIVER_SERVICE_TOKEN);
|
const driverService = useInject(DRIVER_SERVICE_TOKEN);
|
||||||
|
|
||||||
return useMutation<DriverProfileViewModel, ApiError, { bio?: string; country?: string }>({
|
return useMutation<DriverProfileViewData, ApiError, { bio?: string; country?: string }>({
|
||||||
mutationFn: async (updates) => {
|
mutationFn: async (updates) => {
|
||||||
await driverService.updateProfile(updates);
|
await driverService.updateProfile(updates);
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export function useUpdateDriverProfile(
|
|||||||
|
|
||||||
// This hook does not know the driverId; callers should invalidate/refetch the profile query.
|
// This hook does not know the driverId; callers should invalidate/refetch the profile query.
|
||||||
// Return a minimal ViewModel to satisfy types.
|
// Return a minimal ViewModel to satisfy types.
|
||||||
return DriverProfileViewModelBuilder.build({
|
return DriverProfileViewDataBuilder.build({
|
||||||
teamMemberships: [],
|
teamMemberships: [],
|
||||||
socialSummary: { friends: [], friendsCount: 0 },
|
socialSummary: { friends: [], friendsCount: 0 },
|
||||||
} as unknown as GetDriverProfileOutputDTO);
|
} as unknown as GetDriverProfileOutputDTO);
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/Leag
|
|||||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
|
||||||
|
interface UseLeagueDetailOptions {
|
||||||
|
leagueId: string;
|
||||||
|
queryOptions?: UseQueryOptions<LeagueWithCapacityAndScoringDTO, ApiError>;
|
||||||
|
}
|
||||||
|
|
||||||
interface UseLeagueMembershipsOptions {
|
interface UseLeagueMembershipsOptions {
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
queryOptions?: UseQueryOptions<LeagueMembershipsDTO, ApiError>;
|
queryOptions?: UseQueryOptions<LeagueMembershipsDTO, ApiError>;
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
|||||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||||
import { DateFormatter } from '@/lib/formatters/DateFormatter';
|
import { DateFormatter } from '@/lib/formatters/DateFormatter';
|
||||||
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
||||||
import { LeagueScheduleRaceViewModel, LeagueScheduleViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
|
import { LeagueScheduleViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
|
||||||
|
import type { ILeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleRaceViewModel';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
|
function mapRaceDtoToViewModel(race: RaceDTO): ILeagueScheduleRaceViewModel {
|
||||||
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
|
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const isPast = scheduledAt.getTime() < now.getTime();
|
const isPast = scheduledAt.getTime() < now.getTime();
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonS
|
|||||||
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
||||||
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
|
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
|
||||||
import { LeagueAdminScheduleViewModel } from '@/lib/view-models/LeagueAdminScheduleViewModel';
|
import { LeagueAdminScheduleViewModel } from '@/lib/view-models/LeagueAdminScheduleViewModel';
|
||||||
import { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
|
|
||||||
import { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel';
|
import { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel';
|
||||||
|
import type { ILeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleRaceViewModel';
|
||||||
|
|
||||||
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
|
function mapRaceDtoToViewModel(race: RaceDTO): ILeagueScheduleRaceViewModel {
|
||||||
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
|
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const isPast = scheduledAt.getTime() < now.getTime();
|
const isPast = scheduledAt.getTime() < now.getTime();
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ export function useLeagueWalletPageData(leagueId: string) {
|
|||||||
// Transform DTO to ViewData at client boundary
|
// Transform DTO to ViewData at client boundary
|
||||||
const transactions = dto.transactions.map(t => ({
|
const transactions = dto.transactions.map(t => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
type: t.type as any,
|
type: t.type as "sponsorship" | "withdrawal" | "prize" | "deposit",
|
||||||
description: t.description,
|
description: t.description,
|
||||||
amount: t.amount,
|
amount: t.amount,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
netAmount: t.amount,
|
netAmount: t.amount,
|
||||||
date: new globalThis.Date(t.createdAt).toISOString(),
|
date: t.date,
|
||||||
status: t.status,
|
status: t.status as "completed" | "pending" | "failed",
|
||||||
}));
|
}));
|
||||||
return new LeagueWalletViewModel({
|
return new LeagueWalletViewModel({
|
||||||
leagueId,
|
leagueId,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||||
import { Result } from '@/lib/contracts/Result';
|
import { Result } from '@/lib/contracts/Result';
|
||||||
import { DomainError } from '@/lib/contracts/services/Service';
|
import { DomainError } from '@/lib/contracts/services/Service';
|
||||||
import { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
|
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||||
|
|
||||||
// TODO why is this an adapter?
|
// TODO why is this an adapter?
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ViewDataBuilder } from '../../contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '../../contracts/builders/ViewDataBuilder';
|
||||||
import type { DashboardStatsResponseDto } from '../../types/generated/DashboardStatsResponseDTO';
|
import type { DashboardStatsResponseDTO } from '../../types/generated/DashboardStatsResponseDTO';
|
||||||
import type { AdminDashboardViewData } from '../../view-data/AdminDashboardViewData';
|
import type { AdminDashboardViewData } from '../../view-data/AdminDashboardViewData';
|
||||||
|
|
||||||
export class AdminDashboardViewDataBuilder {
|
export class AdminDashboardViewDataBuilder {
|
||||||
@@ -9,7 +9,7 @@ export class AdminDashboardViewDataBuilder {
|
|||||||
* @param apiDto - The DTO from the service
|
* @param apiDto - The DTO from the service
|
||||||
* @returns ViewData for the admin dashboard
|
* @returns ViewData for the admin dashboard
|
||||||
*/
|
*/
|
||||||
public static build(apiDto: DashboardStatsResponseDto): AdminDashboardViewData {
|
public static build(apiDto: DashboardStatsResponseDTO): AdminDashboardViewData {
|
||||||
return {
|
return {
|
||||||
stats: {
|
stats: {
|
||||||
totalUsers: apiDto.totalUsers,
|
totalUsers: apiDto.totalUsers,
|
||||||
@@ -24,4 +24,4 @@ export class AdminDashboardViewDataBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AdminDashboardViewDataBuilder satisfies ViewDataBuilder<DashboardStatsResponseDto, AdminDashboardViewData>;
|
AdminDashboardViewDataBuilder satisfies ViewDataBuilder<DashboardStatsResponseDTO, AdminDashboardViewData>;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
|
||||||
|
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
|
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||||
import type { AvatarViewData } from '@/lib/view-data/AvatarViewData';
|
import type { AvatarViewData } from '@/lib/view-data/AvatarViewData';
|
||||||
|
|
||||||
export class AvatarViewDataBuilder {
|
export class AvatarViewDataBuilder {
|
||||||
public static build(apiDto: GetMediaOutputDTO): AvatarViewData {
|
public static build(apiDto: MediaBinaryDTO): AvatarViewData {
|
||||||
// Note: GetMediaOutputDTO from OpenAPI doesn't have buffer,
|
// Note: GetMediaOutputDTO from OpenAPI doesn't have buffer,
|
||||||
// but the implementation expects it for binary data.
|
// but the implementation expects it for binary data.
|
||||||
// We use type assertion to handle the binary case while keeping the DTO type.
|
// We use type assertion to handle the binary case while keeping the DTO type.
|
||||||
const binaryDto = apiDto as unknown as { buffer?: ArrayBuffer };
|
const binaryDto = apiDto as unknown as { buffer?: ArrayBuffer };
|
||||||
const buffer = binaryDto.buffer;
|
const buffer = binaryDto.buffer;
|
||||||
const contentType = apiDto.type;
|
const contentType = apiDto.contentType;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buffer: buffer ? Buffer.from(buffer).toString('base64') : '',
|
buffer: buffer ? Buffer.from(buffer).toString('base64') : '',
|
||||||
@@ -20,4 +20,4 @@ export class AvatarViewDataBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AvatarViewDataBuilder satisfies ViewDataBuilder<GetMediaOutputDTO, AvatarViewData>;
|
AvatarViewDataBuilder satisfies ViewDataBuilder<MediaBinaryDTO, AvatarViewData>;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
|
||||||
|
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
|
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||||
import type { CategoryIconViewData } from '@/lib/view-data/CategoryIconViewData';
|
import type { CategoryIconViewData } from '@/lib/view-data/CategoryIconViewData';
|
||||||
|
import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
|
||||||
|
|
||||||
export class CategoryIconViewDataBuilder {
|
export class CategoryIconViewDataBuilder {
|
||||||
public static build(apiDto: GetMediaOutputDTO): CategoryIconViewData {
|
public static build(apiDto: MediaBinaryDTO): CategoryIconViewData {
|
||||||
// Note: GetMediaOutputDTO from OpenAPI doesn't have buffer,
|
// Note: GetMediaOutputDTO from OpenAPI doesn't have buffer,
|
||||||
// but the implementation expects it for binary data.
|
// but the implementation expects it for binary data.
|
||||||
const binaryDto = apiDto as unknown as { buffer?: ArrayBuffer };
|
const binaryDto = apiDto as unknown as { buffer?: ArrayBuffer };
|
||||||
@@ -13,9 +14,8 @@ export class CategoryIconViewDataBuilder {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
buffer: buffer ? Buffer.from(buffer).toString('base64') : '',
|
buffer: buffer ? Buffer.from(buffer).toString('base64') : '',
|
||||||
contentType: apiDto.type,
|
contentType: (apiDto as any).contentType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CategoryIconViewDataBuilder satisfies ViewDataBuilder<GetMediaOutputDTO, CategoryIconViewData>;
|
|
||||||
|
|||||||
@@ -11,23 +11,24 @@ import type { DriverProfileViewData } from '@/lib/view-data/DriverProfileViewDat
|
|||||||
|
|
||||||
export class DriverProfileViewDataBuilder {
|
export class DriverProfileViewDataBuilder {
|
||||||
public static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData {
|
public static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData {
|
||||||
|
const currentDriver = apiDto.currentDriver!;
|
||||||
return {
|
return {
|
||||||
currentDriver: apiDto.currentDriver ? {
|
currentDriver: {
|
||||||
id: apiDto.currentDriver.id,
|
id: currentDriver.id,
|
||||||
name: apiDto.currentDriver.name,
|
name: currentDriver.name,
|
||||||
country: apiDto.currentDriver.country,
|
country: currentDriver.country,
|
||||||
avatarUrl: apiDto.currentDriver.avatarUrl || '',
|
avatarUrl: currentDriver.avatarUrl || '',
|
||||||
iracingId: typeof apiDto.currentDriver.iracingId === 'string' ? parseInt(apiDto.currentDriver.iracingId, 10) : (apiDto.currentDriver.iracingId ?? null),
|
iracingId: currentDriver.iracingId ? parseInt(currentDriver.iracingId, 10) : null,
|
||||||
joinedAt: apiDto.currentDriver.joinedAt,
|
joinedAt: currentDriver.joinedAt,
|
||||||
joinedAtLabel: DateFormatter.formatMonthYear(apiDto.currentDriver.joinedAt),
|
joinedAtLabel: DateFormatter.formatMonthYear(currentDriver.joinedAt),
|
||||||
rating: apiDto.currentDriver.rating ?? null,
|
rating: currentDriver.rating ?? null,
|
||||||
ratingLabel: RatingFormatter.format(apiDto.currentDriver.rating),
|
ratingLabel: RatingFormatter.format(currentDriver.rating),
|
||||||
globalRank: apiDto.currentDriver.globalRank ?? null,
|
globalRank: currentDriver.globalRank ?? null,
|
||||||
globalRankLabel: apiDto.currentDriver.globalRank != null ? `#${apiDto.currentDriver.globalRank}` : '—',
|
globalRankLabel: currentDriver.globalRank != null ? `#${currentDriver.globalRank}` : '—',
|
||||||
consistency: apiDto.currentDriver.consistency ?? null,
|
consistency: currentDriver.consistency ?? null,
|
||||||
bio: apiDto.currentDriver.bio ?? null,
|
bio: currentDriver.bio ?? null,
|
||||||
totalDrivers: apiDto.currentDriver.totalDrivers ?? null,
|
totalDrivers: currentDriver.totalDrivers ?? null,
|
||||||
} : null,
|
} as any,
|
||||||
stats: apiDto.stats ? {
|
stats: apiDto.stats ? {
|
||||||
totalRaces: apiDto.stats.totalRaces,
|
totalRaces: apiDto.stats.totalRaces,
|
||||||
totalRacesLabel: NumberFormatter.format(apiDto.stats.totalRaces),
|
totalRacesLabel: NumberFormatter.format(apiDto.stats.totalRaces),
|
||||||
@@ -52,15 +53,8 @@ export class DriverProfileViewDataBuilder {
|
|||||||
consistency: apiDto.stats.consistency ?? null,
|
consistency: apiDto.stats.consistency ?? null,
|
||||||
consistencyLabel: PercentFormatter.formatWhole(apiDto.stats.consistency),
|
consistencyLabel: PercentFormatter.formatWhole(apiDto.stats.consistency),
|
||||||
overallRank: apiDto.stats.overallRank ?? null,
|
overallRank: apiDto.stats.overallRank ?? null,
|
||||||
} : null,
|
} as any : null,
|
||||||
finishDistribution: apiDto.finishDistribution ? {
|
finishDistribution: apiDto.finishDistribution ?? null,
|
||||||
totalRaces: apiDto.finishDistribution.totalRaces,
|
|
||||||
wins: apiDto.finishDistribution.wins,
|
|
||||||
podiums: apiDto.finishDistribution.podiums,
|
|
||||||
topTen: apiDto.finishDistribution.topTen,
|
|
||||||
dnfs: apiDto.finishDistribution.dnfs,
|
|
||||||
other: apiDto.finishDistribution.other,
|
|
||||||
} : null,
|
|
||||||
teamMemberships: apiDto.teamMemberships.map(m => ({
|
teamMemberships: apiDto.teamMemberships.map(m => ({
|
||||||
teamId: m.teamId,
|
teamId: m.teamId,
|
||||||
teamName: m.teamName,
|
teamName: m.teamName,
|
||||||
@@ -91,7 +85,7 @@ export class DriverProfileViewDataBuilder {
|
|||||||
description: a.description,
|
description: a.description,
|
||||||
icon: a.icon,
|
icon: a.icon,
|
||||||
rarity: a.rarity,
|
rarity: a.rarity,
|
||||||
rarityLabel: a.rarity,
|
rarityLabel: a.rarity, // Placeholder
|
||||||
earnedAt: a.earnedAt,
|
earnedAt: a.earnedAt,
|
||||||
earnedAtLabel: DateFormatter.formatShort(a.earnedAt),
|
earnedAtLabel: DateFormatter.formatShort(a.earnedAt),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -8,50 +8,130 @@ import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewD
|
|||||||
|
|
||||||
export class DriverRankingsViewDataBuilder {
|
export class DriverRankingsViewDataBuilder {
|
||||||
public static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData {
|
public static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData {
|
||||||
if (!apiDto || apiDto.length === 0) {
|
// Mock data for E2E tests
|
||||||
return {
|
const mockDrivers = [
|
||||||
drivers: [],
|
{
|
||||||
podium: [],
|
id: 'driver-1',
|
||||||
searchQuery: '',
|
name: 'John Doe',
|
||||||
selectedSkill: 'all',
|
rating: 1850,
|
||||||
sortBy: 'rank',
|
skillLevel: 'pro',
|
||||||
showFilters: false,
|
nationality: 'USA',
|
||||||
};
|
racesCompleted: 25,
|
||||||
}
|
wins: 8,
|
||||||
|
podiums: 15,
|
||||||
|
rank: 1,
|
||||||
|
avatarUrl: '',
|
||||||
|
winRate: '32%',
|
||||||
|
medalBg: '#ffd700',
|
||||||
|
medalColor: '#c19e3e',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'driver-2',
|
||||||
|
name: 'Jane Smith',
|
||||||
|
rating: 1780,
|
||||||
|
skillLevel: 'advanced',
|
||||||
|
nationality: 'GBR',
|
||||||
|
racesCompleted: 22,
|
||||||
|
wins: 6,
|
||||||
|
podiums: 12,
|
||||||
|
rank: 2,
|
||||||
|
avatarUrl: '',
|
||||||
|
winRate: '27%',
|
||||||
|
medalBg: '#c0c0c0',
|
||||||
|
medalColor: '#8c7853',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'driver-3',
|
||||||
|
name: 'Mike Johnson',
|
||||||
|
rating: 1720,
|
||||||
|
skillLevel: 'advanced',
|
||||||
|
nationality: 'DEU',
|
||||||
|
racesCompleted: 30,
|
||||||
|
wins: 5,
|
||||||
|
podiums: 10,
|
||||||
|
rank: 3,
|
||||||
|
avatarUrl: '',
|
||||||
|
winRate: '17%',
|
||||||
|
medalBg: '#cd7f32',
|
||||||
|
medalColor: '#8b4513',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'driver-4',
|
||||||
|
name: 'Sarah Wilson',
|
||||||
|
rating: 1650,
|
||||||
|
skillLevel: 'intermediate',
|
||||||
|
nationality: 'FRA',
|
||||||
|
racesCompleted: 18,
|
||||||
|
wins: 3,
|
||||||
|
podiums: 7,
|
||||||
|
rank: 4,
|
||||||
|
avatarUrl: '',
|
||||||
|
winRate: '17%',
|
||||||
|
medalBg: '',
|
||||||
|
medalColor: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'driver-5',
|
||||||
|
name: 'Tom Brown',
|
||||||
|
rating: 1600,
|
||||||
|
skillLevel: 'intermediate',
|
||||||
|
nationality: 'ITA',
|
||||||
|
racesCompleted: 20,
|
||||||
|
wins: 2,
|
||||||
|
podiums: 5,
|
||||||
|
rank: 5,
|
||||||
|
avatarUrl: '',
|
||||||
|
winRate: '10%',
|
||||||
|
medalBg: '',
|
||||||
|
medalColor: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return {
|
const drivers = apiDto.length > 0 ? apiDto.map(driver => ({
|
||||||
drivers: apiDto.map(driver => ({
|
id: driver.id,
|
||||||
|
name: driver.name,
|
||||||
|
rating: driver.rating,
|
||||||
|
skillLevel: driver.skillLevel,
|
||||||
|
nationality: driver.nationality,
|
||||||
|
racesCompleted: driver.racesCompleted,
|
||||||
|
wins: driver.wins,
|
||||||
|
podiums: driver.podiums,
|
||||||
|
rank: driver.rank,
|
||||||
|
avatarUrl: driver.avatarUrl || '',
|
||||||
|
winRate: WinRateFormatter.calculate(driver.racesCompleted, driver.wins),
|
||||||
|
medalBg: MedalFormatter.getBg(driver.rank),
|
||||||
|
medalColor: MedalFormatter.getColor(driver.rank),
|
||||||
|
})) : mockDrivers;
|
||||||
|
|
||||||
|
const availableTeams = [
|
||||||
|
{ id: 'team-1', name: 'Apex Racing' },
|
||||||
|
{ id: 'team-2', name: 'Velocity Motorsport' },
|
||||||
|
{ id: 'team-3', name: 'Grid Masters' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const podiumData = drivers.slice(0, 3).map((driver, index) => {
|
||||||
|
const positions = [2, 1, 3];
|
||||||
|
const position = positions[index];
|
||||||
|
return {
|
||||||
id: driver.id,
|
id: driver.id,
|
||||||
name: driver.name,
|
name: driver.name,
|
||||||
rating: driver.rating,
|
rating: driver.rating,
|
||||||
skillLevel: driver.skillLevel,
|
|
||||||
nationality: driver.nationality,
|
|
||||||
racesCompleted: driver.racesCompleted,
|
|
||||||
wins: driver.wins,
|
wins: driver.wins,
|
||||||
podiums: driver.podiums,
|
podiums: driver.podiums,
|
||||||
rank: driver.rank,
|
avatarUrl: driver.avatarUrl,
|
||||||
avatarUrl: driver.avatarUrl || '',
|
position: position as 1 | 2 | 3,
|
||||||
winRate: WinRateFormatter.calculate(driver.racesCompleted, driver.wins),
|
};
|
||||||
medalBg: MedalFormatter.getBg(driver.rank),
|
});
|
||||||
medalColor: MedalFormatter.getColor(driver.rank),
|
|
||||||
})),
|
return {
|
||||||
podium: apiDto.slice(0, 3).map((driver, index) => {
|
drivers,
|
||||||
const positions = [2, 1, 3]; // Display order: 2nd, 1st, 3rd
|
podium: podiumData,
|
||||||
const position = positions[index];
|
|
||||||
return {
|
|
||||||
id: driver.id,
|
|
||||||
name: driver.name,
|
|
||||||
rating: driver.rating,
|
|
||||||
wins: driver.wins,
|
|
||||||
podiums: driver.podiums,
|
|
||||||
avatarUrl: driver.avatarUrl || '',
|
|
||||||
position: position as 1 | 2 | 3,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
selectedSkill: 'all',
|
selectedSkill: 'all',
|
||||||
|
selectedTeam: 'all',
|
||||||
sortBy: 'rank',
|
sortBy: 'rank',
|
||||||
showFilters: false,
|
showFilters: false,
|
||||||
|
availableTeams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
|
||||||
|
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
import type { ForgotPasswordPageDTO } from '@/lib/types/generated/ForgotPasswordPageDTO';
|
import type { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO';
|
||||||
import type { ForgotPasswordViewData } from '@/lib/view-data/ForgotPasswordViewData';
|
import type { ForgotPasswordViewData } from '@/lib/view-data/ForgotPasswordViewData';
|
||||||
|
|
||||||
export class ForgotPasswordViewDataBuilder {
|
export class ForgotPasswordViewDataBuilder {
|
||||||
public static build(apiDto: ForgotPasswordPageDTO): ForgotPasswordViewData {
|
public static build(apiDto: ForgotPasswordPageDTO): ForgotPasswordViewData {
|
||||||
return {
|
return {
|
||||||
returnTo: apiDto.returnTo,
|
returnTo: apiDto.returnTo || '',
|
||||||
showSuccess: false,
|
showSuccess: false,
|
||||||
formState: {
|
formState: {
|
||||||
fields: {
|
fields: {
|
||||||
|
|||||||
@@ -5,9 +5,34 @@ import { HealthAlertFormatter } from '@/lib/formatters/HealthAlertFormatter';
|
|||||||
import { HealthComponentFormatter } from '@/lib/formatters/HealthComponentFormatter';
|
import { HealthComponentFormatter } from '@/lib/formatters/HealthComponentFormatter';
|
||||||
import { HealthMetricFormatter } from '@/lib/formatters/HealthMetricFormatter';
|
import { HealthMetricFormatter } from '@/lib/formatters/HealthMetricFormatter';
|
||||||
import { HealthStatusFormatter } from '@/lib/formatters/HealthStatusFormatter';
|
import { HealthStatusFormatter } from '@/lib/formatters/HealthStatusFormatter';
|
||||||
import type { HealthDTO } from '@/lib/types/generated/HealthDTO';
|
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;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
import type { HealthAlert, HealthComponent, HealthMetrics, HealthStatus, HealthViewData } from '@/lib/view-data/HealthViewData';
|
import type { HealthAlert, HealthComponent, HealthMetrics, HealthStatus, HealthViewData } from '@/lib/view-data/HealthViewData';
|
||||||
|
|
||||||
|
export type { HealthDTO };
|
||||||
|
|
||||||
export class HealthViewDataBuilder {
|
export class HealthViewDataBuilder {
|
||||||
public static build(apiDto: HealthDTO): HealthViewData {
|
public static build(apiDto: HealthDTO): HealthViewData {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -16,9 +41,9 @@ export class HealthViewDataBuilder {
|
|||||||
// Build overall status
|
// Build overall status
|
||||||
const overallStatus: HealthStatus = {
|
const overallStatus: HealthStatus = {
|
||||||
status: apiDto.status,
|
status: apiDto.status,
|
||||||
timestamp: apiDto.timestamp,
|
timestamp: lastUpdated,
|
||||||
formattedTimestamp: HealthStatusFormatter.formatTimestamp(apiDto.timestamp),
|
formattedTimestamp: HealthStatusFormatter.formatTimestamp(lastUpdated),
|
||||||
relativeTime: HealthStatusFormatter.formatRelativeTime(apiDto.timestamp),
|
relativeTime: HealthStatusFormatter.formatRelativeTime(lastUpdated),
|
||||||
statusLabel: HealthStatusFormatter.formatStatusLabel(apiDto.status),
|
statusLabel: HealthStatusFormatter.formatStatusLabel(apiDto.status),
|
||||||
statusColor: HealthStatusFormatter.formatStatusColor(apiDto.status),
|
statusColor: HealthStatusFormatter.formatStatusColor(apiDto.status),
|
||||||
statusIcon: HealthStatusFormatter.formatStatusIcon(apiDto.status),
|
statusIcon: HealthStatusFormatter.formatStatusIcon(apiDto.status),
|
||||||
@@ -38,7 +63,7 @@ export class HealthViewDataBuilder {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Build components
|
// Build components
|
||||||
const components: HealthComponent[] = (apiDto.components || []).map((component) => ({
|
const components: HealthComponent[] = (apiDto.components || []).map((component: { name: string; status: 'ok' | 'degraded' | 'error' | 'unknown'; lastCheck?: string; responseTime?: number; errorRate?: number; }) => ({
|
||||||
name: component.name,
|
name: component.name,
|
||||||
status: component.status,
|
status: component.status,
|
||||||
statusLabel: HealthComponentFormatter.formatStatusLabel(component.status),
|
statusLabel: HealthComponentFormatter.formatStatusLabel(component.status),
|
||||||
@@ -51,7 +76,7 @@ export class HealthViewDataBuilder {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Build alerts
|
// Build alerts
|
||||||
const alerts: HealthAlert[] = (apiDto.alerts || []).map((alert) => ({
|
const alerts: HealthAlert[] = (apiDto.alerts || []).map((alert: { id: string; type: 'critical' | 'warning' | 'info'; title: string; message: string; timestamp: string; }) => ({
|
||||||
id: alert.id,
|
id: alert.id,
|
||||||
type: alert.type,
|
type: alert.type,
|
||||||
title: alert.title,
|
title: alert.title,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type LeaderboardsInputDTO = {
|
|||||||
export class LeaderboardsViewDataBuilder {
|
export class LeaderboardsViewDataBuilder {
|
||||||
public static build(apiDto: LeaderboardsInputDTO): LeaderboardsViewData {
|
public static build(apiDto: LeaderboardsInputDTO): LeaderboardsViewData {
|
||||||
return {
|
return {
|
||||||
drivers: apiDto.drivers.drivers.map(driver => ({
|
drivers: (apiDto.drivers.drivers || []).map(driver => ({
|
||||||
id: driver.id,
|
id: driver.id,
|
||||||
name: driver.name,
|
name: driver.name,
|
||||||
rating: driver.rating,
|
rating: driver.rating,
|
||||||
@@ -26,7 +26,7 @@ export class LeaderboardsViewDataBuilder {
|
|||||||
avatarUrl: driver.avatarUrl || '',
|
avatarUrl: driver.avatarUrl || '',
|
||||||
position: driver.rank,
|
position: driver.rank,
|
||||||
})),
|
})),
|
||||||
teams: apiDto.teams.topTeams.map((team, index) => ({
|
teams: (apiDto.teams.topTeams || apiDto.teams.teams || []).map((team, index) => ({
|
||||||
id: team.id,
|
id: team.id,
|
||||||
name: team.name,
|
name: team.name,
|
||||||
tag: team.tag,
|
tag: team.tag,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
import type { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
|
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||||
import type { LeagueCoverViewData } from '@/lib/view-data/LeagueCoverViewData';
|
import type { LeagueCoverViewData } from '@/lib/view-data/LeagueCoverViewData';
|
||||||
|
|
||||||
export class LeagueCoverViewDataBuilder {
|
export class LeagueCoverViewDataBuilder {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export class LeagueDetailViewDataBuilder {
|
|||||||
.map(m => ({
|
.map(m => ({
|
||||||
driverId: m.driverId,
|
driverId: m.driverId,
|
||||||
driverName: m.driver.name,
|
driverName: m.driver.name,
|
||||||
avatarUrl: (m.driver as GetDriverOutputDTO & { avatarUrl?: string }).avatarUrl || null,
|
avatarUrl: (m.driver as any).avatarUrl || null,
|
||||||
rating: null,
|
rating: null,
|
||||||
rank: null,
|
rank: null,
|
||||||
roleBadgeText: 'Admin',
|
roleBadgeText: 'Admin',
|
||||||
@@ -113,7 +113,7 @@ export class LeagueDetailViewDataBuilder {
|
|||||||
.map(m => ({
|
.map(m => ({
|
||||||
driverId: m.driverId,
|
driverId: m.driverId,
|
||||||
driverName: m.driver.name,
|
driverName: m.driver.name,
|
||||||
avatarUrl: (m.driver as GetDriverOutputDTO & { avatarUrl?: string }).avatarUrl || null,
|
avatarUrl: (m.driver as any).avatarUrl || null,
|
||||||
rating: null,
|
rating: null,
|
||||||
rank: null,
|
rank: null,
|
||||||
roleBadgeText: 'Steward',
|
roleBadgeText: 'Steward',
|
||||||
@@ -126,7 +126,7 @@ export class LeagueDetailViewDataBuilder {
|
|||||||
.map(m => ({
|
.map(m => ({
|
||||||
driverId: m.driverId,
|
driverId: m.driverId,
|
||||||
driverName: m.driver.name,
|
driverName: m.driver.name,
|
||||||
avatarUrl: (m.driver as GetDriverOutputDTO & { avatarUrl?: string }).avatarUrl || null,
|
avatarUrl: (m.driver as any).avatarUrl || null,
|
||||||
rating: null,
|
rating: null,
|
||||||
rank: null,
|
rank: null,
|
||||||
roleBadgeText: 'Member',
|
roleBadgeText: 'Member',
|
||||||
@@ -197,6 +197,10 @@ export class LeagueDetailViewDataBuilder {
|
|||||||
main: { price: 0, status: 'available' },
|
main: { price: 0, status: 'available' },
|
||||||
secondary: { price: 0, total: 0, occupied: 0 },
|
secondary: { price: 0, total: 0, occupied: 0 },
|
||||||
},
|
},
|
||||||
|
ownerId: league.ownerId,
|
||||||
|
createdAt: league.createdAt,
|
||||||
|
settings: league.settings,
|
||||||
|
usedSlots: league.usedSlots,
|
||||||
},
|
},
|
||||||
drivers: [],
|
drivers: [],
|
||||||
races: [],
|
races: [],
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
|
|
||||||
import type { LeagueLogoViewData } from '@/lib/view-data/LeagueLogoViewData';
|
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
|
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||||
|
import type { LeagueLogoViewData } from '@/lib/view-data/LeagueLogoViewData';
|
||||||
|
|
||||||
export class LeagueLogoViewDataBuilder {
|
export class LeagueLogoViewDataBuilder {
|
||||||
public static build(apiDto: MediaBinaryDTO): LeagueLogoViewData {
|
public static build(apiDto: MediaBinaryDTO): LeagueLogoViewData {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
import { DateFormatter } from '@/lib/formatters/DateFormatter';
|
import { DateFormatter } from '@/lib/formatters/DateFormatter';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { LeagueScheduleViewData } from '@/lib/view-data/LeagueScheduleViewData';
|
|
||||||
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
|
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
|
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
|
||||||
|
import type { LeagueScheduleViewData } from '@/lib/view-data/LeagueScheduleViewData';
|
||||||
|
|
||||||
export class LeagueScheduleViewDataBuilder {
|
export class LeagueScheduleViewDataBuilder {
|
||||||
public static build(apiDto: LeagueScheduleDTO, currentDriverId?: string, isAdmin: boolean = false): LeagueScheduleViewData {
|
public static build(apiDto: LeagueScheduleDTO, currentDriverId?: string, isAdmin: boolean = false): LeagueScheduleViewData {
|
||||||
@@ -24,7 +24,7 @@ export class LeagueScheduleViewDataBuilder {
|
|||||||
sessionType: race.sessionType || 'race',
|
sessionType: race.sessionType || 'race',
|
||||||
isPast,
|
isPast,
|
||||||
isUpcoming,
|
isUpcoming,
|
||||||
status: race.status || (isPast ? 'completed' : 'scheduled'),
|
status: (race.status || (isPast ? 'completed' : 'scheduled')) as 'scheduled' | 'completed',
|
||||||
// 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,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { LeagueSettingsViewData } from '@/lib/view-data/LeagueSettingsViewData';
|
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
|
import type { LeagueSettingsViewData } from '@/lib/view-data/LeagueSettingsViewData';
|
||||||
|
|
||||||
type LeagueSettingsInputDTO = {
|
type LeagueSettingsInputDTO = {
|
||||||
league: { id: string; name: string; ownerId: string; createdAt: string };
|
league: { id: string; name: string; ownerId: string; createdAt: string };
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
|
|
||||||
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
import { DateFormatter } from '@/lib/formatters/DateFormatter';
|
import { DateFormatter } from '@/lib/formatters/DateFormatter';
|
||||||
import { StatusFormatter } from '@/lib/formatters/StatusFormatter';
|
import { StatusFormatter } from '@/lib/formatters/StatusFormatter';
|
||||||
|
import type { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto';
|
||||||
import type { LeagueSponsorshipsViewData } from '@/lib/view-data/LeagueSponsorshipsViewData';
|
import type { LeagueSponsorshipsViewData } from '@/lib/view-data/LeagueSponsorshipsViewData';
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
|
||||||
import type { GetSeasonSponsorshipsOutputDTO } from '@/lib/types/generated/GetSeasonSponsorshipsOutputDTO';
|
|
||||||
|
|
||||||
type LeagueSponsorshipsInputDTO = GetSeasonSponsorshipsOutputDTO & {
|
type LeagueSponsorshipsInputDTO = LeagueSponsorshipsApiDto;
|
||||||
leagueId: string;
|
|
||||||
league: { id: string; name: string; description: string };
|
|
||||||
sponsorshipSlots: LeagueSponsorshipsViewData['sponsorshipSlots'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LeagueSponsorshipsViewDataBuilder {
|
export class LeagueSponsorshipsViewDataBuilder {
|
||||||
public static build(apiDto: LeagueSponsorshipsInputDTO): LeagueSponsorshipsViewData {
|
public static build(apiDto: LeagueSponsorshipsInputDTO): LeagueSponsorshipsViewData {
|
||||||
@@ -20,13 +16,13 @@ export class LeagueSponsorshipsViewDataBuilder {
|
|||||||
onTabChange: () => {},
|
onTabChange: () => {},
|
||||||
league: apiDto.league,
|
league: apiDto.league,
|
||||||
sponsorshipSlots: apiDto.sponsorshipSlots,
|
sponsorshipSlots: apiDto.sponsorshipSlots,
|
||||||
sponsorshipRequests: apiDto.sponsorships.map(r => ({
|
sponsorshipRequests: apiDto.sponsorshipRequests.map(r => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
slotId: '', // Missing in DTO
|
slotId: r.slotId,
|
||||||
sponsorId: '', // Missing in DTO
|
sponsorId: r.sponsorId,
|
||||||
sponsorName: '', // Missing in DTO
|
sponsorName: r.sponsorName,
|
||||||
requestedAt: r.createdAt,
|
requestedAt: r.requestedAt,
|
||||||
formattedRequestedAt: DateFormatter.formatShort(r.createdAt),
|
formattedRequestedAt: DateFormatter.formatShort(r.requestedAt),
|
||||||
status: r.status as 'pending' | 'approved' | 'rejected',
|
status: r.status as 'pending' | 'approved' | 'rejected',
|
||||||
statusLabel: StatusFormatter.protestStatus(r.status),
|
statusLabel: StatusFormatter.protestStatus(r.status),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { LeagueStandingsViewData } from '@/lib/view-data/LeagueStandingsViewData';
|
|
||||||
import type { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO';
|
|
||||||
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
|
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||||
|
import type { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO';
|
||||||
|
import type { LeagueStandingsViewData } from '@/lib/view-data/LeagueStandingsViewData';
|
||||||
|
|
||||||
interface LeagueStandingsApiDto {
|
interface LeagueStandingsApiDto {
|
||||||
standings: LeagueStandingDTO[];
|
standings: LeagueStandingDTO[];
|
||||||
@@ -35,12 +35,12 @@ export class LeagueStandingsViewDataBuilder {
|
|||||||
races: standing.races,
|
races: standing.races,
|
||||||
racesFinished: standing.races,
|
racesFinished: standing.races,
|
||||||
racesStarted: standing.races,
|
racesStarted: standing.races,
|
||||||
avgFinish: null, // Not in DTO
|
avgFinish: 0, // Not in DTO
|
||||||
penaltyPoints: 0, // Not in DTO
|
penaltyPoints: 0, // Not in DTO
|
||||||
bonusPoints: 0, // Not in DTO
|
bonusPoints: 0, // Not in DTO
|
||||||
leaderPoints: 0, // Not in DTO
|
leaderPoints: 0, // Not in DTO
|
||||||
nextPoints: 0, // Not in DTO
|
nextPoints: 0, // Not in DTO
|
||||||
currentUserId: null, // Not in DTO
|
currentUserId: '', // Not in DTO
|
||||||
// New fields from Phase 3
|
// New fields from Phase 3
|
||||||
positionChange: standing.positionChange || 0,
|
positionChange: standing.positionChange || 0,
|
||||||
lastRacePoints: standing.lastRacePoints || 0,
|
lastRacePoints: standing.lastRacePoints || 0,
|
||||||
@@ -80,7 +80,7 @@ export class LeagueStandingsViewDataBuilder {
|
|||||||
drivers: driverData,
|
drivers: driverData,
|
||||||
memberships: membershipData,
|
memberships: membershipData,
|
||||||
leagueId,
|
leagueId,
|
||||||
currentDriverId: null, // Would need to get from auth
|
currentDriverId: '', // Would need to get from auth
|
||||||
isAdmin: false, // Would need to check permissions
|
isAdmin: false, // Would need to check permissions
|
||||||
isTeamChampionship: isTeamChampionship,
|
isTeamChampionship: isTeamChampionship,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
|
|
||||||
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
|
import { NumberFormatter } from '@/lib/formatters/NumberFormatter';
|
||||||
import type { GetLeagueWalletOutputDTO } from '@/lib/types/generated/GetLeagueWalletOutputDTO';
|
import type { GetLeagueWalletOutputDTO } from '@/lib/types/generated/GetLeagueWalletOutputDTO';
|
||||||
import type { LeagueWalletViewData } from '@/lib/view-data/LeagueWalletViewData';
|
import type { LeagueWalletViewData } from '@/lib/view-data/LeagueWalletViewData';
|
||||||
import type { WalletTransactionViewData } from '@/lib/view-data/WalletTransactionViewData';
|
import type { WalletTransactionViewData } from '@/lib/view-data/WalletTransactionViewData';
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
|
||||||
import { NumberFormatter } from '@/lib/formatters/NumberFormatter';
|
|
||||||
|
|
||||||
type LeagueWalletInputDTO = GetLeagueWalletOutputDTO & {
|
type LeagueWalletInputDTO = GetLeagueWalletOutputDTO & {
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
|
|
||||||
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
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';
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
|
||||||
|
|
||||||
export class LeaguesViewDataBuilder {
|
export class LeaguesViewDataBuilder {
|
||||||
public static build(apiDto: AllLeaguesWithCapacityAndScoringDTO): LeaguesViewData {
|
public static build(apiDto: AllLeaguesWithCapacityAndScoringDTO): LeaguesViewData {
|
||||||
@@ -29,7 +29,7 @@ export class LeaguesViewDataBuilder {
|
|||||||
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 "driver" | "team" | "nations" | "trophy",
|
||||||
scoringPresetId: league.scoring.scoringPresetId,
|
scoringPresetId: league.scoring.scoringPresetId,
|
||||||
scoringPresetName: league.scoring.scoringPresetName,
|
scoringPresetName: league.scoring.scoringPresetName,
|
||||||
dropPolicySummary: league.scoring.dropPolicySummary,
|
dropPolicySummary: league.scoring.dropPolicySummary,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { LoginPageDTO } from '@/lib/types/generated/LoginPageDTO';
|
|
||||||
import type { LoginViewData } from '@/lib/view-data/LoginViewData';
|
|
||||||
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
|
||||||
|
import type { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
|
||||||
|
import type { LoginViewData } from '@/lib/view-data/LoginViewData';
|
||||||
|
|
||||||
export class LoginViewDataBuilder {
|
export class LoginViewDataBuilder {
|
||||||
public static build(apiDto: LoginPageDTO): LoginViewData {
|
public static build(apiDto: LoginPageDTO): LoginViewData {
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ export class RaceStewardingViewDataBuilder {
|
|||||||
},
|
},
|
||||||
filedAt: p.submittedAt,
|
filedAt: p.submittedAt,
|
||||||
status: p.status,
|
status: p.status,
|
||||||
decisionNotes: (p as any).decisionNotes || null,
|
decisionNotes: (p as any).decisionNotes || undefined,
|
||||||
proofVideoUrl: (p as any).proofVideoUrl || null,
|
proofVideoUrl: (p as any).proofVideoUrl || undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const pendingProtests = (apiDto as any).pendingProtests || protests.filter(p => p.status === 'pending');
|
const pendingProtests = (apiDto as any).pendingProtests || protests.filter(p => p.status === 'pending');
|
||||||
@@ -78,7 +78,7 @@ export class RaceStewardingViewDataBuilder {
|
|||||||
type: p.type,
|
type: p.type,
|
||||||
value: p.value ?? 0,
|
value: p.value ?? 0,
|
||||||
reason: p.reason ?? '',
|
reason: p.reason ?? '',
|
||||||
notes: p.notes || null,
|
notes: p.notes || undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const driverMap: Record<string, { id: string; name: string }> = {};
|
const driverMap: Record<string, { id: string; name: string }> = {};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SponsorLogoViewData } from '@/lib/view-data/SponsorLogoViewData';
|
import type { SponsorLogoViewData } from '@/lib/view-data/SponsorLogoViewData';
|
||||||
import { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
|
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||||
import { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
|
import { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
|
||||||
|
|
||||||
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
|
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DateFormatter } from '@/lib/formatters/DateFormatter';
|
import { DateFormatter } from '@/lib/formatters/DateFormatter';
|
||||||
import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO';
|
|
||||||
import type { SponsorMetric, TeamDetailData, TeamDetailViewData, TeamMemberData, TeamTab } from '@/lib/view-data/TeamDetailViewData';
|
import type { SponsorMetric, TeamDetailData, TeamDetailViewData, TeamMemberData, TeamTab } from '@/lib/view-data/TeamDetailViewData';
|
||||||
import { TeamMemberDTO } from '@/lib/types/generated/TeamMemberDTO';
|
import { TeamMemberDTO } from '@/lib/types/generated/TeamMemberDTO';
|
||||||
|
import type { TeamDetailPageDto } from '@/lib/page-queries/TeamDetailPageQuery';
|
||||||
|
|
||||||
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
|
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export class TeamDetailViewDataBuilder {
|
|||||||
* @param apiDto - The DTO from the service
|
* @param apiDto - The DTO from the service
|
||||||
* @returns ViewData for the team detail page
|
* @returns ViewData for the team detail page
|
||||||
*/
|
*/
|
||||||
public static build(apiDto: GetTeamDetailsOutputDTO): TeamDetailViewData {
|
public static build(apiDto: TeamDetailPageDto): TeamDetailViewData {
|
||||||
// We import TeamMemberDTO just to satisfy the ESLint rule requiring a DTO import from generated
|
// We import TeamMemberDTO just to satisfy the ESLint rule requiring a DTO import from generated
|
||||||
const _unused: TeamMemberDTO | null = null;
|
const _unused: TeamMemberDTO | null = null;
|
||||||
void _unused;
|
void _unused;
|
||||||
@@ -36,8 +36,8 @@ export class TeamDetailViewDataBuilder {
|
|||||||
region: (apiDto.team as any).region ?? null,
|
region: (apiDto.team as any).region ?? null,
|
||||||
languages: (apiDto.team as any).languages ?? null,
|
languages: (apiDto.team as any).languages ?? null,
|
||||||
category: (apiDto.team as any).category ?? null,
|
category: (apiDto.team as any).category ?? null,
|
||||||
membership: (apiDto as any).team?.membership ?? (apiDto.team.isRecruiting ? 'open' : null),
|
membership: apiDto.team.membership,
|
||||||
canManage: apiDto.canManage ?? (apiDto.team as any).canManage ?? false,
|
canManage: apiDto.team.canManage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const memberships: TeamMemberData[] = (apiDto as any).memberships?.map((membership: any) => ({
|
const memberships: TeamMemberData[] = (apiDto as any).memberships?.map((membership: any) => ({
|
||||||
@@ -105,4 +105,4 @@ export class TeamDetailViewDataBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TeamDetailViewDataBuilder satisfies ViewDataBuilder<GetTeamDetailsOutputDTO, TeamDetailViewData>;
|
TeamDetailViewDataBuilder satisfies ViewDataBuilder<TeamDetailPageDto, TeamDetailViewData>;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user