view data fixes

This commit is contained in:
2026-01-23 15:30:23 +01:00
parent e22033be38
commit f8099f04bc
213 changed files with 3466 additions and 3003 deletions

View File

@@ -250,7 +250,8 @@
"plugins": [
"@typescript-eslint",
"boundaries",
"import"
"import",
"gridpilot-rules"
],
"rules": {
"@typescript-eslint/no-explicit-any": "error",
@@ -310,7 +311,9 @@
"message": "Interface names should not start with 'I'. Use descriptive names without the 'I' prefix (e.g., 'LiverCompositor' instead of 'ILiveryCompositor').",
"selector": "TSInterfaceDeclaration[id.name=/^I[A-Z]/]"
}
]
],
// GridPilot ESLint Rules
"gridpilot-rules/view-model-taxonomy": "error"
}
},
{

View File

@@ -210,7 +210,8 @@
"lib/view-models/**/*.tsx"
],
"rules": {
"gridpilot-rules/view-model-implements": "error"
"gridpilot-rules/view-model-implements": "error",
"gridpilot-rules/view-model-taxonomy": "error"
}
},
{

View File

@@ -2,19 +2,16 @@
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { TeamLeaderboardTemplate } from '@/templates/TeamLeaderboardTemplate';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
interface TeamLeaderboardViewData extends ViewData extends ViewData {
teams: TeamSummaryViewModel[];
}
export function TeamLeaderboardPageWrapper({ viewData }: ClientWrapperProps<TeamLeaderboardViewData>) {
export function TeamLeaderboardPageWrapper({ viewData }: ClientWrapperProps<{ teams: TeamListItemDTO[] }>) {
const router = useRouter();
// Client-side UI state only (no business logic)
@@ -22,7 +19,13 @@ export function TeamLeaderboardPageWrapper({ viewData }: ClientWrapperProps<Team
const [filterLevel, setFilterLevel] = useState<SkillLevel | 'all'>('all');
const [sortBy, setSortBy] = useState<SortBy>('rating');
if (!viewData.teams || viewData.teams.length === 0) {
// Instantiate ViewModels on the client to wrap plain DTOs with logic
const teamViewModels = useMemo(() =>
(viewData.teams || []).map(dto => new TeamSummaryViewModel(dto)),
[viewData.teams]
);
if (teamViewModels.length === 0) {
return null;
}
@@ -34,8 +37,8 @@ export function TeamLeaderboardPageWrapper({ viewData }: ClientWrapperProps<Team
router.push('/teams');
};
// Apply filtering and sorting
const filteredAndSortedTeams = viewData.teams
// Apply filtering and sorting using ViewModel logic
const filteredAndSortedTeams = teamViewModels
.filter((team) => {
const matchesSearch = team.name.toLowerCase().includes(searchQuery.toLowerCase());
const matchesLevel = filterLevel === 'all' || team.performanceLevel === filterLevel;
@@ -54,7 +57,7 @@ export function TeamLeaderboardPageWrapper({ viewData }: ClientWrapperProps<Team
});
const templateViewData = {
teams: viewData.teams,
teams: teamViewModels,
searchQuery,
filterLevel,
sortBy,

View File

@@ -0,0 +1,160 @@
# ESLint Rule Analysis for RaceWithSOFViewModel.ts
## File Analyzed
`apps/website/lib/view-models/RaceWithSOFViewModel.ts`
## Violations Found
### 1. DTO Import (Line 1)
```typescript
import { RaceWithSOFDTO } from '@/lib/types/generated/RaceWithSOFDTO';
```
**Rule Violated**: `view-model-taxonomy.js`
**Reason**:
- Imports from DTO path (`lib/types/generated/`)
- Uses DTO naming convention (`RaceWithSOFDTO`)
### 2. Inline ViewData Interface (Lines 9-13)
```typescript
export interface RaceWithSOFViewData {
id: string;
track: string;
strengthOfField: number | null;
}
```
**Rule Violated**: `view-model-taxonomy.js`
**Reason**: Defines ViewData interface inline instead of importing from `lib/view-data/`
## Rule Gaps Identified
### Current Rule Issues
1. **Incomplete import checking**: Only reported if imported name contained "DTO", but should forbid ALL imports from disallowed paths
2. **No strict whitelist**: Didn't enforce that imports MUST be from allowed paths
3. **Poor relative import handling**: Couldn't properly resolve relative imports
4. **Missing strict import message**: No message for general import path violations
### Architectural Requirements
The project requires:
1. **Forbid "dto" in the whole directory** ✓ (covered)
2. **Imports only from contracts or view models/view data dir** ✗ (partially covered)
3. **No inline view data interfaces** ✓ (covered)
## Improvements Made
### 1. Updated `view-model-taxonomy.js`
**Changes**:
- Added `strictImport` message for general import path violations
- Changed import check to report for ANY import from disallowed paths (not just those with "DTO" in name)
- Added strict import path enforcement with whitelist
- Improved relative import handling
- Added null checks for `node.id` in interface/type checks
**New Behavior**:
- Forbids ALL imports from DTO/service paths (`lib/types/generated/`, `lib/dtos/`, `lib/api/`, `lib/services/`)
- Enforces strict whitelist: only allows imports from `@/lib/contracts/`, `@/lib/view-models/`, `@/lib/view-data/`
- Allows external imports (npm packages)
- Handles relative imports with heuristic pattern matching
### 2. Updated `test-view-model-taxonomy.js`
**Changes**:
- Added test for service layer imports
- Added test for strict import violations
- Updated test summary to include new test cases
## Test Results
### Before Improvements
- Test 1 (DTO import): ✓ PASS
- Test 2 (Inline ViewData): ✓ PASS
- Test 3 (Valid code): ✓ PASS
### After Improvements
- Test 1 (DTO import): ✓ PASS
- Test 2 (Inline ViewData): ✓ PASS
- Test 3 (Valid code): ✓ PASS
- Test 4 (Service import): ✓ PASS (new)
- Test 5 (Strict import): ✓ PASS (new)
## Recommended Refactoring for RaceWithSOFViewModel.ts
### Current Code (Violations)
```typescript
import { RaceWithSOFDTO } from '@/lib/types/generated/RaceWithSOFDTO';
import { ViewModel } from "../contracts/view-models/ViewModel";
export interface RaceWithSOFViewData {
id: string;
track: string;
strengthOfField: number | null;
}
export class RaceWithSOFViewModel extends ViewModel {
private readonly data: RaceWithSOFViewData;
constructor(data: RaceWithSOFViewData) {
super();
this.data = data;
}
get id(): string { return this.data.id; }
get track(): string { return this.data.track; }
get strengthOfField(): number | null { return this.data.strengthOfField; }
}
```
### Fixed Code (No Violations)
```typescript
import { ViewModel } from "../contracts/view-models/ViewModel";
import { RaceWithSOFViewData } from '@/lib/view-data/RaceWithSOFViewData';
export class RaceWithSOFViewModel extends ViewModel {
private readonly data: RaceWithSOFViewData;
constructor(data: RaceWithSOFViewData) {
super();
this.data = data;
}
get id(): string { return this.data.id; }
get track(): string { return this.data.track; }
get strengthOfField(): number | null { return this.data.strengthOfField; }
}
```
**Changes**:
1. Removed DTO import (`RaceWithSOFDTO`)
2. Moved ViewData interface to `lib/view-data/RaceWithSOFViewData.ts`
3. Imported ViewData from proper location
## Additional Recommendations
### 1. Consider Splitting the Rule
If the rule becomes too complex, consider splitting it into:
- `view-model-taxonomy.js`: Keep only DTO and ViewData definition checks
- `view-model-imports.js`: New rule for strict import path enforcement
### 2. Improve Relative Import Handling
The current heuristic for relative imports may have false positives/negatives. Consider:
- Using a path resolver
- Requiring absolute imports with `@/` prefix
- Adding configuration for allowed relative import patterns
### 3. Add More Tests
- Test with nested view model directories
- Test with type imports (`import type`)
- Test with external package imports
- Test with relative imports from different depths
### 4. Update Documentation
- Document the allowed import paths
- Provide examples of correct and incorrect usage
- Update the rule description to reflect the new strict import enforcement
## Conclusion
The updated `view-model-taxonomy.js` rule now properly enforces all three architectural requirements:
1. ✓ Forbids "DTO" in identifiers
2. ✓ Enforces strict import path whitelist
3. ✓ Forbids inline ViewData definitions
The rule is more robust and catches more violations while maintaining backward compatibility with existing valid code.

View File

@@ -51,6 +51,7 @@ const viewDataBuilderImports = require('./view-data-builder-imports');
const viewModelBuilderImplements = require('./view-model-builder-implements');
const viewDataImplements = require('./view-data-implements');
const viewModelImplements = require('./view-model-implements');
const viewModelTaxonomy = require('./view-model-taxonomy');
module.exports = {
rules: {
@@ -141,6 +142,7 @@ module.exports = {
'view-model-builder-contract': viewModelBuilderContract,
'view-model-builder-implements': viewModelBuilderImplements,
'view-model-implements': viewModelImplements,
'view-model-taxonomy': viewModelTaxonomy,
// Single Export Rules
'single-export-per-file': singleExportPerFile,

View File

@@ -14,17 +14,21 @@ module.exports = {
category: 'Template Purity',
},
messages: {
message: 'ViewModels or DisplayObjects import forbidden in templates - see apps/website/lib/contracts/view-data/ViewData.ts',
message: 'ViewModels or DisplayObjects import forbidden in templates - see apps/website/lib/contracts/view-data/ViewData.ts. Templates should only receive logic-rich ViewModels via props from ClientWrappers, never import them directly.',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
// Templates are allowed to import ViewModels for TYPE-ONLY usage (interface/type)
// but not for instantiation or logic. However, to be safe, we forbid direct imports
// and suggest passing them through ClientWrappers.
if ((importPath.includes('@/lib/view-models/') ||
importPath.includes('@/lib/presenters/') ||
importPath.includes('@/lib/display-objects/')) &&
!isInComment(node)) {
!isInComment(node) &&
node.importKind !== 'type') {
context.report({
node,
messageId: 'message',

View File

@@ -0,0 +1,168 @@
/**
* Test script for view-model-taxonomy rule
*/
const rule = require('./view-model-taxonomy.js');
const { Linter } = require('eslint');
const linter = new Linter();
// Register the plugin
linter.defineRule('gridpilot-rules/view-model-taxonomy', rule);
// Test 1: DTO import should be caught
const codeWithDtoImport = `
import type { RecordEngagementOutputDTO } from '@/lib/types/generated/RecordEngagementOutputDTO';
export class RecordEngagementOutputViewModel {
eventId: string;
engagementWeight: number;
constructor(dto: RecordEngagementOutputDTO) {
this.eventId = dto.eventId;
this.engagementWeight = dto.engagementWeight;
}
}
`;
// Test 2: Inline ViewData interface should be caught
const codeWithInlineViewData = `
export interface RaceViewData {
id: string;
name: string;
}
export class RaceViewModel {
private readonly data: RaceViewData;
constructor(data: RaceViewData) {
this.data = data;
}
}
`;
// Test 3: Valid code (no violations)
const validCode = `
import { RaceViewData } from '@/lib/view-data/RaceViewData';
export class RaceViewModel {
private readonly data: RaceViewData;
constructor(data: RaceViewData) {
this.data = data;
}
}
`;
// Test 4: Disallowed import from service layer (should be caught)
const codeWithServiceImport = `
import { SomeService } from '@/lib/services/SomeService';
export class RaceViewModel {
private readonly service: SomeService;
constructor(service: SomeService) {
this.service = service;
}
}
`;
// Test 5: Strict import violation (import from non-allowed path)
const codeWithStrictImportViolation = `
import { SomeOtherThing } from '@/lib/other/SomeOtherThing';
export class RaceViewModel {
private readonly thing: SomeOtherThing;
constructor(thing: SomeOtherThing) {
this.thing = thing;
}
}
`;
console.log('=== Test 1: DTO import ===');
const messages1 = linter.verify(codeWithDtoImport, {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
'gridpilot-rules/view-model-taxonomy': 'error',
},
});
console.log('Messages:', messages1);
console.log('Expected: Should have 1 error for DTO import');
console.log('Actual: ' + messages1.length + ' error(s)');
console.log('');
console.log('=== Test 2: Inline ViewData interface ===');
const messages2 = linter.verify(codeWithInlineViewData, {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
'gridpilot-rules/view-model-taxonomy': 'error',
},
});
console.log('Messages:', messages2);
console.log('Expected: Should have 1 error for inline ViewData interface');
console.log('Actual: ' + messages2.length + ' error(s)');
console.log('');
console.log('=== Test 3: Valid code ===');
const messages3 = linter.verify(validCode, {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
'gridpilot-rules/view-model-taxonomy': 'error',
},
});
console.log('Messages:', messages3);
console.log('Expected: Should have 0 errors');
console.log('Actual: ' + messages3.length + ' error(s)');
console.log('');
console.log('=== Test 4: Service import (should be caught) ===');
const messages4 = linter.verify(codeWithServiceImport, {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
'gridpilot-rules/view-model-taxonomy': 'error',
},
});
console.log('Messages:', messages4);
console.log('Expected: Should have 1 error for service import');
console.log('Actual: ' + messages4.length + ' error(s)');
console.log('');
console.log('=== Test 5: Strict import violation ===');
const messages5 = linter.verify(codeWithStrictImportViolation, {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
'gridpilot-rules/view-model-taxonomy': 'error',
},
});
console.log('Messages:', messages5);
console.log('Expected: Should have 1 error for strict import violation');
console.log('Actual: ' + messages5.length + ' error(s)');
console.log('');
console.log('=== Summary ===');
console.log('Test 1 (DTO import): ' + (messages1.length === 1 ? '✓ PASS' : '✗ FAIL'));
console.log('Test 2 (Inline ViewData): ' + (messages2.length === 1 ? '✓ PASS' : '✗ FAIL'));
console.log('Test 3 (Valid code): ' + (messages3.length === 0 ? '✓ PASS' : '✗ FAIL'));
console.log('Test 4 (Service import): ' + (messages4.length === 1 ? '✓ PASS' : '✗ FAIL'));
console.log('Test 5 (Strict import): ' + (messages5.length === 1 ? '✓ PASS' : '✗ FAIL'));

View File

@@ -19,6 +19,7 @@ module.exports = {
messages: {
invalidDtoImport: 'ViewDataBuilders must import DTO types from lib/types/generated/, not from {{importPath}}',
invalidViewDataImport: 'ViewDataBuilders must import ViewData types from lib/view-data/, not from {{importPath}}',
noViewModelsInBuilders: 'ViewDataBuilders must not import ViewModels. ViewModels are client-only logic wrappers. Builders should only produce plain ViewData.',
missingDtoImport: 'ViewDataBuilders must import DTO types from lib/types/generated/',
missingViewDataImport: 'ViewDataBuilders must import ViewData types from lib/view-data/',
},

View File

@@ -4,6 +4,7 @@
* ViewData files in lib/view-data/ must:
* 1. Be interfaces or types named *ViewData
* 2. Extend the ViewData interface from contracts
* 3. NOT contain ViewModels (ViewModels are for ClientWrappers/Hooks)
*/
module.exports = {
@@ -19,6 +20,7 @@ module.exports = {
messages: {
notAnInterface: 'ViewData files must be interfaces or types named *ViewData',
missingExtends: 'ViewData must extend the ViewData interface from lib/contracts/view-data/ViewData.ts',
noViewModelsInViewData: 'ViewData must not contain ViewModels. ViewData is for plain JSON data (DTOs) passed through SSR. Use ViewModels in ClientWrappers or Hooks instead.',
},
},
@@ -32,6 +34,18 @@ module.exports = {
let hasCorrectName = false;
return {
// Check for ViewModel imports
ImportDeclaration(node) {
if (!isInViewData) return;
const importPath = node.source.value;
if (importPath.includes('/lib/view-models/')) {
context.report({
node,
messageId: 'noViewModelsInViewData',
});
}
},
// Check interface declarations
TSInterfaceDeclaration(node) {
const interfaceName = node.id?.name;
@@ -39,6 +53,19 @@ module.exports = {
if (interfaceName && interfaceName.endsWith('ViewData')) {
hasCorrectName = true;
// Check for ViewModel usage in properties
node.body.body.forEach(member => {
if (member.type === 'TSPropertySignature' && member.typeAnnotation) {
const typeAnnotation = member.typeAnnotation.typeAnnotation;
if (isViewModelType(typeAnnotation)) {
context.report({
node: member,
messageId: 'noViewModelsInViewData',
});
}
}
});
// Check if it extends ViewData
if (node.extends && node.extends.length > 0) {
for (const ext of node.extends) {

View File

@@ -0,0 +1,128 @@
/**
* ESLint rule to enforce ViewModel architectural boundaries
*
* ViewModels in lib/view-models/ must:
* 1. NOT contain the word "DTO" (DTOs are for API/Services)
* 2. NOT define ViewData interfaces (ViewData must be in lib/view-data/)
* 3. NOT import from DTO paths (DTOs belong to lib/types/generated/)
* 4. ONLY import from allowed paths: lib/contracts/, lib/view-models/, lib/view-data/, lib/display-objects/
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce ViewModel architectural boundaries',
category: 'Architecture',
recommended: true,
},
fixable: null,
schema: [],
messages: {
noDtoInViewModel: 'ViewModels must not use the word "DTO". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.',
noDtoImport: 'ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.',
noViewDataDefinition: 'ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.',
strictImport: 'ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: {{importPath}}',
},
},
create(context) {
const filename = context.getFilename();
const isInViewModels = filename.includes('/lib/view-models/');
if (!isInViewModels) return {};
return {
// Check for "DTO" in any identifier (variable, class, interface, property)
// Only catch identifiers that end with "DTO" or are exactly "DTO"
// This avoids false positives like "formattedTotalSpent" which contains "DTO" as a substring
Identifier(node) {
const name = node.name.toUpperCase();
// Only catch identifiers that end with "DTO" or are exactly "DTO"
if (name === 'DTO' || name.endsWith('DTO')) {
context.report({
node,
messageId: 'noDtoInViewModel',
});
}
},
// Check for imports from DTO paths and enforce strict import rules
ImportDeclaration(node) {
const importPath = node.source.value;
// Check 1: Disallowed paths (DTO and service layers)
// This catches ANY import from these paths, regardless of name
if (importPath.includes('/lib/types/generated/') ||
importPath.includes('/lib/dtos/') ||
importPath.includes('/lib/api/') ||
importPath.includes('/lib/services/')) {
context.report({
node,
messageId: 'noDtoImport',
});
}
// Check 2: Strict import path enforcement
// Only allow imports from these specific paths
const allowedPaths = [
'@/lib/contracts/',
'@/lib/view-models/',
'@/lib/view-data/',
'@/lib/display-objects/',
];
const isAllowed = allowedPaths.some(path => importPath.startsWith(path));
const isRelativeImport = importPath.startsWith('.');
const isExternal = !importPath.startsWith('.') && !importPath.startsWith('@');
// For relative imports, check if they contain allowed patterns
// This is a heuristic - may need refinement based on project structure
const isRelativeAllowed = isRelativeImport && (
importPath.includes('/lib/contracts/') ||
importPath.includes('/lib/view-models/') ||
importPath.includes('/lib/view-data/') ||
importPath.includes('/lib/display-objects/') ||
// Also check for patterns like ../contracts/...
importPath.includes('contracts') ||
importPath.includes('view-models') ||
importPath.includes('view-data') ||
importPath.includes('display-objects') ||
// Allow relative imports to view models (e.g., ./InvoiceViewModel, ../ViewModelName)
// This matches patterns like ./ViewModelName or ../ViewModelName
/^\.\/[A-Z][a-zA-Z0-9]*ViewModel$/.test(importPath) ||
/^\.\.\/[A-Z][a-zA-Z0-9]*ViewModel$/.test(importPath)
);
// Report if it's an internal import that's not allowed
if (!isAllowed && !isRelativeAllowed && !isExternal) {
context.report({
node,
messageId: 'strictImport',
data: { importPath },
});
}
},
// Check for ViewData definitions (Interface or Type Alias)
TSInterfaceDeclaration(node) {
if (node.id && node.id.name && node.id.name.endsWith('ViewData')) {
context.report({
node,
messageId: 'noViewDataDefinition',
});
}
},
TSTypeAliasDeclaration(node) {
if (node.id && node.id.name && node.id.name.endsWith('ViewData')) {
context.report({
node,
messageId: 'noViewDataDefinition',
});
}
},
};
},
};

View File

@@ -1,8 +1,11 @@
/**
* Base interface for ViewData objects
*
* All ViewData must be JSON-serializable.
* All ViewData must be JSON-serializable for SSR.
* This type ensures no class instances or functions are included.
*
* Note: We use 'any' here to allow complex DTO structures, but the
* architectural rule is that these must be plain JSON objects.
*/
export interface ViewData {
[key: string]: any;

View File

@@ -15,14 +15,15 @@
* - ViewModels are client-only
* - Must not expose methods that return Page DTO or API DTO
*
* Architecture Flow:
* 1. PageQuery returns Page DTO (server)
* 2. Presenter transforms Page DTO → ViewModel (client)
* 3. Presenter transforms ViewModel → ViewData (client)
* 4. Template receives ViewData only
* Architecture Flow (Website):
* 1. PageQuery/Builder returns ViewData (server)
* 2. ViewData contains plain DTOs (JSON-serializable)
* 3. Template receives ViewData (SSR)
* 4. ClientWrapper/Hook transforms DTO → ViewModel (client)
* 5. UI Components use ViewModel for computed logic
*
* ViewModels provide UI state and helpers.
* Presenters handle the transformation to ViewData.
* They are instantiated on the client to wrap plain data with logic.
*/
export abstract class ViewModel {

View File

@@ -0,0 +1,10 @@
export class MembershipFeeTypeDisplay {
static format(type: string): string {
switch (type) {
case 'season': return 'Per Season';
case 'monthly': return 'Monthly';
case 'per_race': return 'Per Race';
default: return type;
}
}
}

View File

@@ -0,0 +1,5 @@
export class PayerTypeDisplay {
static format(type: string): string {
return type.charAt(0).toUpperCase() + type.slice(1);
}
}

View File

@@ -0,0 +1,5 @@
export class PaymentTypeDisplay {
static format(type: string): string {
return type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
}
}

View File

@@ -0,0 +1,10 @@
export class PrizeTypeDisplay {
static format(type: string): string {
switch (type) {
case 'cash': return 'Cash Prize';
case 'merchandise': return 'Merchandise';
case 'other': return 'Other';
default: return type;
}
}
}

View File

@@ -0,0 +1,5 @@
export class TransactionTypeDisplay {
static format(type: string): string {
return type.charAt(0).toUpperCase() + type.slice(1);
}
}

View File

@@ -1,8 +1,6 @@
import { ViewData } from "../contracts/view-data/ViewData";
import { ViewData } from '../contracts/view-data/ViewData';
export interface BillingViewData extends ViewData {
paymentMethods: Array<{
export interface PaymentMethodViewData extends ViewData {
id: string;
type: 'card' | 'bank' | 'sepa';
last4: string;
@@ -13,8 +11,9 @@ export interface BillingViewData extends ViewData {
bankName?: string;
displayLabel: string;
expiryDisplay: string | null;
}>;
invoices: Array<{
}
export interface InvoiceViewData extends ViewData {
id: string;
invoiceNumber: string;
date: string;
@@ -30,8 +29,9 @@ export interface BillingViewData extends ViewData {
formattedVatAmount: string;
formattedDate: string;
isOverdue: boolean;
}>;
stats: {
}
export interface BillingStatsViewData extends ViewData {
totalSpent: number;
pendingAmount: number;
nextPaymentDate: string;
@@ -43,5 +43,10 @@ export interface BillingViewData extends ViewData {
formattedNextPaymentAmount: string;
formattedAverageMonthlySpend: string;
formattedNextPaymentDate: string;
};
}
export interface BillingViewData extends ViewData {
paymentMethods: PaymentMethodViewData[];
invoices: InvoiceViewData[];
stats: BillingStatsViewData;
}

View File

@@ -0,0 +1,5 @@
export interface CompleteOnboardingViewData {
success: boolean;
driverId?: string;
errorMessage?: string;
}

View File

@@ -0,0 +1,4 @@
export interface DeleteMediaViewData {
success: boolean;
error?: string;
}

View File

@@ -0,0 +1,10 @@
export interface DriverSummaryData {
driverId: string;
driverName: string;
avatarUrl: string | null;
rating: number | null;
rank: number | null;
roleBadgeText: string;
roleBadgeClasses: string;
profileUrl: string;
}

View File

@@ -0,0 +1,14 @@
/**
* ViewData for Driver
* This is the JSON-serializable input for the Template.
*/
export interface DriverViewData {
id: string;
name: string;
avatarUrl: string | null;
iracingId?: string;
rating?: number;
country?: string;
bio?: string;
joinedAt?: string;
}

View File

@@ -1,7 +1,4 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
export interface LeaderboardDriverItem extends ViewData {
export interface LeaderboardDriverItem {
id: string;
name: string;
rating: number;

View File

@@ -1,7 +1,4 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
export interface LeaderboardTeamItem extends ViewData {
export interface LeaderboardTeamItem {
id: string;
name: string;
tag: string;

View File

@@ -1,9 +1,7 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import type { LeaderboardDriverItem } from './LeaderboardDriverItem';
import type { LeaderboardTeamItem } from './LeaderboardTeamItem';
export interface LeaderboardsViewData extends ViewData {
export interface LeaderboardsViewData {
drivers: LeaderboardDriverItem[];
teams: LeaderboardTeamItem[];
}

View File

@@ -0,0 +1,12 @@
/**
* ViewData for LeagueAdminRosterJoinRequest
* This is the JSON-serializable input for the Template.
*/
export interface LeagueAdminRosterJoinRequestViewData {
id: string;
leagueId: string;
driverId: string;
driverName: string;
requestedAtIso: string;
message?: string;
}

View File

@@ -0,0 +1,12 @@
import type { MembershipRole } from '../types/MembershipRole';
/**
* ViewData for LeagueAdminRosterMember
* This is the JSON-serializable input for the Template.
*/
export interface LeagueAdminRosterMemberViewData {
driverId: string;
driverName: string;
role: MembershipRole;
joinedAtIso: string;
}

View File

@@ -1,10 +1,9 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
export interface AdminScheduleRaceData extends ViewData {
id: string;
name: string;
track: string;
car: string;
scheduledAt: string; // ISO string
/**
* ViewData for LeagueAdminSchedule
* This is the JSON-serializable input for the Template.
*/
export interface LeagueAdminScheduleViewData {
seasonId: string;
published: boolean;
races: any[];
}

View File

@@ -0,0 +1,11 @@
import type { LeagueMemberViewData } from './LeagueMemberViewData';
/**
* ViewData for LeagueAdmin
* This is the JSON-serializable input for the Template.
*/
export interface LeagueAdminViewData {
config: unknown;
members: LeagueMemberViewData[];
joinRequests: any[];
}

View File

@@ -0,0 +1,9 @@
/**
* ViewData for LeagueCard
* This is the JSON-serializable input for the Template.
*/
export interface LeagueCardViewData {
id: string;
name: string;
description?: string;
}

View File

@@ -0,0 +1,41 @@
import type { DriverViewData } from './DriverViewData';
export interface SponsorInfo {
id: string;
name: string;
logoUrl?: string;
websiteUrl?: string;
tier: 'main' | 'secondary';
tagline?: string;
}
export interface LeagueMembershipWithRole {
driverId: string;
role: 'owner' | 'admin' | 'steward' | 'member';
status: 'active' | 'inactive';
joinedAt: string;
}
export interface LeagueDetailPageViewData {
id: string;
name: string;
description?: string;
ownerId: string;
createdAt: string;
settings: {
maxDrivers?: number;
};
socialLinks?: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
owner: DriverViewData | null;
scoringConfig: any | null;
drivers: DriverViewData[];
memberships: LeagueMembershipWithRole[];
allRaces: any[];
averageSOF: number | null;
completedRacesCount: number;
sponsors: SponsorInfo[];
}

View File

@@ -1,140 +1,31 @@
import { ViewData } from '../contracts/view-data/ViewData';
import type { DriverViewData } from './DriverViewData';
import type { RaceViewData } from './RaceViewData';
/**
* LeagueDetailViewData - Pure ViewData for LeagueDetailTemplate
* Contains only raw serializable data, no methods or computed properties
*/
export interface LeagueInfoData {
name: string;
description?: string;
membersCount: number;
racesCount: number;
avgSOF: number | null;
structure: string;
scoring: string;
createdAt: string;
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
}
export interface SponsorInfo {
export interface LeagueViewData {
id: string;
name: string;
tier: 'main' | 'secondary';
logoUrl?: string;
websiteUrl?: string;
tagline?: string;
}
export interface LiveRaceData {
id: string;
name: string;
date: string;
registeredCount?: number;
strengthOfField?: number;
}
export interface DriverSummaryData {
driverId: string;
driverName: string;
avatarUrl: string | null;
rating: number | null;
rank: number | null;
roleBadgeText: string;
roleBadgeClasses: string;
profileUrl: string;
}
export interface SponsorMetric {
icon: any; // React component (lucide-react icon)
label: string;
value: string | number;
color?: string;
trend?: {
value: number;
isPositive: boolean;
game: string;
tier: 'premium' | 'standard' | 'starter';
season: string;
description: string;
drivers: number;
races: number;
completedRaces: number;
totalImpressions: number;
avgViewsPerRace: number;
engagement: number;
rating: number;
seasonStatus: 'active' | 'upcoming' | 'completed';
seasonDates: { start: string; end: string };
nextRace?: { name: string; date: string };
sponsorSlots: {
main: { available: boolean; price: number; benefits: string[] };
secondary: { available: number; total: number; price: number; benefits: string[] };
};
}
export interface SponsorshipSlot {
tier: 'main' | 'secondary';
available: boolean;
price: number;
benefits: string[];
}
export interface NextRaceInfo {
id: string;
name: string;
date: string;
track?: string;
car?: string;
}
export interface SeasonProgress {
completedRaces: number;
totalRaces: number;
percentage: number;
}
export interface RecentResult {
raceId: string;
raceName: string;
position: number;
points: number;
finishedAt: string;
}
export interface LeagueDetailViewData extends ViewData {
// Basic info
leagueId: string;
name: string;
description: string;
logoUrl?: string;
// Info card data
info: LeagueInfoData;
// Live races
runningRaces: LiveRaceData[];
// Sponsors
sponsors: SponsorInfo[];
// Management
ownerSummary: DriverSummaryData | null;
adminSummaries: DriverSummaryData[];
stewardSummaries: DriverSummaryData[];
memberSummaries: DriverSummaryData[];
// Sponsor insights (for sponsor mode)
sponsorInsights: {
avgViewsPerRace: number;
engagementRate: string;
estimatedReach: number;
tier: 'premium' | 'standard' | 'starter';
trustScore: number;
discordMembers: number;
monthlyActivity: number;
mainSponsorAvailable: boolean;
secondarySlotsAvailable: number;
mainSponsorPrice: number;
secondaryPrice: number;
totalImpressions: number;
metrics: SponsorMetric[];
slots: SponsorshipSlot[];
} | null;
// New fields for enhanced league pages
nextRace?: NextRaceInfo;
seasonProgress?: SeasonProgress;
recentResults?: RecentResult[];
// Admin fields
walletBalance?: number;
pendingProtestsCount?: number;
pendingJoinRequestsCount?: number;
export interface LeagueDetailViewData {
league: LeagueViewData;
drivers: (DriverViewData & { impressions: number })[];
races: (RaceViewData & { views: number })[];
}

View File

@@ -0,0 +1,11 @@
/**
* ViewData for LeagueJoinRequest
* This is the JSON-serializable input for the Template.
*/
export interface LeagueJoinRequestViewData {
id: string;
leagueId: string;
driverId: string;
requestedAt: string;
isAdmin: boolean;
}

View File

@@ -0,0 +1,11 @@
/**
* ViewData for LeagueMember
* This is the JSON-serializable input for the Template.
*/
export interface LeagueMemberViewData {
driverId: string;
currentUserId: string;
driver?: any;
role: string;
joinedAt: string;
}

View File

@@ -0,0 +1,9 @@
import type { LeagueMemberViewData } from './LeagueMemberViewData';
/**
* ViewData for LeagueMemberships
* This is the JSON-serializable input for the Template.
*/
export interface LeagueMembershipsViewData {
memberships: LeagueMemberViewData[];
}

View File

@@ -0,0 +1,9 @@
export interface LeaguePageDetailViewData {
id: string;
name: string;
description: string;
ownerId: string;
ownerName: string;
isAdmin: boolean;
mainSponsor: { name: string; logoUrl: string; websiteUrl: string } | null;
}

View File

@@ -1,26 +1,7 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
/**
* LeagueScheduleViewData - Pure ViewData for LeagueScheduleTemplate
* Contains only raw serializable data, no methods or computed properties
* ViewData for LeagueSchedule
* This is the JSON-serializable input for the Template.
*/
export interface ScheduleRaceData {
id: string;
name: string;
track: string;
car: string;
scheduledAt: string;
status: string;
}
export interface LeagueScheduleViewData extends ViewData {
leagueId: string;
races: ScheduleRaceData[];
seasons: Array<{
seasonId: string;
name: string;
status: string;
}>;
export interface LeagueScheduleViewData {
races: any[];
}

View File

@@ -0,0 +1,12 @@
/**
* ViewData for LeagueScoringChampionship
*/
export interface LeagueScoringChampionshipViewData {
id: string;
name: string;
type: string;
sessionTypes: string[];
pointsPreview?: Array<{ sessionType: string; position: number; points: number }> | null;
bonusSummary?: string[] | null;
dropPolicyDescription?: string;
}

View File

@@ -0,0 +1,10 @@
/**
* ViewData for LeagueScoringConfig
* This is the JSON-serializable input for the Template.
*/
export interface LeagueScoringConfigViewData {
gameName: string;
scoringPresetName?: string;
dropPolicySummary?: string;
championships?: any[];
}

View File

@@ -0,0 +1,16 @@
/**
* ViewData for LeagueScoringPreset
*/
export interface LeagueScoringPresetViewData {
id: string;
name: string;
sessionSummary: string;
bonusSummary?: string;
defaultTimings: {
practiceMinutes: number;
qualifyingMinutes: number;
sprintRaceMinutes: number;
mainRaceMinutes: number;
sessionCount: number;
};
}

View File

@@ -0,0 +1,7 @@
/**
* ViewData for league scoring presets
*/
export interface LeagueScoringPresetsViewData {
presets: any[];
totalCount?: number;
}

View File

@@ -0,0 +1,15 @@
import type { LeagueConfigFormModel } from '../types/LeagueConfigFormModel';
import type { LeagueScoringPresetViewData } from './LeagueScoringPresetViewData';
/**
* ViewData for LeagueScoringSection
*/
export interface LeagueScoringSectionViewData {
form: LeagueConfigFormModel;
presets: LeagueScoringPresetViewData[];
options?: {
readOnly?: boolean;
patternOnly?: boolean;
championshipsOnly?: boolean;
};
}

View File

@@ -1,21 +1,18 @@
import { ViewData } from "../contracts/view-data/ViewData";
import type { LeagueConfigFormModel } from '../types/LeagueConfigFormModel';
export interface LeagueSettingsViewData extends ViewData {
leagueId: string;
/**
* ViewData for LeagueSettings
* This is the JSON-serializable input for the Template.
*/
export interface LeagueSettingsViewData {
league: {
id: string;
name: string;
description: string;
visibility: 'public' | 'private';
ownerId: string;
createdAt: string;
updatedAt: string;
};
config: {
maxDrivers: number;
scoringPresetId: string;
allowLateJoin: boolean;
requireApproval: boolean;
};
config: LeagueConfigFormModel;
presets: any[];
owner: any | null;
members: any[];
}

View File

@@ -1,18 +1,11 @@
import type { StandingEntryViewData } from './StandingEntryViewData';
export interface DriverData {
id: string;
name: string;
avatarUrl: string | null;
iracingId?: string;
rating?: number;
country?: string;
}
export interface LeagueMembershipData {
driverId: string;
leagueId: string;
role: 'owner' | 'admin' | 'steward' | 'member';
joinedAt: string;
status: 'active' | 'pending' | 'banned';
/**
* ViewData for LeagueStandings
* This is the JSON-serializable input for the Template.
*/
export interface LeagueStandingsViewData {
standings: StandingEntryViewData[];
drivers: any[];
memberships: any[];
}

View File

@@ -0,0 +1,6 @@
/**
* ViewData for LeagueStats
*/
export interface LeagueStatsViewData {
totalLeagues: number;
}

View File

@@ -0,0 +1,31 @@
/**
* ViewData for LeagueSummary
* This is the JSON-serializable input for the Template.
*/
export interface LeagueSummaryViewData {
id: string;
name: string;
description: string | null;
logoUrl: string | null;
ownerId: string;
createdAt: string;
maxDrivers: number;
usedDriverSlots: number;
activeDriversCount?: number;
nextRaceAt?: string;
maxTeams?: number;
usedTeamSlots?: number;
structureSummary: string;
scoringPatternSummary?: string;
timingSummary: string;
category?: string | null;
scoring?: {
gameId: string;
gameName: string;
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
scoringPresetId: string;
scoringPresetName: string;
dropPolicySummary: string;
scoringPatternSummary: string;
};
}

View File

@@ -1,16 +1,17 @@
import { ViewData } from "../contracts/view-data/ViewData";
import type { WalletTransactionViewData } from './WalletTransactionViewData';
export interface LeagueWalletTransactionViewData extends ViewData {
id: string;
type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize';
amount: number;
formattedAmount: string;
amountColor: string;
description: string;
createdAt: string;
formattedDate: string;
status: 'completed' | 'pending' | 'failed';
statusColor: string;
typeColor: string;
/**
* ViewData for LeagueWallet
* This is the JSON-serializable input for the Template.
*/
export interface LeagueWalletViewData {
balance: number;
currency: string;
totalRevenue: number;
totalFees: number;
totalWithdrawals: number;
pendingPayouts: number;
transactions: WalletTransactionViewData[];
canWithdraw: boolean;
withdrawalBlockReason?: string;
}

View File

@@ -1,9 +1,14 @@
import { MediaAsset } from '@/components/media/MediaGallery';
import { ViewData } from '../contracts/view-data/ViewData';
export interface MediaAssetViewData {
id: string;
src: string;
title: string;
category: string;
date?: string;
dimensions?: string;
}
export interface MediaViewData extends ViewData {
assets: MediaAsset[];
export interface MediaViewData {
assets: MediaAssetViewData[];
categories: { label: string; value: string }[];
title: string;
description?: string;

View File

@@ -0,0 +1,14 @@
/**
* ViewData for MembershipFee
* This is the JSON-serializable input for the Template.
*/
export interface MembershipFeeViewData {
id: string;
leagueId: string;
seasonId?: string;
type: string;
amount: number;
enabled: boolean;
createdAt: string;
updatedAt: string;
}

View File

@@ -0,0 +1,11 @@
/**
* ViewData for NotificationSettings
*/
export interface NotificationSettingsViewData {
emailNewSponsorships: boolean;
emailWeeklyReport: boolean;
emailRaceAlerts: boolean;
emailPaymentAlerts: boolean;
emailNewOpportunities: boolean;
emailContractExpiry: boolean;
}

View File

@@ -0,0 +1,18 @@
/**
* ViewData for Payment
* This is the JSON-serializable input for the Template.
*/
export interface PaymentViewData {
id: string;
type: string;
amount: number;
platformFee: number;
netAmount: number;
payerId: string;
payerType: string;
leagueId: string;
seasonId?: string;
status: string;
createdAt: string;
completedAt?: string;
}

View File

@@ -0,0 +1,9 @@
/**
* ViewData for PrivacySettings
*/
export interface PrivacySettingsViewData {
publicProfile: boolean;
showStats: boolean;
showActiveSponsorships: boolean;
allowDirectContact: boolean;
}

View File

@@ -0,0 +1,18 @@
/**
* ViewData for Prize
* This is the JSON-serializable input for the Template.
*/
export interface PrizeViewData {
id: string;
leagueId: string;
seasonId: string;
position: number;
name: string;
amount: number;
type: string;
description?: string;
awarded: boolean;
awardedTo?: string;
awardedAt?: string;
createdAt: string;
}

View File

@@ -0,0 +1,12 @@
/**
* ViewData for ProfileOverview
* This is the JSON-serializable input for the Template.
*/
export interface ProfileOverviewViewData {
currentDriver: any | null;
stats: any | null;
finishDistribution: any | null;
teamMemberships: any[];
socialSummary: any;
extendedProfile: any | null;
}

View File

@@ -0,0 +1,4 @@
export interface ProtestDriverViewData {
id: string;
name: string;
}

View File

@@ -0,0 +1,21 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
/**
* ViewData for Protest
* This is the JSON-serializable input for the Template.
*/
export interface ProtestViewData extends ViewData {
id: string;
raceId: string;
protestingDriverId: string;
accusedDriverId: string;
description: string;
submittedAt: string;
filedAt?: string;
status: string;
reviewedAt?: string;
decisionNotes?: string;
incident?: { lap?: number; description?: string } | null;
proofVideoUrl?: string | null;
comment?: string | null;
}

View File

@@ -0,0 +1,12 @@
/**
* ViewData for RaceDetailEntry
* This is the JSON-serializable input for the Template.
*/
export interface RaceDetailEntryViewData {
id: string;
name: string;
country: string;
avatarUrl: string;
isCurrentUser: boolean;
rating: number | null;
}

View File

@@ -0,0 +1,14 @@
/**
* ViewData for RaceDetailUserResult
* This is the JSON-serializable input for the Template.
*/
export interface RaceDetailUserResultViewData {
position: number;
startPosition: number;
incidents: number;
fastestLap: number;
positionChange: number;
isPodium: boolean;
isClean: boolean;
ratingChange: number;
}

View File

@@ -0,0 +1,37 @@
import type { RaceDetailEntryViewData } from './RaceDetailEntryViewData';
import type { RaceDetailUserResultViewData } from './RaceDetailUserResultViewData';
export interface RaceDetailsRaceViewData {
id: string;
track: string;
car: string;
scheduledAt: string;
status: string;
sessionType: string;
}
export interface RaceDetailsLeagueViewData {
id: string;
name: string;
description?: string | null;
settings?: unknown;
}
export interface RaceDetailsRegistrationViewData {
canRegister: boolean;
isUserRegistered: boolean;
}
/**
* ViewData for RaceDetails
* This is the JSON-serializable input for the Template.
*/
export interface RaceDetailsViewData {
race: RaceDetailsRaceViewData | null;
league: RaceDetailsLeagueViewData | null;
entryList: RaceDetailEntryViewData[];
registration: RaceDetailsRegistrationViewData;
userResult: RaceDetailUserResultViewData | null;
canReopenRace: boolean;
error?: string;
}

View File

@@ -0,0 +1,17 @@
/**
* ViewData for RaceListItem
* This is the JSON-serializable input for the Template.
*/
export interface RaceListItemViewData {
id: string;
track: string;
car: string;
scheduledAt: string;
status: string;
leagueId: string;
leagueName: string;
strengthOfField: number | null;
isUpcoming: boolean;
isLive: boolean;
isPast: boolean;
}

View File

@@ -0,0 +1,18 @@
/**
* ViewData for RaceResult
* This is the JSON-serializable input for the Template.
*/
export interface RaceResultViewData {
driverId: string;
driverName: string;
avatarUrl: string;
position: number;
startPosition: number;
incidents: number;
fastestLap: number;
positionChange: number;
isPodium: boolean;
isClean: boolean;
id: string;
raceId: string;
}

View File

@@ -0,0 +1,19 @@
import type { RaceResultViewData } from './RaceResultViewData';
/**
* ViewData for RaceResultsDetail
* This is the JSON-serializable input for the Template.
*/
export interface RaceResultsDetailViewData {
raceId: string;
track: string;
currentUserId: string;
results: RaceResultViewData[];
league?: { id: string; name: string };
race?: { id: string; track: string; scheduledAt: string };
drivers: { id: string; name: string }[];
pointsSystem: Record<number, number>;
fastestLapTime: number;
penalties: { driverId: string; type: string; value?: number }[];
currentDriverId: string;
}

View File

@@ -0,0 +1,7 @@
/**
* ViewData for RaceStats
* This is the JSON-serializable input for the Template.
*/
export interface RaceStatsViewData {
totalRaces: number;
}

View File

@@ -1,13 +1,19 @@
/**
* Race Stewarding View Data
*
* ViewData for the race stewarding page template.
* JSON-serializable, template-ready data structure.
* ViewData for RaceStewarding
* This is the JSON-serializable input for the Template.
*/
import { ViewData } from "../contracts/view-data/ViewData";
export interface Protest {
export interface RaceStewardingViewData {
race: {
id: string;
track: string;
scheduledAt: string;
status: string;
} | null;
league: {
id: string;
name: string;
} | null;
protests: Array<{
id: string;
protestingDriverId: string;
accusedDriverId: string;
@@ -17,39 +23,16 @@ export interface Protest {
};
filedAt: string;
status: string;
proofVideoUrl?: string;
decisionNotes?: string;
}
export interface Penalty {
proofVideoUrl?: string;
}>;
penalties: Array<{
id: string;
driverId: string;
type: string;
value: number;
reason: string;
notes?: string;
}
export interface Driver {
id: string;
name: string;
}
export interface RaceStewardingViewData extends ViewData {
race?: {
id: string;
track: string;
scheduledAt: string;
} | null;
league?: {
id: string;
} | null;
pendingProtests: Protest[];
resolvedProtests: Protest[];
penalties: Penalty[];
driverMap: Record<string, Driver>;
pendingCount: number;
resolvedCount: number;
penaltiesCount: number;
}>;
driverMap: Record<string, { id: string; name: string }>;
}

View File

@@ -0,0 +1,19 @@
/**
* Race View Data
*
* ViewData for the race template.
* JSON-serializable, template-ready data structure.
*/
import { ViewData } from "../contracts/view-data/ViewData";
export interface RaceViewData extends ViewData {
id: string;
name: string;
date: string;
track: string;
car: string;
status?: string;
registeredCount?: number;
strengthOfField?: number;
}

View File

@@ -0,0 +1,9 @@
/**
* ViewData for RaceWithSOF
* This is the JSON-serializable input for the Template.
*/
export interface RaceWithSOFViewData {
id: string;
track: string;
strengthOfField: number | null;
}

View File

@@ -0,0 +1,10 @@
import { ViewData } from '../contracts/view-data/ViewData';
import { RaceListItemViewData } from './RaceListItemViewData';
/**
* ViewData for RacesPage
* This is the JSON-serializable input for the Template.
*/
export interface RacesPageViewData extends ViewData {
races: RaceListItemViewData[];
}

View File

@@ -0,0 +1,8 @@
/**
* Record engagement input view data
*/
export interface RecordEngagementInputViewData {
eventType: string;
userId?: string;
metadata?: Record<string, unknown>;
}

View File

@@ -0,0 +1,7 @@
/**
* Record page view input view data
*/
export interface RecordPageViewInputViewData {
path: string;
userId?: string;
}

View File

@@ -0,0 +1,6 @@
/**
* Record page view output view data
*/
export interface RecordPageViewOutputViewData {
pageViewId: string;
}

View File

@@ -0,0 +1,6 @@
/**
* ViewData for RemoveMember
*/
export interface RemoveMemberViewData {
success: boolean;
}

View File

@@ -0,0 +1,10 @@
/**
* ViewData for RenewalAlert
*/
export interface RenewalAlertViewData {
id: string;
name: string;
type: 'league' | 'team' | 'driver' | 'race' | 'platform';
renewDate: string;
price: number;
}

View File

@@ -0,0 +1,18 @@
import type { LeagueConfigFormModel } from '../types/LeagueConfigFormModel';
import type { LeagueScoringPresetViewData } from './LeagueScoringPresetViewData';
export interface CustomPointsConfig {
racePoints: number[];
poleBonusPoints: number;
fastestLapPoints: number;
leaderLapPoints: number;
}
/**
* ViewData for ScoringConfiguration
*/
export interface ScoringConfigurationViewData {
config: LeagueConfigFormModel['scoring'];
presets: LeagueScoringPresetViewData[];
customPoints?: CustomPointsConfig;
}

View File

@@ -1,38 +1,7 @@
import { ViewData } from "../contracts/view-data/ViewData";
export interface SponsorDashboardViewData extends ViewData {
/**
* ViewData for SponsorDashboard
*/
export interface SponsorDashboardViewData {
sponsorId: string;
sponsorName: string;
totalImpressions: string;
totalInvestment: string;
metrics: {
impressionsChange: number;
viewersChange: number;
exposureChange: number;
};
categoryData: {
leagues: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
teams: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
drivers: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
races: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
platform: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
};
sponsorships: Record<string, unknown>; // From DTO
activeSponsorships: number;
formattedTotalInvestment: string;
costPerThousandViews: string;
upcomingRenewals: Array<{
id: string;
type: 'league' | 'team' | 'driver' | 'race' | 'platform';
name: string;
formattedRenewDate: string;
formattedPrice: string;
}>;
recentActivity: Array<{
id: string;
message: string;
time: string;
typeColor: string;
formattedImpressions?: string | null;
}>;
}

View File

@@ -0,0 +1,25 @@
/**
* ViewData for SponsorProfile
*/
export interface SponsorProfileViewData {
companyName: string;
contactName: string;
contactEmail: string;
contactPhone: string;
website: string;
description: string;
logoUrl: string | null;
industry: string;
address: {
street: string;
city: string;
country: string;
postalCode: string;
};
taxId: string;
socialLinks: {
twitter: string;
linkedin: string;
instagram: string;
};
}

View File

@@ -0,0 +1,12 @@
import { NotificationSettingsViewData } from './NotificationSettingsViewData';
import { PrivacySettingsViewData } from './PrivacySettingsViewData';
import type { SponsorProfileViewData } from './SponsorProfileViewData';
/**
* ViewData for SponsorSettings
*/
export interface SponsorSettingsViewData {
profile: SponsorProfileViewData;
notifications: NotificationSettingsViewData;
privacy: PrivacySettingsViewData;
}

View File

@@ -0,0 +1,10 @@
import type { SponsorshipDetailViewData } from './SponsorshipDetailViewData';
/**
* ViewData for SponsorSponsorships
*/
export interface SponsorSponsorshipsViewData {
sponsorId: string;
sponsorName: string;
sponsorships: SponsorshipDetailViewData[];
}

View File

@@ -0,0 +1,9 @@
/**
* ViewData for Sponsor
*/
export interface SponsorViewData {
id: string;
name: string;
logoUrl?: string;
websiteUrl?: string;
}

View File

@@ -0,0 +1,18 @@
/**
* ViewData for SponsorshipDetail
*/
export interface SponsorshipDetailViewData {
id: string;
leagueId: string;
leagueName: string;
seasonId: string;
seasonName: string;
tier: 'main' | 'secondary';
status: string;
amount: number;
currency: string;
type: string;
entityName: string;
price: number;
impressions: number;
}

View File

@@ -0,0 +1,8 @@
/**
* ViewData for SponsorshipPricing
*/
export interface SponsorshipPricingViewData {
mainSlotPrice: number;
secondarySlotPrice: number;
currency: string;
}

View File

@@ -0,0 +1,17 @@
/**
* ViewData for SponsorshipRequest
*/
export interface SponsorshipRequestViewData {
id: string;
sponsorId: string;
sponsorName: string;
sponsorLogo?: string;
tier: 'main' | 'secondary';
offeredAmount: number;
currency: string;
formattedAmount: string;
message?: string;
createdAt: string;
platformFee: number;
netAmount: number;
}

View File

@@ -0,0 +1,23 @@
/**
* Interface for sponsorship data input
*/
export interface SponsorshipViewData {
id: string;
type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';
entityId: string;
entityName: string;
tier?: 'main' | 'secondary';
status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';
applicationDate?: string | Date;
approvalDate?: string | Date;
rejectionReason?: string;
startDate: string | Date;
endDate: string | Date;
price: number;
impressions: number;
impressionsChange?: number;
engagement?: number;
details?: string;
entityOwner?: string;
applicationMessage?: string;
}

View File

@@ -0,0 +1,17 @@
/**
* ViewData for StandingEntry
* This is the JSON-serializable input for the Template.
*/
export interface StandingEntryViewData {
driverId: string;
position: number;
points: number;
wins: number;
podiums: number;
races: number;
leaderPoints: number;
nextPoints: number;
currentUserId: string;
previousPosition?: number;
driver?: any;
}

View File

@@ -0,0 +1,9 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
export interface TeamCardViewData extends ViewData {
id: string;
name: string;
tag: string;
description: string;
logoUrl?: string;
}

View File

@@ -0,0 +1,21 @@
/**
* ViewData for TeamDetails
*/
export interface TeamDetailsViewData {
team: {
id: string;
name: string;
tag: string;
description?: string;
ownerId: string;
leagues: string[];
createdAt?: string;
specialization?: string;
region?: string;
languages?: string[];
category?: string;
};
membership: { role: string; joinedAt: string; isActive: boolean } | null;
canManage: boolean;
currentUserId: string;
}

View File

@@ -0,0 +1,14 @@
/**
* ViewData for TeamJoinRequest
*/
export interface TeamJoinRequestViewData {
requestId: string;
driverId: string;
driverName: string;
teamId: string;
status: string;
requestedAt: string;
avatarUrl?: string;
currentUserId: string;
isOwner: boolean;
}

View File

@@ -1,14 +1,13 @@
import { ViewData } from '../contracts/view-data/ViewData';
import type { TeamSummaryViewModel } from '../view-models/TeamSummaryViewModel';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
export type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
export type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
export interface TeamLeaderboardViewData extends ViewData {
teams: TeamSummaryViewModel[];
teams: TeamListItemDTO[];
searchQuery: string;
filterLevel: SkillLevel | 'all';
sortBy: SortBy;
filteredAndSortedTeams: TeamSummaryViewModel[];
filteredAndSortedTeams: TeamListItemDTO[];
}

View File

@@ -0,0 +1,15 @@
export type TeamMemberRole = 'owner' | 'manager' | 'member';
/**
* ViewData for TeamMember
*/
export interface TeamMemberViewData {
driverId: string;
driverName: string;
role: string;
joinedAt: string;
isActive: boolean;
avatarUrl?: string;
currentUserId: string;
teamOwnerId: string;
}

View File

@@ -0,0 +1,18 @@
export interface TeamSummaryViewData {
id: string;
name: string;
tag: string;
memberCount: number;
description?: string;
totalWins: number;
totalRaces: number;
performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
isRecruiting: boolean;
specialization: 'endurance' | 'sprint' | 'mixed' | undefined;
region: string | undefined;
languages: string[];
leagues: string[];
logoUrl: string | undefined;
rating: number | undefined;
category: string | undefined;
}

View File

@@ -0,0 +1,6 @@
export interface UpcomingRaceCardViewData {
id: string;
track: string;
car: string;
scheduledAt: string;
}

View File

@@ -0,0 +1,7 @@
/**
* ViewData for UpdateAvatar
*/
export interface UpdateAvatarViewData {
success: boolean;
error?: string;
}

View File

@@ -0,0 +1,6 @@
/**
* ViewData for UpdateTeam
*/
export interface UpdateTeamViewData {
success: boolean;
}

View File

@@ -0,0 +1,9 @@
/**
* ViewData for UploadMedia
*/
export interface UploadMediaViewData {
success: boolean;
mediaId?: string;
url?: string;
error?: string;
}

View File

@@ -0,0 +1,9 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
export interface UserProfileViewData extends ViewData {
id: string;
name: string;
avatarUrl?: string;
iracingId?: string;
rating?: number;
}

View File

@@ -0,0 +1,17 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
/**
* ViewData for WalletTransaction
* This is the JSON-serializable input for the Template.
*/
export interface WalletTransactionViewData extends ViewData {
id: string;
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize' | 'deposit';
description: string;
amount: number;
fee: number;
netAmount: number;
date: string; // ISO string
status: 'completed' | 'pending' | 'failed';
reference?: string;
}

View File

@@ -0,0 +1,18 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import type { WalletTransactionViewData } from './WalletTransactionViewData';
/**
* ViewData for Wallet
* This is the JSON-serializable input for the Template.
*/
export interface WalletViewData extends ViewData {
id: string;
leagueId: string;
balance: number;
totalRevenue: number;
totalPlatformFees: number;
totalWithdrawn: number;
createdAt: string;
currency: string;
transactions?: WalletTransactionViewData[];
}

View File

@@ -9,21 +9,19 @@ import { ActivityItemViewData } from "../view-data/ActivityItemViewData";
import { ViewModel } from "../contracts/view-models/ViewModel";
export class ActivityItemViewModel extends ViewModel {
readonly id: string;
readonly type: string;
readonly message: string;
readonly time: string;
readonly impressions?: number;
private readonly data: ActivityItemViewData;
constructor(viewData: ActivityItemViewData) {
constructor(data: ActivityItemViewData) {
super();
this.id = viewData.id;
this.type = viewData.type;
this.message = viewData.message;
this.time = viewData.time;
this.impressions = viewData.impressions;
this.data = data;
}
get id(): string { return this.data.id; }
get type(): string { return this.data.type; }
get message(): string { return this.data.message; }
get time(): string { return this.data.time; }
get impressions(): number | undefined { return this.data.impressions; }
get typeColor(): string {
const colors: Record<string, string> = {
race: 'bg-warning-amber',
@@ -36,6 +34,7 @@ export class ActivityItemViewModel extends ViewModel {
}
get formattedImpressions(): string | null {
// Client-only formatting
return this.impressions ? this.impressions.toLocaleString() : null;
}
}

View File

@@ -1,10 +1,8 @@
import type { AdminUserViewData } from '@/lib/view-data/AdminUserViewData';
import type { DashboardStatsViewData } from '@/lib/view-data/DashboardStatsViewData';
import { ViewModel } from "../contracts/view-models/ViewModel";
import { UserStatusDisplay } from "../display-objects/UserStatusDisplay";
import { UserRoleDisplay } from "../display-objects/UserRoleDisplay";
import { DateDisplay } from "../display-objects/DateDisplay";
import { ActivityLevelDisplay } from "../display-objects/ActivityLevelDisplay";
import { UserStatusDisplay } from "@/lib/display-objects/UserStatusDisplay";
import { UserRoleDisplay } from "@/lib/display-objects/UserRoleDisplay";
import { DateDisplay } from "@/lib/display-objects/DateDisplay";
/**
* AdminUserViewModel
@@ -13,159 +11,48 @@ import { ActivityLevelDisplay } from "../display-objects/ActivityLevelDisplay";
* Transforms API DTO into UI-ready state with formatting and derived fields.
*/
export class AdminUserViewModel extends ViewModel {
id: string;
email: string;
displayName: string;
roles: string[];
status: string;
isSystemAdmin: boolean;
createdAt: Date;
updatedAt: Date;
lastLoginAt?: Date;
primaryDriverId?: string;
private readonly data: AdminUserViewData;
// UI-specific derived fields (primitive outputs only)
readonly roleBadges: string[];
readonly statusBadgeLabel: string;
readonly statusBadgeVariant: string;
readonly lastLoginFormatted: string;
readonly createdAtFormatted: string;
constructor(viewData: AdminUserViewData) {
constructor(data: AdminUserViewData) {
super();
this.id = viewData.id;
this.email = viewData.email;
this.displayName = viewData.displayName;
this.roles = viewData.roles;
this.status = viewData.status;
this.isSystemAdmin = viewData.isSystemAdmin;
this.createdAt = new Date(viewData.createdAt);
this.updatedAt = new Date(viewData.updatedAt);
this.lastLoginAt = viewData.lastLoginAt ? new Date(viewData.lastLoginAt) : undefined;
this.primaryDriverId = viewData.primaryDriverId;
this.data = data;
}
// Derive role badges using Display Object
this.roleBadges = this.roles.map(role => UserRoleDisplay.roleLabel(role));
get id(): string { return this.data.id; }
get email(): string { return this.data.email; }
get displayName(): string { return this.data.displayName; }
get roles(): string[] { return this.data.roles; }
get status(): string { return this.data.status; }
get isSystemAdmin(): boolean { return this.data.isSystemAdmin; }
get createdAt(): string { return this.data.createdAt; }
get updatedAt(): string { return this.data.updatedAt; }
get lastLoginAt(): string | undefined { return this.data.lastLoginAt; }
get primaryDriverId(): string | undefined { return this.data.primaryDriverId; }
// Derive status badge using Display Object
this.statusBadgeLabel = UserStatusDisplay.statusLabel(this.status);
this.statusBadgeVariant = UserStatusDisplay.statusVariant(this.status);
/** UI-specific: Role badges using Display Object */
get roleBadges(): string[] {
return this.roles.map(role => UserRoleDisplay.roleLabel(role));
}
// Format dates using Display Object
this.lastLoginFormatted = this.lastLoginAt
/** UI-specific: Status badge label using Display Object */
get statusBadgeLabel(): string {
return UserStatusDisplay.statusLabel(this.status);
}
/** UI-specific: Status badge variant using Display Object */
get statusBadgeVariant(): string {
return UserStatusDisplay.statusVariant(this.status);
}
/** UI-specific: Formatted last login date */
get lastLoginFormatted(): string {
return this.lastLoginAt
? DateDisplay.formatShort(this.lastLoginAt)
: 'Never';
this.createdAtFormatted = DateDisplay.formatShort(this.createdAt);
}
}
/**
* DashboardStatsViewModel
*
* View Model for admin dashboard statistics.
* Provides formatted statistics and derived metrics for UI.
*/
export class DashboardStatsViewModel extends ViewModel {
totalUsers: number;
activeUsers: number;
suspendedUsers: number;
deletedUsers: number;
systemAdmins: number;
recentLogins: number;
newUsersToday: number;
userGrowth: {
label: string;
value: number;
color: string;
}[];
roleDistribution: {
label: string;
value: number;
color: string;
}[];
statusDistribution: {
active: number;
suspended: number;
deleted: number;
};
activityTimeline: {
date: string;
newUsers: number;
logins: number;
}[];
// UI-specific derived fields (primitive outputs only)
readonly activeRate: number;
readonly activeRateFormatted: string;
readonly adminRatio: string;
readonly activityLevelLabel: string;
readonly activityLevelValue: 'low' | 'medium' | 'high';
constructor(viewData: DashboardStatsViewData) {
super();
this.totalUsers = viewData.totalUsers;
this.activeUsers = viewData.activeUsers;
this.suspendedUsers = viewData.suspendedUsers;
this.deletedUsers = viewData.deletedUsers;
this.systemAdmins = viewData.systemAdmins;
this.recentLogins = viewData.recentLogins;
this.newUsersToday = viewData.newUsersToday;
this.userGrowth = viewData.userGrowth;
this.roleDistribution = viewData.roleDistribution;
this.statusDistribution = viewData.statusDistribution;
this.activityTimeline = viewData.activityTimeline;
// Derive active rate
this.activeRate = this.totalUsers > 0 ? (this.activeUsers / this.totalUsers) * 100 : 0;
this.activeRateFormatted = `${Math.round(this.activeRate)}%`;
// Derive admin ratio
const nonAdmins = Math.max(1, this.totalUsers - this.systemAdmins);
this.adminRatio = `1:${Math.floor(nonAdmins / Math.max(1, this.systemAdmins))}`;
// Derive activity level using Display Object
const engagementRate = this.totalUsers > 0 ? (this.recentLogins / this.totalUsers) * 100 : 0;
this.activityLevelLabel = ActivityLevelDisplay.levelLabel(engagementRate);
this.activityLevelValue = ActivityLevelDisplay.levelValue(engagementRate);
}
}
/**
* UserListViewModel
*
* View Model for user list with pagination and filtering state.
*/
export class UserListViewModel extends ViewModel {
users: AdminUserViewModel[];
total: number;
page: number;
limit: number;
totalPages: number;
// UI-specific derived fields (primitive outputs only)
readonly hasUsers: boolean;
readonly showPagination: boolean;
readonly startIndex: number;
readonly endIndex: number;
constructor(data: {
users: AdminUserViewData[];
total: number;
page: number;
limit: number;
totalPages: number;
}) {
super();
this.users = data.users.map(viewData => new AdminUserViewModel(viewData));
this.total = data.total;
this.page = data.page;
this.limit = data.limit;
this.totalPages = data.totalPages;
// Derive UI state
this.hasUsers = this.users.length > 0;
this.showPagination = this.totalPages > 1;
this.startIndex = this.users.length > 0 ? (this.page - 1) * this.limit + 1 : 0;
this.endIndex = this.users.length > 0 ? (this.page - 1) * this.limit + this.users.length : 0;
}
/** UI-specific: Formatted creation date */
get createdAtFormatted(): string {
return DateDisplay.formatShort(this.createdAt);
}
}

View File

@@ -9,19 +9,18 @@ import { AnalyticsDashboardInputViewData } from "../view-data/AnalyticsDashboard
import { ViewModel } from "../contracts/view-models/ViewModel";
export class AnalyticsDashboardViewModel extends ViewModel {
readonly totalUsers: number;
readonly activeUsers: number;
readonly totalRaces: number;
readonly totalLeagues: number;
private readonly data: AnalyticsDashboardInputViewData;
constructor(viewData: AnalyticsDashboardInputViewData) {
constructor(data: AnalyticsDashboardInputViewData) {
super();
this.totalUsers = viewData.totalUsers;
this.activeUsers = viewData.activeUsers;
this.totalRaces = viewData.totalRaces;
this.totalLeagues = viewData.totalLeagues;
this.data = data;
}
get totalUsers(): number { return this.data.totalUsers; }
get activeUsers(): number { return this.data.activeUsers; }
get totalRaces(): number { return this.data.totalRaces; }
get totalLeagues(): number { return this.data.totalLeagues; }
/** UI-specific: User engagement rate */
get userEngagementRate(): number {
return this.totalUsers > 0 ? (this.activeUsers / this.totalUsers) * 100 : 0;

View File

@@ -6,24 +6,23 @@
*/
import { AnalyticsMetricsViewData } from "../view-data/AnalyticsMetricsViewData";
import { ViewModel } from "../contracts/view-models/ViewModel";
import { NumberDisplay } from "../display-objects/NumberDisplay";
import { DurationDisplay } from "../display-objects/DurationDisplay";
import { PercentDisplay } from "../display-objects/PercentDisplay";
import { NumberDisplay } from "@/lib/display-objects/NumberDisplay";
import { DurationDisplay } from "@/lib/display-objects/DurationDisplay";
import { PercentDisplay } from "@/lib/display-objects/PercentDisplay";
export class AnalyticsMetricsViewModel extends ViewModel {
readonly pageViews: number;
readonly uniqueVisitors: number;
readonly averageSessionDuration: number;
readonly bounceRate: number;
private readonly data: AnalyticsMetricsViewData;
constructor(viewData: AnalyticsMetricsViewData) {
constructor(data: AnalyticsMetricsViewData) {
super();
this.pageViews = viewData.pageViews;
this.uniqueVisitors = viewData.uniqueVisitors;
this.averageSessionDuration = viewData.averageSessionDuration;
this.bounceRate = viewData.bounceRate;
this.data = data;
}
get pageViews(): number { return this.data.pageViews; }
get uniqueVisitors(): number { return this.data.uniqueVisitors; }
get averageSessionDuration(): number { return this.data.averageSessionDuration; }
get bounceRate(): number { return this.data.bounceRate; }
/** UI-specific: Formatted page views */
get formattedPageViews(): string {
return NumberDisplay.format(this.pageViews);

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