diff --git a/.eslintrc.json b/.eslintrc.json index 2d199c5cc..1cf33cdac 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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" } }, { diff --git a/apps/website/.eslintrc.json b/apps/website/.eslintrc.json index c5e3abacd..fc855228d 100644 --- a/apps/website/.eslintrc.json +++ b/apps/website/.eslintrc.json @@ -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" } }, { diff --git a/apps/website/client-wrapper/TeamLeaderboardPageWrapper.tsx b/apps/website/client-wrapper/TeamLeaderboardPageWrapper.tsx index f93961b29..7d419e178 100644 --- a/apps/website/client-wrapper/TeamLeaderboardPageWrapper.tsx +++ b/apps/website/client-wrapper/TeamLeaderboardPageWrapper.tsx @@ -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) { +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('all'); const [sortBy, setSortBy] = useState('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 { const matchesSearch = team.name.toLowerCase().includes(searchQuery.toLowerCase()); const matchesLevel = filterLevel === 'all' || team.performanceLevel === filterLevel; @@ -54,7 +57,7 @@ export function TeamLeaderboardPageWrapper({ viewData }: ClientWrapperProps { + 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) { diff --git a/apps/website/eslint-rules/view-model-taxonomy.js b/apps/website/eslint-rules/view-model-taxonomy.js new file mode 100644 index 000000000..8425b42b8 --- /dev/null +++ b/apps/website/eslint-rules/view-model-taxonomy.js @@ -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', + }); + } + }, + }; + }, +}; diff --git a/apps/website/lib/contracts/view-data/ViewData.ts b/apps/website/lib/contracts/view-data/ViewData.ts index 1c7a513ca..094208e7d 100644 --- a/apps/website/lib/contracts/view-data/ViewData.ts +++ b/apps/website/lib/contracts/view-data/ViewData.ts @@ -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; diff --git a/apps/website/lib/contracts/view-models/ViewModel.ts b/apps/website/lib/contracts/view-models/ViewModel.ts index 7df76f362..6a349aa8b 100644 --- a/apps/website/lib/contracts/view-models/ViewModel.ts +++ b/apps/website/lib/contracts/view-models/ViewModel.ts @@ -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 { diff --git a/apps/website/lib/display-objects/MembershipFeeTypeDisplay.ts b/apps/website/lib/display-objects/MembershipFeeTypeDisplay.ts new file mode 100644 index 000000000..4d1f9e01e --- /dev/null +++ b/apps/website/lib/display-objects/MembershipFeeTypeDisplay.ts @@ -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; + } + } +} diff --git a/apps/website/lib/display-objects/PayerTypeDisplay.ts b/apps/website/lib/display-objects/PayerTypeDisplay.ts new file mode 100644 index 000000000..bc6168b99 --- /dev/null +++ b/apps/website/lib/display-objects/PayerTypeDisplay.ts @@ -0,0 +1,5 @@ +export class PayerTypeDisplay { + static format(type: string): string { + return type.charAt(0).toUpperCase() + type.slice(1); + } +} diff --git a/apps/website/lib/display-objects/PaymentTypeDisplay.ts b/apps/website/lib/display-objects/PaymentTypeDisplay.ts new file mode 100644 index 000000000..b0db97cec --- /dev/null +++ b/apps/website/lib/display-objects/PaymentTypeDisplay.ts @@ -0,0 +1,5 @@ +export class PaymentTypeDisplay { + static format(type: string): string { + return type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()); + } +} diff --git a/apps/website/lib/display-objects/PrizeTypeDisplay.ts b/apps/website/lib/display-objects/PrizeTypeDisplay.ts new file mode 100644 index 000000000..1b0229dc8 --- /dev/null +++ b/apps/website/lib/display-objects/PrizeTypeDisplay.ts @@ -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; + } + } +} diff --git a/apps/website/lib/display-objects/TransactionTypeDisplay.ts b/apps/website/lib/display-objects/TransactionTypeDisplay.ts new file mode 100644 index 000000000..3a4474059 --- /dev/null +++ b/apps/website/lib/display-objects/TransactionTypeDisplay.ts @@ -0,0 +1,5 @@ +export class TransactionTypeDisplay { + static format(type: string): string { + return type.charAt(0).toUpperCase() + type.slice(1); + } +} diff --git a/apps/website/lib/view-data/BillingViewData.ts b/apps/website/lib/view-data/BillingViewData.ts index 0eec76d6f..9194c35c7 100644 --- a/apps/website/lib/view-data/BillingViewData.ts +++ b/apps/website/lib/view-data/BillingViewData.ts @@ -1,47 +1,52 @@ -import { ViewData } from "../contracts/view-data/ViewData"; +import { ViewData } from '../contracts/view-data/ViewData'; +export interface PaymentMethodViewData extends ViewData { + id: string; + type: 'card' | 'bank' | 'sepa'; + last4: string; + brand?: string; + isDefault: boolean; + expiryMonth?: number; + expiryYear?: number; + bankName?: string; + displayLabel: string; + expiryDisplay: string | null; +} + +export interface InvoiceViewData extends ViewData { + id: string; + invoiceNumber: string; + date: string; + dueDate: string; + amount: number; + vatAmount: number; + totalAmount: number; + status: 'paid' | 'pending' | 'overdue' | 'failed'; + description: string; + sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform'; + pdfUrl: string; + formattedTotalAmount: string; + formattedVatAmount: string; + formattedDate: string; + isOverdue: boolean; +} + +export interface BillingStatsViewData extends ViewData { + totalSpent: number; + pendingAmount: number; + nextPaymentDate: string; + nextPaymentAmount: number; + activeSponsorships: number; + averageMonthlySpend: number; + formattedTotalSpent: string; + formattedPendingAmount: string; + formattedNextPaymentAmount: string; + formattedAverageMonthlySpend: string; + formattedNextPaymentDate: string; +} export interface BillingViewData extends ViewData { - paymentMethods: Array<{ - id: string; - type: 'card' | 'bank' | 'sepa'; - last4: string; - brand?: string; - isDefault: boolean; - expiryMonth?: number; - expiryYear?: number; - bankName?: string; - displayLabel: string; - expiryDisplay: string | null; - }>; - invoices: Array<{ - id: string; - invoiceNumber: string; - date: string; - dueDate: string; - amount: number; - vatAmount: number; - totalAmount: number; - status: 'paid' | 'pending' | 'overdue' | 'failed'; - description: string; - sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform'; - pdfUrl: string; - formattedTotalAmount: string; - formattedVatAmount: string; - formattedDate: string; - isOverdue: boolean; - }>; - stats: { - totalSpent: number; - pendingAmount: number; - nextPaymentDate: string; - nextPaymentAmount: number; - activeSponsorships: number; - averageMonthlySpend: number; - formattedTotalSpent: string; - formattedPendingAmount: string; - formattedNextPaymentAmount: string; - formattedAverageMonthlySpend: string; - formattedNextPaymentDate: string; - }; + paymentMethods: PaymentMethodViewData[]; + invoices: InvoiceViewData[]; + stats: BillingStatsViewData; } diff --git a/apps/website/lib/view-data/CompleteOnboardingViewData.ts b/apps/website/lib/view-data/CompleteOnboardingViewData.ts new file mode 100644 index 000000000..7b59ae7da --- /dev/null +++ b/apps/website/lib/view-data/CompleteOnboardingViewData.ts @@ -0,0 +1,5 @@ +export interface CompleteOnboardingViewData { + success: boolean; + driverId?: string; + errorMessage?: string; +} diff --git a/apps/website/lib/view-data/DeleteMediaViewData.ts b/apps/website/lib/view-data/DeleteMediaViewData.ts new file mode 100644 index 000000000..9d315e5e2 --- /dev/null +++ b/apps/website/lib/view-data/DeleteMediaViewData.ts @@ -0,0 +1,4 @@ +export interface DeleteMediaViewData { + success: boolean; + error?: string; +} diff --git a/apps/website/lib/view-data/DriverSummaryData.ts b/apps/website/lib/view-data/DriverSummaryData.ts new file mode 100644 index 000000000..c68be0f83 --- /dev/null +++ b/apps/website/lib/view-data/DriverSummaryData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/DriverViewData.ts b/apps/website/lib/view-data/DriverViewData.ts new file mode 100644 index 000000000..e9409f523 --- /dev/null +++ b/apps/website/lib/view-data/DriverViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/LeaderboardDriverItem.ts b/apps/website/lib/view-data/LeaderboardDriverItem.ts index 70180bb9b..e89a9239a 100644 --- a/apps/website/lib/view-data/LeaderboardDriverItem.ts +++ b/apps/website/lib/view-data/LeaderboardDriverItem.ts @@ -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; diff --git a/apps/website/lib/view-data/LeaderboardTeamItem.ts b/apps/website/lib/view-data/LeaderboardTeamItem.ts index 4a3d95afb..3d38e48f4 100644 --- a/apps/website/lib/view-data/LeaderboardTeamItem.ts +++ b/apps/website/lib/view-data/LeaderboardTeamItem.ts @@ -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; diff --git a/apps/website/lib/view-data/LeaderboardsViewData.ts b/apps/website/lib/view-data/LeaderboardsViewData.ts index 314f005e0..d6fd91a01 100644 --- a/apps/website/lib/view-data/LeaderboardsViewData.ts +++ b/apps/website/lib/view-data/LeaderboardsViewData.ts @@ -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[]; -} \ No newline at end of file +} diff --git a/apps/website/lib/view-data/LeagueAdminRosterJoinRequestViewData.ts b/apps/website/lib/view-data/LeagueAdminRosterJoinRequestViewData.ts new file mode 100644 index 000000000..0dd7027f1 --- /dev/null +++ b/apps/website/lib/view-data/LeagueAdminRosterJoinRequestViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/LeagueAdminRosterMemberViewData.ts b/apps/website/lib/view-data/LeagueAdminRosterMemberViewData.ts new file mode 100644 index 000000000..bb5c78153 --- /dev/null +++ b/apps/website/lib/view-data/LeagueAdminRosterMemberViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/LeagueAdminScheduleViewData.ts b/apps/website/lib/view-data/LeagueAdminScheduleViewData.ts index 63c5e3455..b49b70447 100644 --- a/apps/website/lib/view-data/LeagueAdminScheduleViewData.ts +++ b/apps/website/lib/view-data/LeagueAdminScheduleViewData.ts @@ -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 -} \ No newline at end of file +/** + * ViewData for LeagueAdminSchedule + * This is the JSON-serializable input for the Template. + */ +export interface LeagueAdminScheduleViewData { + seasonId: string; + published: boolean; + races: any[]; +} diff --git a/apps/website/lib/view-data/LeagueAdminViewData.ts b/apps/website/lib/view-data/LeagueAdminViewData.ts new file mode 100644 index 000000000..5c7aa41f6 --- /dev/null +++ b/apps/website/lib/view-data/LeagueAdminViewData.ts @@ -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[]; +} diff --git a/apps/website/lib/view-data/LeagueCardViewData.ts b/apps/website/lib/view-data/LeagueCardViewData.ts new file mode 100644 index 000000000..37988f6af --- /dev/null +++ b/apps/website/lib/view-data/LeagueCardViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/LeagueDetailPageViewData.ts b/apps/website/lib/view-data/LeagueDetailPageViewData.ts new file mode 100644 index 000000000..8a44f63b4 --- /dev/null +++ b/apps/website/lib/view-data/LeagueDetailPageViewData.ts @@ -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[]; +} diff --git a/apps/website/lib/view-data/LeagueDetailViewData.ts b/apps/website/lib/view-data/LeagueDetailViewData.ts index 342997540..16027b37a 100644 --- a/apps/website/lib/view-data/LeagueDetailViewData.ts +++ b/apps/website/lib/view-data/LeagueDetailViewData.ts @@ -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 })[]; } diff --git a/apps/website/lib/view-data/LeagueJoinRequestViewData.ts b/apps/website/lib/view-data/LeagueJoinRequestViewData.ts new file mode 100644 index 000000000..b5cf06d24 --- /dev/null +++ b/apps/website/lib/view-data/LeagueJoinRequestViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/LeagueMemberViewData.ts b/apps/website/lib/view-data/LeagueMemberViewData.ts new file mode 100644 index 000000000..1e92eed9c --- /dev/null +++ b/apps/website/lib/view-data/LeagueMemberViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/LeagueMembershipsViewData.ts b/apps/website/lib/view-data/LeagueMembershipsViewData.ts new file mode 100644 index 000000000..ebf64f975 --- /dev/null +++ b/apps/website/lib/view-data/LeagueMembershipsViewData.ts @@ -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[]; +} diff --git a/apps/website/lib/view-data/LeaguePageDetailViewData.ts b/apps/website/lib/view-data/LeaguePageDetailViewData.ts new file mode 100644 index 000000000..50f0cad91 --- /dev/null +++ b/apps/website/lib/view-data/LeaguePageDetailViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/LeagueScheduleViewData.ts b/apps/website/lib/view-data/LeagueScheduleViewData.ts index e16ba9bd5..2e1ad5073 100644 --- a/apps/website/lib/view-data/LeagueScheduleViewData.ts +++ b/apps/website/lib/view-data/LeagueScheduleViewData.ts @@ -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 { + races: any[]; } - - -export interface LeagueScheduleViewData extends ViewData { - leagueId: string; - races: ScheduleRaceData[]; - seasons: Array<{ - seasonId: string; - name: string; - status: string; - }>; -} \ No newline at end of file diff --git a/apps/website/lib/view-data/LeagueScoringChampionshipViewData.ts b/apps/website/lib/view-data/LeagueScoringChampionshipViewData.ts new file mode 100644 index 000000000..82ec7ffcd --- /dev/null +++ b/apps/website/lib/view-data/LeagueScoringChampionshipViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/LeagueScoringConfigViewData.ts b/apps/website/lib/view-data/LeagueScoringConfigViewData.ts new file mode 100644 index 000000000..e599dc9cc --- /dev/null +++ b/apps/website/lib/view-data/LeagueScoringConfigViewData.ts @@ -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[]; +} diff --git a/apps/website/lib/view-data/LeagueScoringPresetViewData.ts b/apps/website/lib/view-data/LeagueScoringPresetViewData.ts new file mode 100644 index 000000000..58929ed90 --- /dev/null +++ b/apps/website/lib/view-data/LeagueScoringPresetViewData.ts @@ -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; + }; +} diff --git a/apps/website/lib/view-data/LeagueScoringPresetsViewData.ts b/apps/website/lib/view-data/LeagueScoringPresetsViewData.ts new file mode 100644 index 000000000..ff5904ef7 --- /dev/null +++ b/apps/website/lib/view-data/LeagueScoringPresetsViewData.ts @@ -0,0 +1,7 @@ +/** + * ViewData for league scoring presets + */ +export interface LeagueScoringPresetsViewData { + presets: any[]; + totalCount?: number; +} diff --git a/apps/website/lib/view-data/LeagueScoringSectionViewData.ts b/apps/website/lib/view-data/LeagueScoringSectionViewData.ts new file mode 100644 index 000000000..9cc952187 --- /dev/null +++ b/apps/website/lib/view-data/LeagueScoringSectionViewData.ts @@ -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; + }; +} diff --git a/apps/website/lib/view-data/LeagueSettingsViewData.ts b/apps/website/lib/view-data/LeagueSettingsViewData.ts index ad4674cf2..c8e40a2ff 100644 --- a/apps/website/lib/view-data/LeagueSettingsViewData.ts +++ b/apps/website/lib/view-data/LeagueSettingsViewData.ts @@ -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; - }; -} \ No newline at end of file + config: LeagueConfigFormModel; + presets: any[]; + owner: any | null; + members: any[]; +} diff --git a/apps/website/lib/view-data/LeagueStandingsViewData.ts b/apps/website/lib/view-data/LeagueStandingsViewData.ts index 5b8e664e7..36922d661 100644 --- a/apps/website/lib/view-data/LeagueStandingsViewData.ts +++ b/apps/website/lib/view-data/LeagueStandingsViewData.ts @@ -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; +/** + * ViewData for LeagueStandings + * This is the JSON-serializable input for the Template. + */ +export interface LeagueStandingsViewData { + standings: StandingEntryViewData[]; + drivers: any[]; + memberships: any[]; } - -export interface LeagueMembershipData { - driverId: string; - leagueId: string; - role: 'owner' | 'admin' | 'steward' | 'member'; - joinedAt: string; - status: 'active' | 'pending' | 'banned'; -} \ No newline at end of file diff --git a/apps/website/lib/view-data/LeagueStatsViewData.ts b/apps/website/lib/view-data/LeagueStatsViewData.ts new file mode 100644 index 000000000..465497321 --- /dev/null +++ b/apps/website/lib/view-data/LeagueStatsViewData.ts @@ -0,0 +1,6 @@ +/** + * ViewData for LeagueStats + */ +export interface LeagueStatsViewData { + totalLeagues: number; +} diff --git a/apps/website/lib/view-data/LeagueSummaryViewData.ts b/apps/website/lib/view-data/LeagueSummaryViewData.ts new file mode 100644 index 000000000..d458817c0 --- /dev/null +++ b/apps/website/lib/view-data/LeagueSummaryViewData.ts @@ -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; + }; +} diff --git a/apps/website/lib/view-data/LeagueWalletViewData.ts b/apps/website/lib/view-data/LeagueWalletViewData.ts index 819465ef8..f72ae2a90 100644 --- a/apps/website/lib/view-data/LeagueWalletViewData.ts +++ b/apps/website/lib/view-data/LeagueWalletViewData.ts @@ -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; -} \ No newline at end of file +/** + * 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; +} diff --git a/apps/website/lib/view-data/MediaViewData.ts b/apps/website/lib/view-data/MediaViewData.ts index e7b990e77..bdf8ea161 100644 --- a/apps/website/lib/view-data/MediaViewData.ts +++ b/apps/website/lib/view-data/MediaViewData.ts @@ -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; diff --git a/apps/website/lib/view-data/MembershipFeeViewData.ts b/apps/website/lib/view-data/MembershipFeeViewData.ts new file mode 100644 index 000000000..01576bcb0 --- /dev/null +++ b/apps/website/lib/view-data/MembershipFeeViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/NotificationSettingsViewData.ts b/apps/website/lib/view-data/NotificationSettingsViewData.ts new file mode 100644 index 000000000..177e230e4 --- /dev/null +++ b/apps/website/lib/view-data/NotificationSettingsViewData.ts @@ -0,0 +1,11 @@ +/** + * ViewData for NotificationSettings + */ +export interface NotificationSettingsViewData { + emailNewSponsorships: boolean; + emailWeeklyReport: boolean; + emailRaceAlerts: boolean; + emailPaymentAlerts: boolean; + emailNewOpportunities: boolean; + emailContractExpiry: boolean; +} diff --git a/apps/website/lib/view-data/PaymentViewData.ts b/apps/website/lib/view-data/PaymentViewData.ts new file mode 100644 index 000000000..0c6eacf39 --- /dev/null +++ b/apps/website/lib/view-data/PaymentViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/PrivacySettingsViewData.ts b/apps/website/lib/view-data/PrivacySettingsViewData.ts new file mode 100644 index 000000000..aaff6ed74 --- /dev/null +++ b/apps/website/lib/view-data/PrivacySettingsViewData.ts @@ -0,0 +1,9 @@ +/** + * ViewData for PrivacySettings + */ +export interface PrivacySettingsViewData { + publicProfile: boolean; + showStats: boolean; + showActiveSponsorships: boolean; + allowDirectContact: boolean; +} diff --git a/apps/website/lib/view-data/PrizeViewData.ts b/apps/website/lib/view-data/PrizeViewData.ts new file mode 100644 index 000000000..844bbda86 --- /dev/null +++ b/apps/website/lib/view-data/PrizeViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/ProfileOverviewViewData.ts b/apps/website/lib/view-data/ProfileOverviewViewData.ts new file mode 100644 index 000000000..2ca91b118 --- /dev/null +++ b/apps/website/lib/view-data/ProfileOverviewViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/ProtestDriverViewData.ts b/apps/website/lib/view-data/ProtestDriverViewData.ts new file mode 100644 index 000000000..09b04788d --- /dev/null +++ b/apps/website/lib/view-data/ProtestDriverViewData.ts @@ -0,0 +1,4 @@ +export interface ProtestDriverViewData { + id: string; + name: string; +} diff --git a/apps/website/lib/view-data/ProtestViewData.ts b/apps/website/lib/view-data/ProtestViewData.ts new file mode 100644 index 000000000..7073a02cf --- /dev/null +++ b/apps/website/lib/view-data/ProtestViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/RaceDetailEntryViewData.ts b/apps/website/lib/view-data/RaceDetailEntryViewData.ts new file mode 100644 index 000000000..bc0744fe0 --- /dev/null +++ b/apps/website/lib/view-data/RaceDetailEntryViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/RaceDetailUserResultViewData.ts b/apps/website/lib/view-data/RaceDetailUserResultViewData.ts new file mode 100644 index 000000000..8656587e2 --- /dev/null +++ b/apps/website/lib/view-data/RaceDetailUserResultViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/RaceDetailsViewData.ts b/apps/website/lib/view-data/RaceDetailsViewData.ts new file mode 100644 index 000000000..898943d21 --- /dev/null +++ b/apps/website/lib/view-data/RaceDetailsViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/RaceListItemViewData.ts b/apps/website/lib/view-data/RaceListItemViewData.ts new file mode 100644 index 000000000..553b4b8b5 --- /dev/null +++ b/apps/website/lib/view-data/RaceListItemViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/RaceResultViewData.ts b/apps/website/lib/view-data/RaceResultViewData.ts new file mode 100644 index 000000000..1696174f7 --- /dev/null +++ b/apps/website/lib/view-data/RaceResultViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/RaceResultsDetailViewData.ts b/apps/website/lib/view-data/RaceResultsDetailViewData.ts new file mode 100644 index 000000000..668b745a5 --- /dev/null +++ b/apps/website/lib/view-data/RaceResultsDetailViewData.ts @@ -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; + fastestLapTime: number; + penalties: { driverId: string; type: string; value?: number }[]; + currentDriverId: string; +} diff --git a/apps/website/lib/view-data/RaceStatsViewData.ts b/apps/website/lib/view-data/RaceStatsViewData.ts new file mode 100644 index 000000000..76c4d902f --- /dev/null +++ b/apps/website/lib/view-data/RaceStatsViewData.ts @@ -0,0 +1,7 @@ +/** + * ViewData for RaceStats + * This is the JSON-serializable input for the Template. + */ +export interface RaceStatsViewData { + totalRaces: number; +} diff --git a/apps/website/lib/view-data/RaceStewardingViewData.ts b/apps/website/lib/view-data/RaceStewardingViewData.ts index 8d025f68a..9add7df69 100644 --- a/apps/website/lib/view-data/RaceStewardingViewData.ts +++ b/apps/website/lib/view-data/RaceStewardingViewData.ts @@ -1,55 +1,38 @@ /** - * 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 { - id: string; - protestingDriverId: string; - accusedDriverId: string; - incident: { - lap: number; - description: string; - }; - filedAt: string; - status: string; - proofVideoUrl?: string; - decisionNotes?: string; -} - -export interface Penalty { - 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?: { +export interface RaceStewardingViewData { + race: { id: string; track: string; scheduledAt: string; + status: string; } | null; - league?: { + league: { id: string; + name: string; } | null; - pendingProtests: Protest[]; - resolvedProtests: Protest[]; - penalties: Penalty[]; - driverMap: Record; - pendingCount: number; - resolvedCount: number; - penaltiesCount: number; -} \ No newline at end of file + protests: Array<{ + id: string; + protestingDriverId: string; + accusedDriverId: string; + incident: { + lap: number; + description: string; + }; + filedAt: string; + status: string; + decisionNotes?: string; + proofVideoUrl?: string; + }>; + penalties: Array<{ + id: string; + driverId: string; + type: string; + value: number; + reason: string; + notes?: string; + }>; + driverMap: Record; +} diff --git a/apps/website/lib/view-data/RaceViewData.ts b/apps/website/lib/view-data/RaceViewData.ts new file mode 100644 index 000000000..2658d1ab3 --- /dev/null +++ b/apps/website/lib/view-data/RaceViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/RaceWithSOFViewData.ts b/apps/website/lib/view-data/RaceWithSOFViewData.ts new file mode 100644 index 000000000..70b1c289a --- /dev/null +++ b/apps/website/lib/view-data/RaceWithSOFViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/RacesPageViewData.ts b/apps/website/lib/view-data/RacesPageViewData.ts new file mode 100644 index 000000000..77bdcb872 --- /dev/null +++ b/apps/website/lib/view-data/RacesPageViewData.ts @@ -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[]; +} diff --git a/apps/website/lib/view-data/RecordEngagementInputViewData.ts b/apps/website/lib/view-data/RecordEngagementInputViewData.ts new file mode 100644 index 000000000..9738d2ff3 --- /dev/null +++ b/apps/website/lib/view-data/RecordEngagementInputViewData.ts @@ -0,0 +1,8 @@ +/** + * Record engagement input view data + */ +export interface RecordEngagementInputViewData { + eventType: string; + userId?: string; + metadata?: Record; +} diff --git a/apps/website/lib/view-data/RecordPageViewInputViewData.ts b/apps/website/lib/view-data/RecordPageViewInputViewData.ts new file mode 100644 index 000000000..f523a6334 --- /dev/null +++ b/apps/website/lib/view-data/RecordPageViewInputViewData.ts @@ -0,0 +1,7 @@ +/** + * Record page view input view data + */ +export interface RecordPageViewInputViewData { + path: string; + userId?: string; +} diff --git a/apps/website/lib/view-data/RecordPageViewOutputViewData.ts b/apps/website/lib/view-data/RecordPageViewOutputViewData.ts new file mode 100644 index 000000000..f7a2cd07d --- /dev/null +++ b/apps/website/lib/view-data/RecordPageViewOutputViewData.ts @@ -0,0 +1,6 @@ +/** + * Record page view output view data + */ +export interface RecordPageViewOutputViewData { + pageViewId: string; +} diff --git a/apps/website/lib/view-data/RemoveMemberViewData.ts b/apps/website/lib/view-data/RemoveMemberViewData.ts new file mode 100644 index 000000000..7af6811be --- /dev/null +++ b/apps/website/lib/view-data/RemoveMemberViewData.ts @@ -0,0 +1,6 @@ +/** + * ViewData for RemoveMember + */ +export interface RemoveMemberViewData { + success: boolean; +} diff --git a/apps/website/lib/view-data/RenewalAlertViewData.ts b/apps/website/lib/view-data/RenewalAlertViewData.ts new file mode 100644 index 000000000..aaff0e145 --- /dev/null +++ b/apps/website/lib/view-data/RenewalAlertViewData.ts @@ -0,0 +1,10 @@ +/** + * ViewData for RenewalAlert + */ +export interface RenewalAlertViewData { + id: string; + name: string; + type: 'league' | 'team' | 'driver' | 'race' | 'platform'; + renewDate: string; + price: number; +} diff --git a/apps/website/lib/view-data/ScoringConfigurationViewData.ts b/apps/website/lib/view-data/ScoringConfigurationViewData.ts new file mode 100644 index 000000000..3934c9e8e --- /dev/null +++ b/apps/website/lib/view-data/ScoringConfigurationViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/SponsorDashboardViewData.ts b/apps/website/lib/view-data/SponsorDashboardViewData.ts index 51404ab48..8e8f451de 100644 --- a/apps/website/lib/view-data/SponsorDashboardViewData.ts +++ b/apps/website/lib/view-data/SponsorDashboardViewData.ts @@ -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; // 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; - }>; } diff --git a/apps/website/lib/view-data/SponsorProfileViewData.ts b/apps/website/lib/view-data/SponsorProfileViewData.ts new file mode 100644 index 000000000..fb0561ddf --- /dev/null +++ b/apps/website/lib/view-data/SponsorProfileViewData.ts @@ -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; + }; +} diff --git a/apps/website/lib/view-data/SponsorSettingsViewData.ts b/apps/website/lib/view-data/SponsorSettingsViewData.ts new file mode 100644 index 000000000..e743203d0 --- /dev/null +++ b/apps/website/lib/view-data/SponsorSettingsViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/SponsorSponsorshipsViewData.ts b/apps/website/lib/view-data/SponsorSponsorshipsViewData.ts new file mode 100644 index 000000000..44a382161 --- /dev/null +++ b/apps/website/lib/view-data/SponsorSponsorshipsViewData.ts @@ -0,0 +1,10 @@ +import type { SponsorshipDetailViewData } from './SponsorshipDetailViewData'; + +/** + * ViewData for SponsorSponsorships + */ +export interface SponsorSponsorshipsViewData { + sponsorId: string; + sponsorName: string; + sponsorships: SponsorshipDetailViewData[]; +} diff --git a/apps/website/lib/view-data/SponsorViewData.ts b/apps/website/lib/view-data/SponsorViewData.ts new file mode 100644 index 000000000..e85cc2ca6 --- /dev/null +++ b/apps/website/lib/view-data/SponsorViewData.ts @@ -0,0 +1,9 @@ +/** + * ViewData for Sponsor + */ +export interface SponsorViewData { + id: string; + name: string; + logoUrl?: string; + websiteUrl?: string; +} diff --git a/apps/website/lib/view-data/SponsorshipDetailViewData.ts b/apps/website/lib/view-data/SponsorshipDetailViewData.ts new file mode 100644 index 000000000..e18d2d5ed --- /dev/null +++ b/apps/website/lib/view-data/SponsorshipDetailViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/SponsorshipPricingViewData.ts b/apps/website/lib/view-data/SponsorshipPricingViewData.ts new file mode 100644 index 000000000..62467192c --- /dev/null +++ b/apps/website/lib/view-data/SponsorshipPricingViewData.ts @@ -0,0 +1,8 @@ +/** + * ViewData for SponsorshipPricing + */ +export interface SponsorshipPricingViewData { + mainSlotPrice: number; + secondarySlotPrice: number; + currency: string; +} diff --git a/apps/website/lib/view-data/SponsorshipRequestViewData.ts b/apps/website/lib/view-data/SponsorshipRequestViewData.ts new file mode 100644 index 000000000..db58af76b --- /dev/null +++ b/apps/website/lib/view-data/SponsorshipRequestViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/SponsorshipViewData.ts b/apps/website/lib/view-data/SponsorshipViewData.ts new file mode 100644 index 000000000..8cf0688da --- /dev/null +++ b/apps/website/lib/view-data/SponsorshipViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/StandingEntryViewData.ts b/apps/website/lib/view-data/StandingEntryViewData.ts new file mode 100644 index 000000000..6bb18bb16 --- /dev/null +++ b/apps/website/lib/view-data/StandingEntryViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/TeamCardViewData.ts b/apps/website/lib/view-data/TeamCardViewData.ts new file mode 100644 index 000000000..db427cb79 --- /dev/null +++ b/apps/website/lib/view-data/TeamCardViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/TeamDetailsViewData.ts b/apps/website/lib/view-data/TeamDetailsViewData.ts new file mode 100644 index 000000000..0ccc5cdcb --- /dev/null +++ b/apps/website/lib/view-data/TeamDetailsViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/TeamJoinRequestViewData.ts b/apps/website/lib/view-data/TeamJoinRequestViewData.ts new file mode 100644 index 000000000..e03f8c8ac --- /dev/null +++ b/apps/website/lib/view-data/TeamJoinRequestViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/TeamLeaderboardViewData.ts b/apps/website/lib/view-data/TeamLeaderboardViewData.ts index 9aafcf987..46e2d3ec3 100644 --- a/apps/website/lib/view-data/TeamLeaderboardViewData.ts +++ b/apps/website/lib/view-data/TeamLeaderboardViewData.ts @@ -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[]; } diff --git a/apps/website/lib/view-data/TeamMemberViewData.ts b/apps/website/lib/view-data/TeamMemberViewData.ts new file mode 100644 index 000000000..a12d80773 --- /dev/null +++ b/apps/website/lib/view-data/TeamMemberViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/TeamSummaryViewData.ts b/apps/website/lib/view-data/TeamSummaryViewData.ts new file mode 100644 index 000000000..ad2837272 --- /dev/null +++ b/apps/website/lib/view-data/TeamSummaryViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/UpcomingRaceCardViewData.ts b/apps/website/lib/view-data/UpcomingRaceCardViewData.ts new file mode 100644 index 000000000..988ed0ef0 --- /dev/null +++ b/apps/website/lib/view-data/UpcomingRaceCardViewData.ts @@ -0,0 +1,6 @@ +export interface UpcomingRaceCardViewData { + id: string; + track: string; + car: string; + scheduledAt: string; +} diff --git a/apps/website/lib/view-data/UpdateAvatarViewData.ts b/apps/website/lib/view-data/UpdateAvatarViewData.ts new file mode 100644 index 000000000..0c3debfa3 --- /dev/null +++ b/apps/website/lib/view-data/UpdateAvatarViewData.ts @@ -0,0 +1,7 @@ +/** + * ViewData for UpdateAvatar + */ +export interface UpdateAvatarViewData { + success: boolean; + error?: string; +} diff --git a/apps/website/lib/view-data/UpdateTeamViewData.ts b/apps/website/lib/view-data/UpdateTeamViewData.ts new file mode 100644 index 000000000..72dec35d2 --- /dev/null +++ b/apps/website/lib/view-data/UpdateTeamViewData.ts @@ -0,0 +1,6 @@ +/** + * ViewData for UpdateTeam + */ +export interface UpdateTeamViewData { + success: boolean; +} diff --git a/apps/website/lib/view-data/UploadMediaViewData.ts b/apps/website/lib/view-data/UploadMediaViewData.ts new file mode 100644 index 000000000..5dcb25574 --- /dev/null +++ b/apps/website/lib/view-data/UploadMediaViewData.ts @@ -0,0 +1,9 @@ +/** + * ViewData for UploadMedia + */ +export interface UploadMediaViewData { + success: boolean; + mediaId?: string; + url?: string; + error?: string; +} diff --git a/apps/website/lib/view-data/UserProfileViewData.ts b/apps/website/lib/view-data/UserProfileViewData.ts new file mode 100644 index 000000000..0fdd04e2f --- /dev/null +++ b/apps/website/lib/view-data/UserProfileViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/WalletTransactionViewData.ts b/apps/website/lib/view-data/WalletTransactionViewData.ts new file mode 100644 index 000000000..66af01afb --- /dev/null +++ b/apps/website/lib/view-data/WalletTransactionViewData.ts @@ -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; +} diff --git a/apps/website/lib/view-data/WalletViewData.ts b/apps/website/lib/view-data/WalletViewData.ts new file mode 100644 index 000000000..c424b5b3d --- /dev/null +++ b/apps/website/lib/view-data/WalletViewData.ts @@ -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[]; +} diff --git a/apps/website/lib/view-models/ActivityItemViewModel.ts b/apps/website/lib/view-models/ActivityItemViewModel.ts index 52678f4f0..15228f0f2 100644 --- a/apps/website/lib/view-models/ActivityItemViewModel.ts +++ b/apps/website/lib/view-models/ActivityItemViewModel.ts @@ -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 = { 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; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/AdminUserViewModel.ts b/apps/website/lib/view-models/AdminUserViewModel.ts index a0c9a8b3a..97ee42845 100644 --- a/apps/website/lib/view-models/AdminUserViewModel.ts +++ b/apps/website/lib/view-models/AdminUserViewModel.ts @@ -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); + } + + /** UI-specific: Formatted creation date */ + get createdAtFormatted(): string { + return 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; - } -} \ No newline at end of file diff --git a/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts b/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts index c69a896a8..f752b10e0 100644 --- a/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts +++ b/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts @@ -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; diff --git a/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts b/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts index 18de3fb78..8612b807b 100644 --- a/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts +++ b/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts @@ -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); diff --git a/apps/website/lib/view-models/AvailableLeaguesViewModel.ts b/apps/website/lib/view-models/AvailableLeaguesViewModel.ts index 91ac0c900..47d9fb010 100644 --- a/apps/website/lib/view-models/AvailableLeaguesViewModel.ts +++ b/apps/website/lib/view-models/AvailableLeaguesViewModel.ts @@ -13,44 +13,37 @@ import { LeagueTierDisplay } from "../display-objects/LeagueTierDisplay"; import { SeasonStatusDisplay } from "../display-objects/SeasonStatusDisplay"; export class AvailableLeaguesViewModel extends ViewModel { + private readonly data: AvailableLeaguesViewData; readonly leagues: AvailableLeagueViewModel[]; - constructor(viewData: AvailableLeaguesViewData) { + constructor(data: AvailableLeaguesViewData) { super(); - this.leagues = viewData.leagues.map(league => new AvailableLeagueViewModel(league)); + this.data = data; + this.leagues = data.leagues.map(league => new AvailableLeagueViewModel(league)); } } export class AvailableLeagueViewModel extends ViewModel { - readonly id: string; - readonly name: string; - readonly game: string; - readonly drivers: number; - readonly avgViewsPerRace: number; - readonly mainSponsorSlot: { available: boolean; price: number }; - readonly secondarySlots: { available: number; total: number; price: number }; - readonly rating: number; - readonly tier: 'premium' | 'standard' | 'starter'; - readonly nextRace?: string; - readonly seasonStatus: 'active' | 'upcoming' | 'completed'; - readonly description: string; + private readonly data: AvailableLeagueViewData; - constructor(viewData: AvailableLeagueViewData) { + constructor(data: AvailableLeagueViewData) { super(); - this.id = viewData.id; - this.name = viewData.name; - this.game = viewData.game; - this.drivers = viewData.drivers; - this.avgViewsPerRace = viewData.avgViewsPerRace; - this.mainSponsorSlot = viewData.mainSponsorSlot; - this.secondarySlots = viewData.secondarySlots; - this.rating = viewData.rating; - this.tier = viewData.tier; - this.nextRace = viewData.nextRace; - this.seasonStatus = viewData.seasonStatus; - this.description = viewData.description; + this.data = data; } + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get game(): string { return this.data.game; } + get drivers(): number { return this.data.drivers; } + get avgViewsPerRace(): number { return this.data.avgViewsPerRace; } + get mainSponsorSlot() { return this.data.mainSponsorSlot; } + get secondarySlots() { return this.data.secondarySlots; } + get rating(): number { return this.data.rating; } + get tier(): 'premium' | 'standard' | 'starter' { return this.data.tier; } + get nextRace(): string | undefined { return this.data.nextRace; } + get seasonStatus(): 'active' | 'upcoming' | 'completed' { return this.data.seasonStatus; } + get description(): string { return this.data.description; } + /** UI-specific: Formatted average views */ get formattedAvgViews(): string { return NumberDisplay.formatCompact(this.avgViewsPerRace); diff --git a/apps/website/lib/view-models/AvatarGenerationViewModel.ts b/apps/website/lib/view-models/AvatarGenerationViewModel.ts index 5e9a6f5b6..5ba7daa1b 100644 --- a/apps/website/lib/view-models/AvatarGenerationViewModel.ts +++ b/apps/website/lib/view-models/AvatarGenerationViewModel.ts @@ -9,14 +9,14 @@ import { AvatarGenerationViewData } from "../view-data/AvatarGenerationViewData" * Accepts AvatarGenerationViewData as input and produces UI-ready data. */ export class AvatarGenerationViewModel extends ViewModel { - readonly success: boolean; - readonly avatarUrls: string[]; - readonly errorMessage?: string; + private readonly data: AvatarGenerationViewData; - constructor(viewData: AvatarGenerationViewData) { + constructor(data: AvatarGenerationViewData) { super(); - this.success = viewData.success; - this.avatarUrls = viewData.avatarUrls; - this.errorMessage = viewData.errorMessage; + this.data = data; } + + get success(): boolean { return this.data.success; } + get avatarUrls(): string[] { return this.data.avatarUrls; } + get errorMessage(): string | undefined { return this.data.errorMessage; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/AvatarViewModel.ts b/apps/website/lib/view-models/AvatarViewModel.ts index c01a797b7..7f9bca66b 100644 --- a/apps/website/lib/view-models/AvatarViewModel.ts +++ b/apps/website/lib/view-models/AvatarViewModel.ts @@ -9,21 +9,25 @@ import { AvatarViewData } from "@/lib/view-data/AvatarViewData"; * Transforms AvatarViewData into UI-ready state with formatting and derived fields. */ export class AvatarViewModel extends ViewModel { - // UI-specific derived fields (primitive outputs only) - readonly bufferBase64: string; - readonly contentTypeLabel: string; - readonly hasValidData: boolean; + private readonly data: AvatarViewData; - constructor(viewData: AvatarViewData) { + constructor(data: AvatarViewData) { super(); + this.data = data; + } - // Buffer is already base64 encoded in ViewData - this.bufferBase64 = viewData.buffer; + /** UI-specific: Buffer is already base64 encoded in ViewData */ + get bufferBase64(): string { + return this.data.buffer; + } - // Derive content type label using Display Object - this.contentTypeLabel = AvatarDisplay.formatContentType(viewData.contentType); + /** UI-specific: Derive content type label using Display Object */ + get contentTypeLabel(): string { + return AvatarDisplay.formatContentType(this.data.contentType); + } - // Derive validity check using Display Object - this.hasValidData = AvatarDisplay.hasValidData(viewData.buffer, viewData.contentType); + /** UI-specific: Derive validity check using Display Object */ + get hasValidData(): boolean { + return AvatarDisplay.hasValidData(this.data.buffer, this.data.contentType); } } \ No newline at end of file diff --git a/apps/website/lib/view-models/BillingStatsViewModel.ts b/apps/website/lib/view-models/BillingStatsViewModel.ts new file mode 100644 index 000000000..5ad7cbc17 --- /dev/null +++ b/apps/website/lib/view-models/BillingStatsViewModel.ts @@ -0,0 +1,23 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { BillingStatsViewData } from "@/lib/view-data/BillingViewData"; + +export class BillingStatsViewModel extends ViewModel { + private readonly data: BillingStatsViewData; + + constructor(data: BillingStatsViewData) { + super(); + this.data = data; + } + + get totalSpent(): number { return this.data.totalSpent; } + get pendingAmount(): number { return this.data.pendingAmount; } + get nextPaymentDate(): string { return this.data.nextPaymentDate; } + get nextPaymentAmount(): number { return this.data.nextPaymentAmount; } + get activeSponsorships(): number { return this.data.activeSponsorships; } + get averageMonthlySpend(): number { return this.data.averageMonthlySpend; } + get totalSpentDisplay(): string { return this.data.formattedTotalSpent; } + get pendingAmountDisplay(): string { return this.data.formattedPendingAmount; } + get nextPaymentAmountDisplay(): string { return this.data.formattedNextPaymentAmount; } + get averageMonthlySpendDisplay(): string { return this.data.formattedAverageMonthlySpend; } + get nextPaymentDateDisplay(): string { return this.data.formattedNextPaymentDate; } +} diff --git a/apps/website/lib/view-models/BillingViewModel.ts b/apps/website/lib/view-models/BillingViewModel.ts index 87367d568..6eff867cf 100644 --- a/apps/website/lib/view-models/BillingViewModel.ts +++ b/apps/website/lib/view-models/BillingViewModel.ts @@ -6,8 +6,9 @@ */ import type { BillingViewData } from '@/lib/view-data/BillingViewData'; import { ViewModel } from "../contracts/view-models/ViewModel"; -import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; -import { DateDisplay } from "../display-objects/DateDisplay"; +import { PaymentMethodViewModel } from "./PaymentMethodViewModel"; +import { InvoiceViewModel } from "./InvoiceViewModel"; +import { BillingStatsViewModel } from "./BillingStatsViewModel"; /** * BillingViewModel @@ -16,170 +17,16 @@ import { DateDisplay } from "../display-objects/DateDisplay"; * Transforms BillingViewData into UI-ready state with formatting and derived fields. */ export class BillingViewModel extends ViewModel { - paymentMethods: PaymentMethodViewModel[]; - invoices: InvoiceViewModel[]; - stats: BillingStatsViewModel; + private readonly data: BillingViewData; + readonly paymentMethods: PaymentMethodViewModel[]; + readonly invoices: InvoiceViewModel[]; + readonly stats: BillingStatsViewModel; - constructor(viewData: BillingViewData) { + constructor(data: BillingViewData) { super(); - this.paymentMethods = viewData.paymentMethods.map(pm => new PaymentMethodViewModel(pm)); - this.invoices = viewData.invoices.map(inv => new InvoiceViewModel(inv)); - this.stats = new BillingStatsViewModel(viewData.stats); + this.data = data; + this.paymentMethods = data.paymentMethods.map(pm => new PaymentMethodViewModel(pm)); + this.invoices = data.invoices.map(inv => new InvoiceViewModel(inv)); + this.stats = new BillingStatsViewModel(data.stats); } } - -/** - * PaymentMethodViewModel - * - * View Model for payment method data. - * Provides formatted display labels and expiry information. - */ -export class PaymentMethodViewModel extends ViewModel { - id: string; - type: 'card' | 'bank' | 'sepa'; - last4: string; - brand?: string; - isDefault: boolean; - expiryMonth?: number; - expiryYear?: number; - bankName?: string; - - // UI-specific derived fields (primitive outputs only) - readonly displayLabel: string; - readonly expiryDisplay: string | null; - - constructor(viewData: { - id: string; - type: 'card' | 'bank' | 'sepa'; - last4: string; - brand?: string; - isDefault: boolean; - expiryMonth?: number; - expiryYear?: number; - bankName?: string; - displayLabel: string; - expiryDisplay: string | null; - }) { - super(); - this.id = viewData.id; - this.type = viewData.type; - this.last4 = viewData.last4; - this.brand = viewData.brand; - this.isDefault = viewData.isDefault; - this.expiryMonth = viewData.expiryMonth; - this.expiryYear = viewData.expiryYear; - this.bankName = viewData.bankName; - this.displayLabel = viewData.displayLabel; - this.expiryDisplay = viewData.expiryDisplay; - } -} - -/** - * InvoiceViewModel - * - * View Model for invoice data. - * Provides formatted amounts, dates, and derived status flags. - */ -export class InvoiceViewModel extends ViewModel { - id: string; - invoiceNumber: string; - date: Date; - dueDate: Date; - amount: number; - vatAmount: number; - totalAmount: number; - status: 'paid' | 'pending' | 'overdue' | 'failed'; - description: string; - sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform'; - pdfUrl: string; - - // UI-specific derived fields (primitive outputs only) - readonly formattedTotalAmount: string; - readonly formattedVatAmount: string; - readonly formattedDate: string; - readonly isOverdue: boolean; - - constructor(viewData: { - id: string; - invoiceNumber: string; - date: string; - dueDate: string; - amount: number; - vatAmount: number; - totalAmount: number; - status: 'paid' | 'pending' | 'overdue' | 'failed'; - description: string; - sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform'; - pdfUrl: string; - formattedTotalAmount: string; - formattedVatAmount: string; - formattedDate: string; - isOverdue: boolean; - }) { - super(); - this.id = viewData.id; - this.invoiceNumber = viewData.invoiceNumber; - this.date = new Date(viewData.date); - this.dueDate = new Date(viewData.dueDate); - this.amount = viewData.amount; - this.vatAmount = viewData.vatAmount; - this.totalAmount = viewData.totalAmount; - this.status = viewData.status; - this.description = viewData.description; - this.sponsorshipType = viewData.sponsorshipType; - this.pdfUrl = viewData.pdfUrl; - this.formattedTotalAmount = viewData.formattedTotalAmount; - this.formattedVatAmount = viewData.formattedVatAmount; - this.formattedDate = viewData.formattedDate; - this.isOverdue = viewData.isOverdue; - } -} - -/** - * BillingStatsViewModel - * - * View Model for billing statistics. - * Provides formatted monetary fields and derived metrics. - */ -export class BillingStatsViewModel extends ViewModel { - totalSpent: number; - pendingAmount: number; - nextPaymentDate: Date; - nextPaymentAmount: number; - activeSponsorships: number; - averageMonthlySpend: number; - - // UI-specific derived fields (primitive outputs only) - readonly formattedTotalSpent: string; - readonly formattedPendingAmount: string; - readonly formattedNextPaymentAmount: string; - readonly formattedAverageMonthlySpend: string; - readonly formattedNextPaymentDate: string; - - constructor(viewData: { - totalSpent: number; - pendingAmount: number; - nextPaymentDate: string; - nextPaymentAmount: number; - activeSponsorships: number; - averageMonthlySpend: number; - formattedTotalSpent: string; - formattedPendingAmount: string; - formattedNextPaymentAmount: string; - formattedAverageMonthlySpend: string; - formattedNextPaymentDate: string; - }) { - super(); - this.totalSpent = viewData.totalSpent; - this.pendingAmount = viewData.pendingAmount; - this.nextPaymentDate = new Date(viewData.nextPaymentDate); - this.nextPaymentAmount = viewData.nextPaymentAmount; - this.activeSponsorships = viewData.activeSponsorships; - this.averageMonthlySpend = viewData.averageMonthlySpend; - this.formattedTotalSpent = viewData.formattedTotalSpent; - this.formattedPendingAmount = viewData.formattedPendingAmount; - this.formattedNextPaymentAmount = viewData.formattedNextPaymentAmount; - this.formattedAverageMonthlySpend = viewData.formattedAverageMonthlySpend; - this.formattedNextPaymentDate = viewData.formattedNextPaymentDate; - } -} \ No newline at end of file diff --git a/apps/website/lib/view-models/CompleteOnboardingViewModel.ts b/apps/website/lib/view-models/CompleteOnboardingViewModel.ts index c86fab6af..08f03fc13 100644 --- a/apps/website/lib/view-models/CompleteOnboardingViewModel.ts +++ b/apps/website/lib/view-models/CompleteOnboardingViewModel.ts @@ -1,7 +1,6 @@ -import type { CompleteOnboardingViewData } from '@/lib/builders/view-data/CompleteOnboardingViewData'; -import { OnboardingStatusDisplay } from '../display-objects/OnboardingStatusDisplay'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { OnboardingStatusDisplay } from '../display-objects/OnboardingStatusDisplay'; +import type { CompleteOnboardingViewData } from '../view-data/CompleteOnboardingViewData'; /** * Complete onboarding view model @@ -10,27 +9,35 @@ import { ViewModel } from "../contracts/view-models/ViewModel"; * Composes Display Objects and transforms ViewData for UI consumption. */ export class CompleteOnboardingViewModel extends ViewModel { - success: boolean; - driverId?: string; - errorMessage?: string; + private readonly data: CompleteOnboardingViewData; - // UI-specific derived fields (primitive outputs only) - readonly statusLabel: string; - readonly statusVariant: string; - readonly statusIcon: string; - readonly statusMessage: string; - - constructor(viewData: CompleteOnboardingViewData) { + constructor(data: CompleteOnboardingViewData) { super(); - this.success = viewData.success; - if (viewData.driverId !== undefined) this.driverId = viewData.driverId; - if (viewData.errorMessage !== undefined) this.errorMessage = viewData.errorMessage; + this.data = data; + } - // Derive UI-specific fields using Display Object - this.statusLabel = OnboardingStatusDisplay.statusLabel(this.success); - this.statusVariant = OnboardingStatusDisplay.statusVariant(this.success); - this.statusIcon = OnboardingStatusDisplay.statusIcon(this.success); - this.statusMessage = OnboardingStatusDisplay.statusMessage(this.success, this.errorMessage); + get success(): boolean { return this.data.success; } + get driverId(): string | undefined { return this.data.driverId; } + get errorMessage(): string | undefined { return this.data.errorMessage; } + + /** UI-specific: Status label using Display Object */ + get statusLabel(): string { + return OnboardingStatusDisplay.statusLabel(this.success); + } + + /** UI-specific: Status variant using Display Object */ + get statusVariant(): string { + return OnboardingStatusDisplay.statusVariant(this.success); + } + + /** UI-specific: Status icon using Display Object */ + get statusIcon(): string { + return OnboardingStatusDisplay.statusIcon(this.success); + } + + /** UI-specific: Status message using Display Object */ + get statusMessage(): string { + return OnboardingStatusDisplay.statusMessage(this.success, this.errorMessage); } /** UI-specific: Whether onboarding was successful */ diff --git a/apps/website/lib/view-models/CreateLeagueViewModel.ts b/apps/website/lib/view-models/CreateLeagueViewModel.ts index 314a0e2e3..760627a60 100644 --- a/apps/website/lib/view-models/CreateLeagueViewModel.ts +++ b/apps/website/lib/view-models/CreateLeagueViewModel.ts @@ -9,19 +9,19 @@ import { LeagueCreationStatusDisplay } from '../display-objects/LeagueCreationSt * Composes Display Objects and transforms ViewData for UI consumption. */ export class CreateLeagueViewModel extends ViewModel { - readonly leagueId: string; - readonly success: boolean; + private readonly data: CreateLeagueViewData; - // UI-specific derived fields (primitive outputs only) - readonly successMessage: string; - - constructor(viewData: CreateLeagueViewData) { + constructor(data: CreateLeagueViewData) { super(); - this.leagueId = viewData.leagueId; - this.success = viewData.success; + this.data = data; + } - // Derive UI-specific fields using Display Object - this.successMessage = LeagueCreationStatusDisplay.statusMessage(this.success); + get leagueId(): string { return this.data.leagueId; } + get success(): boolean { return this.data.success; } + + /** UI-specific: Success message using Display Object */ + get successMessage(): string { + return LeagueCreationStatusDisplay.statusMessage(this.success); } /** UI-specific: Whether league creation was successful */ diff --git a/apps/website/lib/view-models/CreateTeamViewModel.ts b/apps/website/lib/view-models/CreateTeamViewModel.ts index c8eb0e19e..cd0200e77 100644 --- a/apps/website/lib/view-models/CreateTeamViewModel.ts +++ b/apps/website/lib/view-models/CreateTeamViewModel.ts @@ -9,19 +9,19 @@ import { TeamCreationStatusDisplay } from '../display-objects/TeamCreationStatus * Composes Display Objects and transforms ViewData for UI consumption. */ export class CreateTeamViewModel extends ViewModel { - readonly teamId: string; - readonly success: boolean; + private readonly data: CreateTeamViewData; - // UI-specific derived fields (primitive outputs only) - readonly successMessage: string; - - constructor(viewData: CreateTeamViewData) { + constructor(data: CreateTeamViewData) { super(); - this.teamId = viewData.teamId; - this.success = viewData.success; + this.data = data; + } - // Derive UI-specific fields using Display Object - this.successMessage = TeamCreationStatusDisplay.statusMessage(this.success); + get teamId(): string { return this.data.teamId; } + get success(): boolean { return this.data.success; } + + /** UI-specific: Success message using Display Object */ + get successMessage(): string { + return TeamCreationStatusDisplay.statusMessage(this.success); } /** UI-specific: Whether team creation was successful */ diff --git a/apps/website/lib/view-models/DashboardStatsViewModel.ts b/apps/website/lib/view-models/DashboardStatsViewModel.ts new file mode 100644 index 000000000..0c484d122 --- /dev/null +++ b/apps/website/lib/view-models/DashboardStatsViewModel.ts @@ -0,0 +1,74 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { ActivityLevelDisplay } from "@/lib/display-objects/ActivityLevelDisplay"; +import type { DashboardStatsViewData } from '@/lib/view-data/DashboardStatsViewData'; + +/** + * 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); + } +} diff --git a/apps/website/lib/view-models/DeleteMediaViewModel.ts b/apps/website/lib/view-models/DeleteMediaViewModel.ts index f5cabb501..d4caca658 100644 --- a/apps/website/lib/view-models/DeleteMediaViewModel.ts +++ b/apps/website/lib/view-models/DeleteMediaViewModel.ts @@ -1,5 +1,5 @@ -import type { DeleteMediaViewData } from '@/lib/builders/view-data/DeleteMediaViewData'; import { ViewModel } from '../contracts/view-models/ViewModel'; +import type { DeleteMediaViewData } from '../view-data/DeleteMediaViewData'; /** * Delete Media View Model @@ -8,17 +8,16 @@ import { ViewModel } from '../contracts/view-models/ViewModel'; * Composes ViewData for UI consumption. */ export class DeleteMediaViewModel extends ViewModel { - success: boolean; - error?: string; + private readonly data: DeleteMediaViewData; - constructor(viewData: DeleteMediaViewData) { + constructor(data: DeleteMediaViewData) { super(); - this.success = viewData.success; - if (viewData.error !== undefined) { - this.error = viewData.error; - } + this.data = data; } + get success(): boolean { return this.data.success; } + get error(): string | undefined { return this.data.error; } + /** UI-specific: Whether the deletion was successful */ get isSuccessful(): boolean { return this.success; diff --git a/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts b/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts index aa0939c65..8b7480b63 100644 --- a/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts +++ b/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts @@ -1,41 +1,32 @@ -import type { LeaderboardDriverItem } from '@/lib/view-data/LeaderboardDriverItem'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeaderboardDriverItem } from '../view-data/LeaderboardDriverItem'; import { SkillLevelDisplay } from "../display-objects/SkillLevelDisplay"; import { SkillLevelIconDisplay } from "../display-objects/SkillLevelIconDisplay"; import { WinRateDisplay } from "../display-objects/WinRateDisplay"; import { RatingTrendDisplay } from "../display-objects/RatingTrendDisplay"; export class DriverLeaderboardItemViewModel extends ViewModel { - id: string; - name: string; - rating: number; - skillLevel: string; - nationality: string; - racesCompleted: number; - wins: number; - podiums: number; - rank: number; - avatarUrl: string; - position: number; - private previousRating: number | undefined; + private readonly data: LeaderboardDriverItem; + private readonly previousRating: number | undefined; - constructor(viewData: LeaderboardDriverItem, previousRating?: number) { + constructor(data: LeaderboardDriverItem, previousRating?: number) { super(); - this.id = viewData.id; - this.name = viewData.name; - this.rating = viewData.rating; - this.skillLevel = viewData.skillLevel; - this.nationality = viewData.nationality; - this.racesCompleted = viewData.racesCompleted; - this.wins = viewData.wins; - this.podiums = viewData.podiums; - this.rank = viewData.rank; - this.avatarUrl = viewData.avatarUrl; - this.position = viewData.position; + this.data = data; this.previousRating = previousRating; } + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get rating(): number { return this.data.rating; } + get skillLevel(): string { return this.data.skillLevel; } + get nationality(): string { return this.data.nationality; } + get racesCompleted(): number { return this.data.racesCompleted; } + get wins(): number { return this.data.wins; } + get podiums(): number { return this.data.podiums; } + get rank(): number { return this.data.rank; } + get avatarUrl(): string { return this.data.avatarUrl; } + get position(): number { return this.data.position; } + /** UI-specific: Skill level color */ get skillLevelColor(): string { return SkillLevelDisplay.getColor(this.skillLevel); diff --git a/apps/website/lib/view-models/DriverLeaderboardViewModel.test.ts b/apps/website/lib/view-models/DriverLeaderboardViewModel.test.ts index fc7f6147e..b9f822be8 100644 --- a/apps/website/lib/view-models/DriverLeaderboardViewModel.test.ts +++ b/apps/website/lib/view-models/DriverLeaderboardViewModel.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect } from 'vitest'; -import { DriverLeaderboardViewModel } from './DriverLeaderboardViewModel'; +import { describe, expect, it } from 'vitest'; +import { LeaderboardDriverItem } from '../view-data/LeaderboardDriverItem'; +import { LeaderboardsViewData } from '../view-data/LeaderboardsViewData'; import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel'; -import { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData'; -import { LeaderboardDriverItem } from '@/lib/view-data/LeaderboardDriverItem'; +import { DriverLeaderboardViewModel } from './DriverLeaderboardViewModel'; const createDriverViewData = (overrides: Partial = {}): LeaderboardDriverItem => ({ id: 'driver-1', diff --git a/apps/website/lib/view-models/DriverLeaderboardViewModel.ts b/apps/website/lib/view-models/DriverLeaderboardViewModel.ts index 7695b9817..da4dd2c1e 100644 --- a/apps/website/lib/view-models/DriverLeaderboardViewModel.ts +++ b/apps/website/lib/view-models/DriverLeaderboardViewModel.ts @@ -1,17 +1,18 @@ -import { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData'; -import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel'; +import type { LeaderboardsViewData } from '../view-data/LeaderboardsViewData'; export class DriverLeaderboardViewModel extends ViewModel { - drivers: DriverLeaderboardItemViewModel[]; + private readonly data: LeaderboardsViewData; + readonly drivers: DriverLeaderboardItemViewModel[]; constructor( - viewData: LeaderboardsViewData, + data: LeaderboardsViewData, previousRatings?: Record, ) { super(); - this.drivers = viewData.drivers.map((driver) => { + this.data = data; + this.drivers = data.drivers.map((driver) => { const previousRating = previousRatings?.[driver.id]; return new DriverLeaderboardItemViewModel(driver, previousRating); }); diff --git a/apps/website/lib/view-models/DriverSummaryViewModel.ts b/apps/website/lib/view-models/DriverSummaryViewModel.ts index 3926c03df..af8dc4677 100644 --- a/apps/website/lib/view-models/DriverSummaryViewModel.ts +++ b/apps/website/lib/view-models/DriverSummaryViewModel.ts @@ -1,5 +1,5 @@ import { ViewModel } from "../contracts/view-models/ViewModel"; -import type { DriverSummaryData } from "../view-data/LeagueDetailViewData"; +import type { DriverSummaryData } from "../view-data/DriverSummaryData"; import { NumberDisplay } from "../display-objects/NumberDisplay"; import { RatingDisplay } from "../display-objects/RatingDisplay"; diff --git a/apps/website/lib/view-models/DriverViewModel.ts b/apps/website/lib/view-models/DriverViewModel.ts index 050756a85..6230cdb74 100644 --- a/apps/website/lib/view-models/DriverViewModel.ts +++ b/apps/website/lib/view-models/DriverViewModel.ts @@ -5,35 +5,26 @@ * Note: client-only ViewModel created from ViewData (never DTO). */ import { ViewModel } from "../contracts/view-models/ViewModel"; -import type { DriverData } from "../view-data/LeagueStandingsViewData"; - -type DriverViewModelViewData = DriverData & { - bio?: string; - joinedAt?: string; -}; +import { RatingDisplay } from "../display-objects/RatingDisplay"; +import type { DriverViewData } from "../view-data/DriverViewData"; export class DriverViewModel extends ViewModel { - id: string; - name: string; - avatarUrl: string | null; - iracingId?: string; - rating?: number; - country?: string; - bio?: string; - joinedAt?: string; + private readonly data: DriverViewData; - constructor(viewData: DriverViewModelViewData) { + constructor(data: DriverViewData) { super(); - this.id = viewData.id; - this.name = viewData.name; - this.avatarUrl = viewData.avatarUrl ?? null; - if (viewData.iracingId !== undefined) this.iracingId = viewData.iracingId; - if (viewData.rating !== undefined) this.rating = viewData.rating; - if (viewData.country !== undefined) this.country = viewData.country; - if (viewData.bio !== undefined) this.bio = viewData.bio; - if (viewData.joinedAt !== undefined) this.joinedAt = viewData.joinedAt; + this.data = data; } + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get avatarUrl(): string { return this.data.avatarUrl || ''; } + get iracingId(): string | undefined { return this.data.iracingId; } + get rating(): number | undefined { return this.data.rating; } + get country(): string | undefined { return this.data.country; } + get bio(): string | undefined { return this.data.bio; } + get joinedAt(): string | undefined { return this.data.joinedAt; } + /** UI-specific: Whether driver has an iRacing ID */ get hasIracingId(): boolean { return !!this.iracingId; @@ -41,6 +32,6 @@ export class DriverViewModel extends ViewModel { /** UI-specific: Formatted rating */ get formattedRating(): string { - return this.rating ? this.rating.toFixed(0) : "Unrated"; + return this.rating !== undefined ? RatingDisplay.format(this.rating) : "Unrated"; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/EmailSignupViewModel.ts b/apps/website/lib/view-models/EmailSignupViewModel.ts index 5aad392f8..c190cf97e 100644 --- a/apps/website/lib/view-models/EmailSignupViewModel.ts +++ b/apps/website/lib/view-models/EmailSignupViewModel.ts @@ -7,20 +7,23 @@ import { ViewModel } from "../contracts/view-models/ViewModel"; import type { EmailSignupViewData } from "../view-data/EmailSignupViewData"; export class EmailSignupViewModel extends ViewModel { - constructor(private readonly viewData: EmailSignupViewData) { + private readonly data: EmailSignupViewData; + + constructor(data: EmailSignupViewData) { super(); + this.data = data; } get email(): string { - return this.viewData.email; + return this.data.email; } get message(): string { - return this.viewData.message; + return this.data.message; } get status(): EmailSignupViewData["status"] { - return this.viewData.status; + return this.data.status; } get isSuccess(): boolean { diff --git a/apps/website/lib/view-models/HomeDiscoveryViewModel.ts b/apps/website/lib/view-models/HomeDiscoveryViewModel.ts index febe3efcb..da2c0a2bd 100644 --- a/apps/website/lib/view-models/HomeDiscoveryViewModel.ts +++ b/apps/website/lib/view-models/HomeDiscoveryViewModel.ts @@ -7,17 +7,17 @@ import type { HomeDiscoveryViewData } from '@/lib/view-data/HomeDiscoveryViewDat import { ViewModel } from '../contracts/view-models/ViewModel'; export class HomeDiscoveryViewModel extends ViewModel { - readonly topLeagues: HomeDiscoveryViewData['topLeagues']; - readonly teams: HomeDiscoveryViewData['teams']; - readonly upcomingRaces: HomeDiscoveryViewData['upcomingRaces']; + private readonly data: HomeDiscoveryViewData; - constructor(viewData: HomeDiscoveryViewData) { + constructor(data: HomeDiscoveryViewData) { super(); - this.topLeagues = viewData.topLeagues; - this.teams = viewData.teams; - this.upcomingRaces = viewData.upcomingRaces; + this.data = data; } + get topLeagues() { return this.data.topLeagues; } + get teams() { return this.data.teams; } + get upcomingRaces() { return this.data.upcomingRaces; } + get hasTopLeagues(): boolean { return this.topLeagues.length > 0; } diff --git a/apps/website/lib/view-models/ImportRaceResultsSummaryViewModel.ts b/apps/website/lib/view-models/ImportRaceResultsSummaryViewModel.ts index 4f3c2ec3d..3c1febbcd 100644 --- a/apps/website/lib/view-models/ImportRaceResultsSummaryViewModel.ts +++ b/apps/website/lib/view-models/ImportRaceResultsSummaryViewModel.ts @@ -2,28 +2,31 @@ import { ViewModel } from "../contracts/view-models/ViewModel"; import type { ImportRaceResultsSummaryViewData } from "../view-data/ImportRaceResultsSummaryViewData"; export class ImportRaceResultsSummaryViewModel extends ViewModel { - constructor(private readonly viewData: ImportRaceResultsSummaryViewData) { + private readonly data: ImportRaceResultsSummaryViewData; + + constructor(data: ImportRaceResultsSummaryViewData) { super(); + this.data = data; } get success(): boolean { - return this.viewData.success; + return this.data.success; } get raceId(): string { - return this.viewData.raceId; + return this.data.raceId; } get driversProcessed(): number { - return this.viewData.driversProcessed; + return this.data.driversProcessed; } get resultsRecorded(): number { - return this.viewData.resultsRecorded; + return this.data.resultsRecorded; } get errors(): string[] { - return this.viewData.errors; + return this.data.errors; } get hasErrors(): boolean { diff --git a/apps/website/lib/view-models/InvoiceViewModel.ts b/apps/website/lib/view-models/InvoiceViewModel.ts new file mode 100644 index 000000000..9c6e84310 --- /dev/null +++ b/apps/website/lib/view-models/InvoiceViewModel.ts @@ -0,0 +1,27 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { InvoiceViewData } from "@/lib/view-data/BillingViewData"; + +export class InvoiceViewModel extends ViewModel { + private readonly data: InvoiceViewData; + + constructor(data: InvoiceViewData) { + super(); + this.data = data; + } + + get id(): string { return this.data.id; } + get invoiceNumber(): string { return this.data.invoiceNumber; } + get date(): string { return this.data.date; } + get dueDate(): string { return this.data.dueDate; } + get amount(): number { return this.data.amount; } + get vatAmount(): number { return this.data.vatAmount; } + get totalAmount(): number { return this.data.totalAmount; } + get status(): string { return this.data.status; } + get description(): string { return this.data.description; } + get sponsorshipType(): string { return this.data.sponsorshipType; } + get pdfUrl(): string { return this.data.pdfUrl; } + get totalAmountFormatted(): string { return this.data.formattedTotalAmount; } + get vatAmountFormatted(): string { return this.data.formattedVatAmount; } + get dateFormatted(): string { return this.data.formattedDate; } + get isOverdue(): boolean { return this.data.isOverdue; } +} diff --git a/apps/website/lib/view-models/LeagueAdminRosterJoinRequestViewModel.ts b/apps/website/lib/view-models/LeagueAdminRosterJoinRequestViewModel.ts index 6107e31c1..83e2f160b 100644 --- a/apps/website/lib/view-models/LeagueAdminRosterJoinRequestViewModel.ts +++ b/apps/website/lib/view-models/LeagueAdminRosterJoinRequestViewModel.ts @@ -1,10 +1,18 @@ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueAdminRosterJoinRequestViewData } from "../view-data/LeagueAdminRosterJoinRequestViewData"; -export interface LeagueAdminRosterJoinRequestViewModel extends ViewModel { - id: string; - leagueId: string; - driverId: string; - driverName: string; - requestedAtIso: string; - message?: string; +export class LeagueAdminRosterJoinRequestViewModel extends ViewModel { + private readonly data: LeagueAdminRosterJoinRequestViewData; + + constructor(data: LeagueAdminRosterJoinRequestViewData) { + super(); + this.data = data; + } + + get id(): string { return this.data.id; } + get leagueId(): string { return this.data.leagueId; } + get driverId(): string { return this.data.driverId; } + get driverName(): string { return this.data.driverName; } + get requestedAtIso(): string { return this.data.requestedAtIso; } + get message(): string | undefined { return this.data.message; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueAdminRosterMemberViewModel.ts b/apps/website/lib/view-models/LeagueAdminRosterMemberViewModel.ts index c5b09782d..a27ce6747 100644 --- a/apps/website/lib/view-models/LeagueAdminRosterMemberViewModel.ts +++ b/apps/website/lib/view-models/LeagueAdminRosterMemberViewModel.ts @@ -1,10 +1,17 @@ -import type { MembershipRole } from '@/lib/types/MembershipRole'; - +import type { MembershipRole } from '../types/MembershipRole'; import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueAdminRosterMemberViewData } from "../view-data/LeagueAdminRosterMemberViewData"; -export interface LeagueAdminRosterMemberViewModel extends ViewModel { - driverId: string; - driverName: string; - role: MembershipRole; - joinedAtIso: string; +export class LeagueAdminRosterMemberViewModel extends ViewModel { + private readonly data: LeagueAdminRosterMemberViewData; + + constructor(data: LeagueAdminRosterMemberViewData) { + super(); + this.data = data; + } + + get driverId(): string { return this.data.driverId; } + get driverName(): string { return this.data.driverName; } + get role(): MembershipRole { return this.data.role; } + get joinedAtIso(): string { return this.data.joinedAtIso; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueAdminScheduleViewModel.ts b/apps/website/lib/view-models/LeagueAdminScheduleViewModel.ts index a1799bd42..e5e1c68d6 100644 --- a/apps/website/lib/view-models/LeagueAdminScheduleViewModel.ts +++ b/apps/website/lib/view-models/LeagueAdminScheduleViewModel.ts @@ -1,15 +1,15 @@ -import type { LeagueScheduleRaceViewModel } from './LeagueScheduleViewModel'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueAdminScheduleViewData } from "../view-data/LeagueAdminScheduleViewData"; export class LeagueAdminScheduleViewModel extends ViewModel { - readonly seasonId: string; - readonly published: boolean; - readonly races: LeagueScheduleRaceViewModel[]; + private readonly data: LeagueAdminScheduleViewData; - constructor(input: { seasonId: string; published: boolean; races: LeagueScheduleRaceViewModel[] }) { - this.seasonId = input.seasonId; - this.published = input.published; - this.races = input.races; + constructor(data: LeagueAdminScheduleViewData) { + super(); + this.data = data; } + + get seasonId(): string { return this.data.seasonId; } + get published(): boolean { return this.data.published; } + get races(): any[] { return this.data.races; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueAdminViewModel.ts b/apps/website/lib/view-models/LeagueAdminViewModel.ts index 5b6671214..930a9af29 100644 --- a/apps/website/lib/view-models/LeagueAdminViewModel.ts +++ b/apps/website/lib/view-models/LeagueAdminViewModel.ts @@ -1,27 +1,20 @@ -import type { LeagueMemberViewModel } from './LeagueMemberViewModel'; -import type { LeagueJoinRequestViewModel } from './LeagueJoinRequestViewModel'; - -/** - * League admin view model - * Transform from DTO to ViewModel with UI fields - */ +import { LeagueMemberViewModel } from './LeagueMemberViewModel'; import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueAdminViewData } from "../view-data/LeagueAdminViewData"; export class LeagueAdminViewModel extends ViewModel { - config: unknown; - members: LeagueMemberViewModel[]; - joinRequests: LeagueJoinRequestViewModel[]; + private readonly data: LeagueAdminViewData; + readonly members: LeagueMemberViewModel[]; - constructor(dto: { - config: unknown; - members: LeagueMemberViewModel[]; - joinRequests: LeagueJoinRequestViewModel[]; - }) { - this.config = dto.config; - this.members = dto.members; - this.joinRequests = dto.joinRequests; + constructor(data: LeagueAdminViewData) { + super(); + this.data = data; + this.members = data.members.map(m => new LeagueMemberViewModel(m)); } + get config(): unknown { return this.data.config; } + get joinRequests(): any[] { return this.data.joinRequests; } + /** UI-specific: Total pending requests count */ get pendingRequestsCount(): number { return this.joinRequests.length; diff --git a/apps/website/lib/view-models/LeagueCardViewModel.ts b/apps/website/lib/view-models/LeagueCardViewModel.ts index c3a602fbe..9aa695908 100644 --- a/apps/website/lib/view-models/LeagueCardViewModel.ts +++ b/apps/website/lib/view-models/LeagueCardViewModel.ts @@ -1,25 +1,15 @@ -import type { LeagueSummaryDTO } from '@/lib/types/generated/LeagueSummaryDTO'; - -interface LeagueCardDTO { - id: string; - name: string; - description?: string; -} - -/** - * League card view model - * UI representation of a league on the landing page. - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueCardViewData } from "../view-data/LeagueCardViewData"; export class LeagueCardViewModel extends ViewModel { - readonly id: string; - readonly name: string; - readonly description: string; + private readonly data: LeagueCardViewData; - constructor(dto: LeagueCardDTO | LeagueSummaryDTO & { description?: string }) { - this.id = dto.id; - this.name = dto.name; - this.description = dto.description ?? 'Competitive iRacing league'; + constructor(data: LeagueCardViewData) { + super(); + this.data = data; } + + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get description(): string { return this.data.description ?? 'Competitive iRacing league'; } } diff --git a/apps/website/lib/view-models/LeagueDetailDriverViewModel.ts b/apps/website/lib/view-models/LeagueDetailDriverViewModel.ts new file mode 100644 index 000000000..31a946ce7 --- /dev/null +++ b/apps/website/lib/view-models/LeagueDetailDriverViewModel.ts @@ -0,0 +1,19 @@ +import { DriverViewModel } from "./DriverViewModel"; +import type { DriverViewData } from "../view-data/DriverViewData"; + +export class LeagueDetailDriverViewModel extends DriverViewModel { + private readonly detailData: DriverViewData & { impressions: number }; + + constructor(data: DriverViewData & { impressions: number }) { + super(data); + this.detailData = data; + } + + get impressions(): number { + return this.detailData.impressions; + } + + get formattedImpressions(): string { + return this.impressions.toLocaleString(); + } +} diff --git a/apps/website/lib/view-models/LeagueDetailPageViewModel.ts b/apps/website/lib/view-models/LeagueDetailPageViewModel.ts index cc15e8019..df6f5d4a4 100644 --- a/apps/website/lib/view-models/LeagueDetailPageViewModel.ts +++ b/apps/website/lib/view-models/LeagueDetailPageViewModel.ts @@ -1,192 +1,79 @@ -import { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO'; -import { LeagueStatsDTO } from '@/lib/types/generated/LeagueStatsDTO'; -import { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO'; -import { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO'; -import { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO'; +import { ViewModel } from "../contracts/view-models/ViewModel"; import { RaceViewModel } from './RaceViewModel'; import { DriverViewModel } from './DriverViewModel'; +import type { LeagueDetailPageViewData, LeagueMembershipWithRole, SponsorInfo } from '../view-data/LeagueDetailPageViewData'; -// Sponsor info type -export interface SponsorInfo { - id: string; - name: string; - logoUrl?: string; - websiteUrl?: string; - tier: 'main' | 'secondary'; - tagline?: string; -} - -// Driver summary for management section export interface DriverSummary { driver: DriverViewModel; rating: number | null; rank: number | null; } -// League membership with role -export interface LeagueMembershipWithRole { - driverId: string; - role: 'owner' | 'admin' | 'steward' | 'member'; - status: 'active' | 'inactive'; - joinedAt: string; -} - -// Helper interfaces for type narrowing -interface LeagueSettings { - maxDrivers?: number; -} - -interface SocialLinks { - discordUrl?: string; - youtubeUrl?: string; - websiteUrl?: string; -} - -interface LeagueStatsExtended { - averageSOF?: number; - averageRating?: number; - completedRaces?: number; - totalRaces?: number; -} - -interface MembershipsContainer { - members?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>; - memberships?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>; -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - export class LeagueDetailPageViewModel extends ViewModel { - // League basic info - id: string; - name: string; - description?: string; - ownerId: string; - createdAt: string; - settings: { - maxDrivers?: number; - }; - socialLinks: { - discordUrl?: string; - youtubeUrl?: string; - websiteUrl?: string; - } | undefined; + private readonly data: LeagueDetailPageViewData; + readonly allRaces: RaceViewModel[]; + readonly runningRaces: RaceViewModel[]; + readonly ownerSummary: DriverSummary | null; + readonly adminSummaries: DriverSummary[]; + readonly stewardSummaries: DriverSummary[]; - // Owner info - owner: GetDriverOutputDTO | null; - - // Scoring configuration - scoringConfig: LeagueScoringConfigDTO | null; - - // Drivers and memberships - drivers: GetDriverOutputDTO[]; - memberships: LeagueMembershipWithRole[]; - - // Races - allRaces: RaceViewModel[]; - runningRaces: RaceViewModel[]; - - // Stats - averageSOF: number | null; - completedRacesCount: number; - - // Sponsors - sponsors: SponsorInfo[]; - - // Sponsor insights data - sponsorInsights: { - avgViewsPerRace: number; - totalImpressions: number; - engagementRate: string; - estimatedReach: number; - mainSponsorAvailable: boolean; - secondarySlotsAvailable: number; - mainSponsorPrice: number; - secondaryPrice: number; - tier: 'premium' | 'standard' | 'starter'; - trustScore: number; - discordMembers: number; - monthlyActivity: number; - }; - - // Driver summaries for management - ownerSummary: DriverSummary | null; - adminSummaries: DriverSummary[]; - stewardSummaries: DriverSummary[]; - - constructor( - league: LeagueWithCapacityAndScoringDTO, - owner: GetDriverOutputDTO | null, - scoringConfig: LeagueScoringConfigDTO | null, - drivers: GetDriverOutputDTO[], - memberships: LeagueMembershipsDTO, - allRaces: RaceViewModel[], - leagueStats: LeagueStatsDTO, - sponsors: SponsorInfo[] - ) { - this.id = league.id; - this.name = league.name; - this.description = league.description ?? ''; - this.ownerId = league.ownerId; - this.createdAt = league.createdAt; + constructor(data: LeagueDetailPageViewData) { + super(); + this.data = data; - // Handle settings with proper type narrowing - const settings = league.settings as LeagueSettings | undefined; - const maxDrivers = settings?.maxDrivers; - this.settings = { - maxDrivers: maxDrivers, - }; - - // Handle social links with proper type narrowing - const socialLinks = league.socialLinks as SocialLinks | undefined; - const discordUrl = socialLinks?.discordUrl; - const youtubeUrl = socialLinks?.youtubeUrl; - const websiteUrl = socialLinks?.websiteUrl; - - this.socialLinks = { - discordUrl, - youtubeUrl, - websiteUrl, + this.allRaces = data.allRaces.map(r => r instanceof RaceViewModel ? r : new RaceViewModel(r)); + this.runningRaces = this.allRaces.filter(r => r.status === 'running'); + + // Build driver summaries + this.ownerSummary = this.buildDriverSummary(data.ownerId); + this.adminSummaries = data.memberships + .filter(m => m.role === 'admin') + .slice(0, 3) + .map(m => this.buildDriverSummary(m.driverId)) + .filter((s): s is DriverSummary => s !== null); + this.stewardSummaries = data.memberships + .filter(m => m.role === 'steward') + .slice(0, 3) + .map(m => this.buildDriverSummary(m.driverId)) + .filter((s): s is DriverSummary => s !== null); + } + + private buildDriverSummary(driverId: string): DriverSummary | null { + const driverData = this.data.drivers.find(d => d.id === driverId) || + (this.data.owner?.id === driverId ? this.data.owner : null); + + if (!driverData) return null; + + const driver = new DriverViewModel(driverData); + + return { + driver, + rating: null, + rank: null, }; + } - this.owner = owner; - this.scoringConfig = scoringConfig; - this.drivers = drivers; + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get description(): string { return this.data.description ?? ''; } + get ownerId(): string { return this.data.ownerId; } + get createdAt(): string { return this.data.createdAt; } + get settings() { return this.data.settings; } + get socialLinks() { return this.data.socialLinks; } + get owner() { return this.data.owner; } + get scoringConfig() { return this.data.scoringConfig; } + get drivers() { return this.data.drivers; } + get memberships() { return this.data.memberships; } + get averageSOF(): number | null { return this.data.averageSOF; } + get completedRacesCount(): number { return this.data.completedRacesCount; } + get sponsors(): SponsorInfo[] { return this.data.sponsors; } - // Handle memberships with proper type narrowing - const membershipsContainer = memberships as MembershipsContainer; - const membershipDtos = membershipsContainer.members ?? - membershipsContainer.memberships ?? - []; - - this.memberships = membershipDtos.map((m) => ({ - driverId: m.driverId, - role: m.role as 'owner' | 'admin' | 'steward' | 'member', - status: m.status ?? 'active', - joinedAt: m.joinedAt, - })); - - this.allRaces = allRaces; - this.runningRaces = allRaces.filter(r => r.status === 'running'); - - // Calculate SOF from available data with proper type narrowing - const statsExtended = leagueStats as LeagueStatsExtended; - const averageSOF = statsExtended.averageSOF ?? - statsExtended.averageRating ?? undefined; - const completedRaces = statsExtended.completedRaces ?? - statsExtended.totalRaces ?? undefined; - - this.averageSOF = typeof averageSOF === 'number' ? averageSOF : null; - this.completedRacesCount = typeof completedRaces === 'number' ? completedRaces : 0; - - this.sponsors = sponsors; - - // Calculate sponsor insights + get sponsorInsights() { const memberCount = this.memberships.length; const mainSponsorTaken = this.sponsors.some(s => s.tier === 'main'); const secondaryTaken = this.sponsors.filter(s => s.tier === 'secondary').length; - this.sponsorInsights = { + return { avgViewsPerRace: 5400 + memberCount * 50, totalImpressions: 45000 + memberCount * 500, engagementRate: (3.5 + (memberCount / 50)).toFixed(1), @@ -200,59 +87,17 @@ export class LeagueDetailPageViewModel extends ViewModel { discordMembers: memberCount * 3, monthlyActivity: Math.min(100, 40 + this.completedRacesCount * 2), }; - - // Build driver summaries - this.ownerSummary = this.buildDriverSummary(this.ownerId); - this.adminSummaries = this.memberships - .filter(m => m.role === 'admin') - .slice(0, 3) - .map(m => this.buildDriverSummary(m.driverId)) - .filter((s): s is DriverSummary => s !== null); - this.stewardSummaries = this.memberships - .filter(m => m.role === 'steward') - .slice(0, 3) - .map(m => this.buildDriverSummary(m.driverId)) - .filter((s): s is DriverSummary => s !== null); } - private buildDriverSummary(driverId: string): DriverSummary | null { - const driverDto = this.drivers.find(d => d.id === driverId); - if (!driverDto) return null; - - // Handle avatarUrl with proper type checking - const driverAny = driverDto as { avatarUrl?: unknown }; - const avatarUrl = typeof driverAny.avatarUrl === 'string' ? driverAny.avatarUrl : null; - - const driver = new DriverViewModel({ - id: driverDto.id, - name: driverDto.name, - avatarUrl: avatarUrl, - iracingId: driverDto.iracingId, - }); - - // Detailed rating and rank data are not wired from the analytics services yet; - // expose the driver identity only so the UI can still render role assignments. - return { - driver, - rating: null, - rank: null, - }; - } - - // UI helper methods get isSponsorMode(): boolean { - // League detail pages are rendered in organizer mode in this build; sponsor-specific - // mode switches will be introduced once sponsor dashboards share this view model. return false; } get currentUserMembership(): LeagueMembershipWithRole | null { - // Current user identity is not available in this view model context yet; callers must - // pass an explicit membership if they need per-user permissions. return null; } get canEndRaces(): boolean { return this.currentUserMembership?.role === 'admin' || this.currentUserMembership?.role === 'owner'; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/LeagueDetailRaceViewModel.ts b/apps/website/lib/view-models/LeagueDetailRaceViewModel.ts new file mode 100644 index 000000000..9e43c4501 --- /dev/null +++ b/apps/website/lib/view-models/LeagueDetailRaceViewModel.ts @@ -0,0 +1,19 @@ +import { RaceViewModel } from "./RaceViewModel"; +import type { RaceViewData } from "../view-data/RaceViewData"; + +export class LeagueDetailRaceViewModel extends RaceViewModel { + private readonly detailData: RaceViewData & { views: number }; + + constructor(data: RaceViewData & { views: number }) { + super(data); + this.detailData = data; + } + + get views(): number { + return this.detailData.views; + } + + get formattedViews(): string { + return this.views.toLocaleString(); + } +} diff --git a/apps/website/lib/view-models/LeagueDetailViewModel.ts b/apps/website/lib/view-models/LeagueDetailViewModel.ts index 1a6c5e2f0..344109a2c 100644 --- a/apps/website/lib/view-models/LeagueDetailViewModel.ts +++ b/apps/website/lib/view-models/LeagueDetailViewModel.ts @@ -1,135 +1,20 @@ -import { DriverViewModel as SharedDriverViewModel } from "./DriverViewModel"; -import { RaceViewModel as SharedRaceViewModel } from "./RaceViewModel"; - -/** - * League Detail View Model - * - * View model for detailed league information for sponsors. - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import { LeagueViewModel } from "./LeagueViewModel"; +import { LeagueDetailDriverViewModel } from "./LeagueDetailDriverViewModel"; +import { LeagueDetailRaceViewModel } from "./LeagueDetailRaceViewModel"; +import type { LeagueDetailViewData } from "../view-data/LeagueDetailViewData"; export class LeagueDetailViewModel extends ViewModel { - league: LeagueViewModel; - drivers: LeagueDetailDriverViewModel[]; - races: LeagueDetailRaceViewModel[]; + private readonly data: LeagueDetailViewData; + readonly league: LeagueViewModel; + readonly drivers: LeagueDetailDriverViewModel[]; + readonly races: LeagueDetailRaceViewModel[]; - constructor(data: { league: unknown; drivers: unknown[]; races: unknown[] }) { + constructor(data: LeagueDetailViewData) { + super(); + this.data = data; this.league = new LeagueViewModel(data.league); - this.drivers = data.drivers.map(driver => new LeagueDetailDriverViewModel(driver as any)); - this.races = data.races.map(race => new LeagueDetailRaceViewModel(race as any)); - } -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export class LeagueDetailDriverViewModel extends SharedDriverViewModel extends ViewModel { - impressions: number; - - constructor(dto: any) { - super(dto); - this.impressions = dto.impressions || 0; - } - - get formattedImpressions(): string { - return this.impressions.toLocaleString(); - } -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export class LeagueDetailRaceViewModel extends SharedRaceViewModel extends ViewModel { - views: number; - - constructor(dto: any) { - super(dto); - this.views = dto.views || 0; - } - - get formattedViews(): string { - return this.views.toLocaleString(); - } -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export class LeagueViewModel extends ViewModel { - id: string; - name: string; - 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[] }; - }; - - constructor(data: unknown) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const d = data as any; - this.id = d.id; - this.name = d.name; - this.game = d.game; - this.tier = d.tier; - this.season = d.season; - this.description = d.description; - this.drivers = d.drivers; - this.races = d.races; - this.completedRaces = d.completedRaces; - this.totalImpressions = d.totalImpressions; - this.avgViewsPerRace = d.avgViewsPerRace; - this.engagement = d.engagement; - this.rating = d.rating; - this.seasonStatus = d.seasonStatus; - this.seasonDates = d.seasonDates; - this.nextRace = d.nextRace; - this.sponsorSlots = d.sponsorSlots; - } - - get formattedTotalImpressions(): string { - return this.totalImpressions.toLocaleString(); - } - - get formattedAvgViewsPerRace(): string { - return this.avgViewsPerRace.toLocaleString(); - } - - get projectedTotalViews(): number { - return Math.round(this.avgViewsPerRace * this.races); - } - - get formattedProjectedTotal(): string { - return this.projectedTotalViews.toLocaleString(); - } - - get mainSponsorCpm(): number { - return Math.round((this.sponsorSlots.main.price / this.projectedTotalViews) * 1000); - } - - get formattedMainSponsorCpm(): string { - return `$${this.mainSponsorCpm.toFixed(2)}`; - } - - get racesLeft(): number { - return this.races - this.completedRaces; - } - - get tierConfig() { - const configs = { - premium: { color: 'text-yellow-400', bgColor: 'bg-yellow-500/10', border: 'border-yellow-500/30' }, - standard: { color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', border: 'border-primary-blue/30' }, - starter: { color: 'text-gray-400', bgColor: 'bg-gray-500/10', border: 'border-gray-500/30' }, - }; - return configs[this.tier]; + this.drivers = data.drivers.map(driver => new LeagueDetailDriverViewModel(driver)); + this.races = data.races.map(race => new LeagueDetailRaceViewModel(race)); } } diff --git a/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts b/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts index 65b1b5e62..4d348b9f8 100644 --- a/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts +++ b/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts @@ -1,27 +1,19 @@ -import type { LeagueJoinRequestDTO } from '@/lib/types/generated/LeagueJoinRequestDTO'; - -/** - * League join request view model - * Transform from DTO to ViewModel with UI fields - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueJoinRequestViewData } from "../view-data/LeagueJoinRequestViewData"; export class LeagueJoinRequestViewModel extends ViewModel { - id: string; - leagueId: string; - driverId: string; - requestedAt: string; + private readonly data: LeagueJoinRequestViewData; - private isAdmin: boolean; - - constructor(dto: LeagueJoinRequestDTO, currentUserId: string, isAdmin: boolean) { - this.id = dto.id; - this.leagueId = dto.leagueId; - this.driverId = dto.driverId; - this.requestedAt = dto.requestedAt; - this.isAdmin = isAdmin; + constructor(data: LeagueJoinRequestViewData) { + super(); + this.data = data; } + get id(): string { return this.data.id; } + get leagueId(): string { return this.data.leagueId; } + get driverId(): string { return this.data.driverId; } + get requestedAt(): string { return this.data.requestedAt; } + /** UI-specific: Formatted request date */ get formattedRequestedAt(): string { return new Date(this.requestedAt).toLocaleString(); @@ -29,11 +21,11 @@ export class LeagueJoinRequestViewModel extends ViewModel { /** UI-specific: Whether the request can be approved by current user */ get canApprove(): boolean { - return this.isAdmin; + return this.data.isAdmin; } /** UI-specific: Whether the request can be rejected by current user */ get canReject(): boolean { - return this.isAdmin; + return this.data.isAdmin; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueMemberViewModel.ts b/apps/website/lib/view-models/LeagueMemberViewModel.ts index fd223072c..192ec0216 100644 --- a/apps/website/lib/view-models/LeagueMemberViewModel.ts +++ b/apps/website/lib/view-models/LeagueMemberViewModel.ts @@ -1,36 +1,29 @@ -import { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO'; -import { DriverViewModel } from './DriverViewModel'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { LeagueRoleDisplay, LeagueRole } from "../display-objects/LeagueRoleDisplay"; +import type { LeagueMemberViewData } from "../view-data/LeagueMemberViewData"; export class LeagueMemberViewModel extends ViewModel { - driverId: string; + private readonly data: LeagueMemberViewData; - currentUserId: string; - - constructor(dto: LeagueMemberDTO, currentUserId: string) { - this.driverId = dto.driverId; - this.currentUserId = currentUserId; + constructor(data: LeagueMemberViewData) { + super(); + this.data = data; } - // Note: The generated DTO is incomplete - // These fields will need to be added when the OpenAPI spec is updated - driver?: DriverViewModel; - role: string = 'member'; - joinedAt: string = new Date().toISOString(); + get driverId(): string { return this.data.driverId; } + get driver(): any { return this.data.driver; } + get role(): string { return this.data.role; } + get joinedAt(): string { return this.data.joinedAt; } /** UI-specific: Formatted join date */ get formattedJoinedAt(): string { + // Client-only formatting return new Date(this.joinedAt).toLocaleDateString(); } - /** UI-specific: Badge variant for role */ - get roleBadgeVariant(): string { - switch (this.role) { - case 'owner': return 'primary'; - case 'admin': return 'secondary'; - default: return 'default'; - } + /** UI-specific: Badge classes for role */ + get roleBadgeClasses(): string { + return LeagueRoleDisplay.getLeagueRoleDisplay(this.role as LeagueRole)?.badgeClasses || ''; } /** UI-specific: Whether this member is the owner */ @@ -40,6 +33,6 @@ export class LeagueMemberViewModel extends ViewModel { /** UI-specific: Whether this is the current user */ get isCurrentUser(): boolean { - return this.driverId === this.currentUserId; + return this.driverId === this.data.currentUserId; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueMembershipsViewModel.ts b/apps/website/lib/view-models/LeagueMembershipsViewModel.ts index 53d6c858b..c20daf163 100644 --- a/apps/website/lib/view-models/LeagueMembershipsViewModel.ts +++ b/apps/website/lib/view-models/LeagueMembershipsViewModel.ts @@ -1,19 +1,15 @@ import { LeagueMemberViewModel } from './LeagueMemberViewModel'; -import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO'; - -/** - * View Model for League Memberships - * - * Represents the league's memberships in a UI-ready format. - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueMembershipsViewData } from "../view-data/LeagueMembershipsViewData"; export class LeagueMembershipsViewModel extends ViewModel { - memberships: LeagueMemberViewModel[]; + private readonly data: LeagueMembershipsViewData; + readonly memberships: LeagueMemberViewModel[]; - constructor(dto: { members?: LeagueMemberDTO[]; memberships?: LeagueMemberDTO[] }, currentUserId: string) { - const memberships = dto.members ?? dto.memberships ?? []; - this.memberships = memberships.map((membership) => new LeagueMemberViewModel(membership, currentUserId)); + constructor(data: LeagueMembershipsViewData) { + super(); + this.data = data; + this.memberships = data.memberships.map((m) => new LeagueMemberViewModel(m)); } /** UI-specific: Number of members */ diff --git a/apps/website/lib/view-models/LeaguePageDetailViewModel.ts b/apps/website/lib/view-models/LeaguePageDetailViewModel.ts index 2493d82b6..bf5f72cff 100644 --- a/apps/website/lib/view-models/LeaguePageDetailViewModel.ts +++ b/apps/website/lib/view-models/LeaguePageDetailViewModel.ts @@ -4,6 +4,7 @@ * View model for league page details. */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeaguePageDetailViewData } from "../view-data/LeaguePageDetailViewData"; export class LeaguePageDetailViewModel extends ViewModel { id: string; @@ -14,7 +15,8 @@ export class LeaguePageDetailViewModel extends ViewModel { isAdmin: boolean; mainSponsor: { name: string; logoUrl: string; websiteUrl: string } | null; - constructor(data: any) { + constructor(data: LeaguePageDetailViewData) { + super(); this.id = data.id; this.name = data.name; this.description = data.description; @@ -23,4 +25,4 @@ export class LeaguePageDetailViewModel extends ViewModel { this.isAdmin = data.isAdmin; this.mainSponsor = data.mainSponsor; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/LeagueScheduleRaceViewModel.ts b/apps/website/lib/view-models/LeagueScheduleRaceViewModel.ts new file mode 100644 index 000000000..ee84e6f0b --- /dev/null +++ b/apps/website/lib/view-models/LeagueScheduleRaceViewModel.ts @@ -0,0 +1,16 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface LeagueScheduleRaceViewModel extends ViewModel { + id: string; + name: string; + scheduledAt: Date; + formattedDate: string; + formattedTime: string; + isPast: boolean; + isUpcoming: boolean; + status: string; + track?: string; + car?: string; + sessionType?: string; + isRegistered?: boolean; +} diff --git a/apps/website/lib/view-models/LeagueScheduleViewModel.ts b/apps/website/lib/view-models/LeagueScheduleViewModel.ts index dc1f6ef18..a06dbda6c 100644 --- a/apps/website/lib/view-models/LeagueScheduleViewModel.ts +++ b/apps/website/lib/view-models/LeagueScheduleViewModel.ts @@ -1,32 +1,15 @@ -/** - * View Model for League Schedule - * - * Service layer maps DTOs into these shapes; UI consumes ViewModels only. - */ -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export interface LeagueScheduleRaceViewModel extends ViewModel { - id: string; - name: string; - scheduledAt: Date; - formattedDate: string; - formattedTime: string; - isPast: boolean; - isUpcoming: boolean; - status: string; - track?: string; - car?: string; - sessionType?: string; - isRegistered?: boolean; -} - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueScheduleRaceViewModel } from "./LeagueScheduleRaceViewModel"; +import type { LeagueScheduleViewData } from "../view-data/LeagueScheduleViewData"; export class LeagueScheduleViewModel extends ViewModel { + private readonly data: LeagueScheduleViewData; readonly races: LeagueScheduleRaceViewModel[]; - constructor(races: LeagueScheduleRaceViewModel[]) { - this.races = races; + constructor(data: LeagueScheduleViewData) { + super(); + this.data = data; + this.races = data.races; } get raceCount(): number { @@ -36,4 +19,4 @@ export class LeagueScheduleViewModel extends ViewModel { get hasRaces(): boolean { return this.raceCount > 0; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/LeagueScoringChampionshipViewModel.ts b/apps/website/lib/view-models/LeagueScoringChampionshipViewModel.ts index fc7b63c65..a88c6997e 100644 --- a/apps/website/lib/view-models/LeagueScoringChampionshipViewModel.ts +++ b/apps/website/lib/view-models/LeagueScoringChampionshipViewModel.ts @@ -1,19 +1,5 @@ -export type LeagueScoringChampionshipViewModelInput = { - id: string; - name: string; - type: string; - sessionTypes: string[]; - pointsPreview?: Array<{ sessionType: string; position: number; points: number }> | null; - bonusSummary?: string[] | null; - dropPolicyDescription?: string; -}; - -/** - * LeagueScoringChampionshipViewModel - * - * View model for league scoring championship - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueScoringChampionshipViewData } from "../view-data/LeagueScoringChampionshipViewData"; export class LeagueScoringChampionshipViewModel extends ViewModel { readonly id: string; @@ -24,13 +10,14 @@ export class LeagueScoringChampionshipViewModel extends ViewModel { readonly bonusSummary: string[]; readonly dropPolicyDescription?: string; - constructor(input: LeagueScoringChampionshipViewModelInput) { - this.id = input.id; - this.name = input.name; - this.type = input.type; - this.sessionTypes = input.sessionTypes; - this.pointsPreview = input.pointsPreview ?? []; - this.bonusSummary = input.bonusSummary ?? []; - this.dropPolicyDescription = input.dropPolicyDescription; + constructor(data: LeagueScoringChampionshipViewData) { + super(); + this.id = data.id; + this.name = data.name; + this.type = data.type; + this.sessionTypes = data.sessionTypes; + this.pointsPreview = data.pointsPreview ?? []; + this.bonusSummary = data.bonusSummary ?? []; + this.dropPolicyDescription = data.dropPolicyDescription; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/LeagueScoringConfigViewModel.ts b/apps/website/lib/view-models/LeagueScoringConfigViewModel.ts index 98254cb97..dc4ef8875 100644 --- a/apps/website/lib/view-models/LeagueScoringConfigViewModel.ts +++ b/apps/website/lib/view-models/LeagueScoringConfigViewModel.ts @@ -1,23 +1,16 @@ -import { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO'; -import type { LeagueScoringChampionshipDTO } from '@/lib/types/generated/LeagueScoringChampionshipDTO'; - -/** - * LeagueScoringConfigViewModel - * - * View model for league scoring configuration - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueScoringConfigViewData } from "../view-data/LeagueScoringConfigViewData"; export class LeagueScoringConfigViewModel extends ViewModel { - readonly gameName: string; - readonly scoringPresetName?: string; - readonly dropPolicySummary?: string; - readonly championships?: LeagueScoringChampionshipDTO[]; + private readonly data: LeagueScoringConfigViewData; - constructor(dto: LeagueScoringConfigDTO) { - this.gameName = dto.gameName; - this.scoringPresetName = dto.scoringPresetName; - this.dropPolicySummary = dto.dropPolicySummary; - this.championships = dto.championships; + constructor(data: LeagueScoringConfigViewData) { + super(); + this.data = data; } + + get gameName(): string { return this.data.gameName; } + get scoringPresetName(): string | undefined { return this.data.scoringPresetName; } + get dropPolicySummary(): string | undefined { return this.data.dropPolicySummary; } + get championships(): any[] | undefined { return this.data.championships; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueScoringPresetViewModel.ts b/apps/website/lib/view-models/LeagueScoringPresetViewModel.ts index 0f62f08cd..9e762982a 100644 --- a/apps/website/lib/view-models/LeagueScoringPresetViewModel.ts +++ b/apps/website/lib/view-models/LeagueScoringPresetViewModel.ts @@ -1,40 +1,19 @@ import { ViewModel } from "../contracts/view-models/ViewModel"; - -export type LeagueScoringPresetTimingDefaultsViewModel = ViewModel & { - practiceMinutes: number; - qualifyingMinutes: number; - sprintRaceMinutes: number; - mainRaceMinutes: number; - sessionCount: number; -}; - -export type LeagueScoringPresetViewModelInput = { - id: string; - name: string; - sessionSummary: string; - bonusSummary?: string; - defaultTimings: LeagueScoringPresetTimingDefaultsViewModel; -}; - -/** - * LeagueScoringPresetViewModel - * - * View model for league scoring preset configuration - */ -import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueScoringPresetViewData } from "../view-data/LeagueScoringPresetViewData"; export class LeagueScoringPresetViewModel extends ViewModel { readonly id: string; readonly name: string; readonly sessionSummary: string; readonly bonusSummary?: string; - readonly defaultTimings: LeagueScoringPresetTimingDefaultsViewModel; + readonly defaultTimings: LeagueScoringPresetViewData['defaultTimings']; - constructor(input: LeagueScoringPresetViewModelInput) { - this.id = input.id; - this.name = input.name; - this.sessionSummary = input.sessionSummary; - this.bonusSummary = input.bonusSummary; - this.defaultTimings = input.defaultTimings; + constructor(data: LeagueScoringPresetViewData) { + super(); + this.id = data.id; + this.name = data.name; + this.sessionSummary = data.sessionSummary; + this.bonusSummary = data.bonusSummary; + this.defaultTimings = data.defaultTimings; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/LeagueScoringPresetsViewModel.ts b/apps/website/lib/view-models/LeagueScoringPresetsViewModel.ts index b264e694b..0c69cb794 100644 --- a/apps/website/lib/view-models/LeagueScoringPresetsViewModel.ts +++ b/apps/website/lib/view-models/LeagueScoringPresetsViewModel.ts @@ -1,20 +1,16 @@ -import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO'; - /** * View Model for league scoring presets - * Transform from DTO to ViewModel with UI fields */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueScoringPresetsViewData } from "../view-data/LeagueScoringPresetsViewData"; export class LeagueScoringPresetsViewModel extends ViewModel { - presets: LeagueScoringPresetDTO[]; + presets: any[]; totalCount: number; - constructor(dto: { - presets: LeagueScoringPresetDTO[]; - totalCount?: number; - }) { - this.presets = dto.presets; - this.totalCount = dto.totalCount ?? dto.presets.length; + constructor(data: LeagueScoringPresetsViewData) { + super(); + this.presets = data.presets; + this.totalCount = data.totalCount ?? data.presets.length; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/LeagueScoringSectionViewModel.ts b/apps/website/lib/view-models/LeagueScoringSectionViewModel.ts index ed9bce71b..a611f37fe 100644 --- a/apps/website/lib/view-models/LeagueScoringSectionViewModel.ts +++ b/apps/website/lib/view-models/LeagueScoringSectionViewModel.ts @@ -1,16 +1,9 @@ -import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; -import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel'; -import type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationViewModel'; - -/** - * LeagueScoringSectionViewModel - * - * View model for the league scoring section UI state and operations - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import { LeagueScoringPresetViewModel } from './LeagueScoringPresetViewModel'; +import type { CustomPointsConfig } from "../view-data/ScoringConfigurationViewData"; +import type { LeagueScoringSectionViewData } from "../view-data/LeagueScoringSectionViewData"; export class LeagueScoringSectionViewModel extends ViewModel { - readonly form: LeagueConfigFormModel; readonly presets: LeagueScoringPresetViewModel[]; readonly readOnly: boolean; readonly patternOnly: boolean; @@ -18,28 +11,24 @@ export class LeagueScoringSectionViewModel extends ViewModel { readonly disabled: boolean; readonly currentPreset: LeagueScoringPresetViewModel | null; readonly isCustom: boolean; + private readonly data: LeagueScoringSectionViewData; - constructor( - form: LeagueConfigFormModel, - presets: LeagueScoringPresetViewModel[], - options: { - readOnly?: boolean; - patternOnly?: boolean; - championshipsOnly?: boolean; - } = {} - ) { - this.form = form; - this.presets = presets; - this.readOnly = options.readOnly || false; - this.patternOnly = options.patternOnly || false; - this.championshipsOnly = options.championshipsOnly || false; + constructor(data: LeagueScoringSectionViewData) { + super(); + this.data = data; + this.presets = data.presets.map(p => new LeagueScoringPresetViewModel(p)); + this.readOnly = data.options?.readOnly || false; + this.patternOnly = data.options?.patternOnly || false; + this.championshipsOnly = data.options?.championshipsOnly || false; this.disabled = this.readOnly; - this.currentPreset = form.scoring.patternId - ? presets.find(p => p.id === form.scoring.patternId) || null + this.currentPreset = data.form.scoring.patternId + ? this.presets.find(p => p.id === data.form.scoring.patternId) || null : null; - this.isCustom = form.scoring.customScoringEnabled || false; + this.isCustom = data.form.scoring.customScoringEnabled || false; } + get form() { return this.data.form; } + /** * Get default custom points configuration */ @@ -120,8 +109,6 @@ export class LeagueScoringSectionViewModel extends ViewModel { * Get the active custom points configuration */ getActiveCustomPoints(): CustomPointsConfig { - // This would be stored separately in the form model - // For now, return defaults return LeagueScoringSectionViewModel.getDefaultCustomPoints(); } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/LeagueSeasonSummaryViewModel.ts b/apps/website/lib/view-models/LeagueSeasonSummaryViewModel.ts index 01c3d34d2..115722b76 100644 --- a/apps/website/lib/view-models/LeagueSeasonSummaryViewModel.ts +++ b/apps/website/lib/view-models/LeagueSeasonSummaryViewModel.ts @@ -9,17 +9,16 @@ export type LeagueSeasonSummaryViewModelInput = { import { ViewModel } from "../contracts/view-models/ViewModel"; export class LeagueSeasonSummaryViewModel extends ViewModel { - readonly seasonId: string; - readonly name: string; - readonly status: string; - readonly isPrimary: boolean; - readonly isParallelActive: boolean; + private readonly data: LeagueSeasonSummaryViewModelInput; - constructor(input: LeagueSeasonSummaryViewModelInput) { - this.seasonId = input.seasonId; - this.name = input.name; - this.status = input.status; - this.isPrimary = input.isPrimary; - this.isParallelActive = input.isParallelActive; + constructor(data: LeagueSeasonSummaryViewModelInput) { + super(); + this.data = data; } + + get seasonId(): string { return this.data.seasonId; } + get name(): string { return this.data.name; } + get status(): string { return this.data.status; } + get isPrimary(): boolean { return this.data.isPrimary; } + get isParallelActive(): boolean { return this.data.isParallelActive; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueSettingsViewModel.ts b/apps/website/lib/view-models/LeagueSettingsViewModel.ts index 28c795b1a..83dd7738e 100644 --- a/apps/website/lib/view-models/LeagueSettingsViewModel.ts +++ b/apps/website/lib/view-models/LeagueSettingsViewModel.ts @@ -1,40 +1,20 @@ -import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; -import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO'; import { DriverSummaryViewModel } from './DriverSummaryViewModel'; - -/** - * View Model for league settings page - * Combines league config, presets, owner, and members - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueSettingsViewData } from "../view-data/LeagueSettingsViewData"; export class LeagueSettingsViewModel extends ViewModel { - league: { - id: string; - name: string; - ownerId: string; - }; - config: LeagueConfigFormModel; - presets: LeagueScoringPresetDTO[]; - owner: DriverSummaryViewModel | null; - members: DriverSummaryViewModel[]; + private readonly data: LeagueSettingsViewData; + readonly owner: DriverSummaryViewModel | null; + readonly members: DriverSummaryViewModel[]; - constructor(dto: { - league: { - id: string; - name: string; - ownerId: string; - createdAt: string; - }; - config: LeagueConfigFormModel; - presets: LeagueScoringPresetDTO[]; - owner: DriverSummaryViewModel | null; - members: DriverSummaryViewModel[]; - }) { - this.league = dto.league; - this.config = dto.config; - this.presets = dto.presets; - this.owner = dto.owner; - this.members = dto.members; + constructor(data: LeagueSettingsViewData) { + super(); + this.data = data; + this.owner = data.owner ? new DriverSummaryViewModel(data.owner) : null; + this.members = data.members.map(m => new DriverSummaryViewModel(m)); } + + get league() { return this.data.league; } + get config() { return this.data.config; } + get presets() { return this.data.presets; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueStandingsViewModel.ts b/apps/website/lib/view-models/LeagueStandingsViewModel.ts index 940ad5b87..6cb835992 100644 --- a/apps/website/lib/view-models/LeagueStandingsViewModel.ts +++ b/apps/website/lib/view-models/LeagueStandingsViewModel.ts @@ -1,23 +1,17 @@ -import { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO'; import { StandingEntryViewModel } from './StandingEntryViewModel'; -import { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO'; -import { LeagueMembership } from '@/lib/types/LeagueMembership'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueStandingsViewData } from "../view-data/LeagueStandingsViewData"; export class LeagueStandingsViewModel extends ViewModel { - standings: StandingEntryViewModel[]; - drivers: GetDriverOutputDTO[]; - memberships: LeagueMembership[]; + private readonly data: LeagueStandingsViewData; + readonly standings: StandingEntryViewModel[]; - constructor(dto: { standings: LeagueStandingDTO[]; drivers: GetDriverOutputDTO[]; memberships: LeagueMembership[] }, currentUserId: string, previousStandings?: LeagueStandingDTO[]) { - const leaderPoints = dto.standings[0]?.points || 0; - this.standings = dto.standings.map((entry, index) => { - const nextPoints = dto.standings[index + 1]?.points || entry.points; - const previousPosition = previousStandings?.find(p => p.driverId === entry.driverId)?.position; - return new StandingEntryViewModel(entry, leaderPoints, nextPoints, currentUserId, previousPosition); - }); - this.drivers = dto.drivers; - this.memberships = dto.memberships; + constructor(data: LeagueStandingsViewData) { + super(); + this.data = data; + this.standings = data.standings.map(s => new StandingEntryViewModel(s)); } + + get drivers(): any[] { return this.data.drivers; } + get memberships(): any[] { return this.data.memberships; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueStatsViewModel.ts b/apps/website/lib/view-models/LeagueStatsViewModel.ts index 441714a5f..ddfd0ab04 100644 --- a/apps/website/lib/view-models/LeagueStatsViewModel.ts +++ b/apps/website/lib/view-models/LeagueStatsViewModel.ts @@ -4,16 +4,21 @@ * Represents the total number of leagues in a UI-ready format. */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueStatsViewData } from "../view-data/LeagueStatsViewData"; export class LeagueStatsViewModel extends ViewModel { - totalLeagues: number; + private readonly data: LeagueStatsViewData; - constructor(dto: { totalLeagues: number }) { - this.totalLeagues = dto.totalLeagues; + constructor(data: LeagueStatsViewData) { + super(); + this.data = data; } + get totalLeagues(): number { return this.data.totalLeagues; } + /** UI-specific: Formatted total leagues display */ get formattedTotalLeagues(): string { + // Client-only formatting return this.totalLeagues.toLocaleString(); } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueStewardingViewModel.ts b/apps/website/lib/view-models/LeagueStewardingViewModel.ts index 9d17c63bb..58489ffc7 100644 --- a/apps/website/lib/view-models/LeagueStewardingViewModel.ts +++ b/apps/website/lib/view-models/LeagueStewardingViewModel.ts @@ -4,14 +4,26 @@ */ import { ViewModel } from "../contracts/view-models/ViewModel"; +/** + * ViewData for LeagueStewarding + * This is the JSON-serializable input for the Template. + */ +export interface LeagueStewardingViewData { + racesWithData: RaceWithProtests[]; + driverMap: Record; +} + export class LeagueStewardingViewModel extends ViewModel { - constructor( - public readonly racesWithData: RaceWithProtests[], - public readonly driverMap: Record - ) { + private readonly data: LeagueStewardingViewData; + + constructor(data: LeagueStewardingViewData) { super(); + this.data = data; } + get racesWithData(): RaceWithProtests[] { return this.data.racesWithData; } + get driverMap() { return this.data.driverMap; } + /** UI-specific: Total pending protests count */ get totalPending(): number { return this.racesWithData.reduce((sum, r) => sum + r.pendingProtests.length, 0); diff --git a/apps/website/lib/view-models/LeagueSummaryViewModel.ts b/apps/website/lib/view-models/LeagueSummaryViewModel.ts index 274668553..8b5375753 100644 --- a/apps/website/lib/view-models/LeagueSummaryViewModel.ts +++ b/apps/website/lib/view-models/LeagueSummaryViewModel.ts @@ -1,29 +1,29 @@ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { LeagueSummaryViewData } from "../view-data/LeagueSummaryViewData"; -export interface LeagueSummaryViewModel extends ViewModel { - 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; - }; +export class LeagueSummaryViewModel extends ViewModel { + private readonly data: LeagueSummaryViewData; + + constructor(data: LeagueSummaryViewData) { + super(); + this.data = data; + } + + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get description(): string | null { return this.data.description; } + get logoUrl(): string | null { return this.data.logoUrl; } + get ownerId(): string { return this.data.ownerId; } + get createdAt(): string { return this.data.createdAt; } + get maxDrivers(): number { return this.data.maxDrivers; } + get usedDriverSlots(): number { return this.data.usedDriverSlots; } + get activeDriversCount(): number | undefined { return this.data.activeDriversCount; } + get nextRaceAt(): string | undefined { return this.data.nextRaceAt; } + get maxTeams(): number | undefined { return this.data.maxTeams; } + get usedTeamSlots(): number | undefined { return this.data.usedTeamSlots; } + get structureSummary(): string { return this.data.structureSummary; } + get scoringPatternSummary(): string | undefined { return this.data.scoringPatternSummary; } + get timingSummary(): string { return this.data.timingSummary; } + get category(): string | null | undefined { return this.data.category; } + get scoring() { return this.data.scoring; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueViewModel.ts b/apps/website/lib/view-models/LeagueViewModel.ts new file mode 100644 index 000000000..209d72c51 --- /dev/null +++ b/apps/website/lib/view-models/LeagueViewModel.ts @@ -0,0 +1,63 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import { LeagueTierDisplay } from "../display-objects/LeagueTierDisplay"; +import type { LeagueViewData } from "../view-data/LeagueDetailViewData"; + +export class LeagueViewModel extends ViewModel { + private readonly data: LeagueViewData; + + constructor(data: LeagueViewData) { + super(); + this.data = data; + } + + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get game(): string { return this.data.game; } + get tier(): 'premium' | 'standard' | 'starter' { return this.data.tier; } + get season(): string { return this.data.season; } + get description(): string { return this.data.description; } + get drivers(): number { return this.data.drivers; } + get races(): number { return this.data.races; } + get completedRaces(): number { return this.data.completedRaces; } + get totalImpressions(): number { return this.data.totalImpressions; } + get avgViewsPerRace(): number { return this.data.avgViewsPerRace; } + get engagement(): number { return this.data.engagement; } + get rating(): number { return this.data.rating; } + get seasonStatus(): 'active' | 'upcoming' | 'completed' { return this.data.seasonStatus; } + get seasonDates() { return this.data.seasonDates; } + get nextRace() { return this.data.nextRace; } + get sponsorSlots() { return this.data.sponsorSlots; } + + get formattedTotalImpressions(): string { + return this.totalImpressions.toLocaleString(); + } + + get formattedAvgViewsPerRace(): string { + return this.avgViewsPerRace.toLocaleString(); + } + + get projectedTotalViews(): number { + return Math.round(this.avgViewsPerRace * this.races); + } + + get formattedProjectedTotal(): string { + return this.projectedTotalViews.toLocaleString(); + } + + get mainSponsorCpm(): number { + return Math.round((this.sponsorSlots.main.price / this.projectedTotalViews) * 1000); + } + + get formattedMainSponsorCpm(): string { + return CurrencyDisplay.format(this.mainSponsorCpm); + } + + get racesLeft(): number { + return this.races - this.completedRaces; + } + + get tierConfig() { + return LeagueTierDisplay.getDisplay(this.tier); + } +} diff --git a/apps/website/lib/view-models/LeagueWalletViewModel.ts b/apps/website/lib/view-models/LeagueWalletViewModel.ts index 4bd8508cf..203625486 100644 --- a/apps/website/lib/view-models/LeagueWalletViewModel.ts +++ b/apps/website/lib/view-models/LeagueWalletViewModel.ts @@ -1,63 +1,49 @@ import { WalletTransactionViewModel } from './WalletTransactionViewModel'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import type { LeagueWalletViewData } from "../view-data/LeagueWalletViewData"; export class LeagueWalletViewModel extends ViewModel { - balance: number; - currency: string; - totalRevenue: number; - totalFees: number; - totalWithdrawals: number; - pendingPayouts: number; - transactions: WalletTransactionViewModel[]; - canWithdraw: boolean; - withdrawalBlockReason?: string; + private readonly data: LeagueWalletViewData; + readonly transactions: WalletTransactionViewModel[]; - constructor(dto: { - balance: number; - currency: string; - totalRevenue: number; - totalFees: number; - totalWithdrawals: number; - pendingPayouts: number; - transactions: WalletTransactionViewModel[]; - canWithdraw: boolean; - withdrawalBlockReason?: string; - }) { + constructor(data: LeagueWalletViewData) { super(); - this.balance = dto.balance; - this.currency = dto.currency; - this.totalRevenue = dto.totalRevenue; - this.totalFees = dto.totalFees; - this.totalWithdrawals = dto.totalWithdrawals; - this.pendingPayouts = dto.pendingPayouts; - this.transactions = dto.transactions; - this.canWithdraw = dto.canWithdraw; - this.withdrawalBlockReason = dto.withdrawalBlockReason; + this.data = data; + this.transactions = data.transactions.map(t => new WalletTransactionViewModel(t)); } + get balance(): number { return this.data.balance; } + get currency(): string { return this.data.currency; } + get totalRevenue(): number { return this.data.totalRevenue; } + get totalFees(): number { return this.data.totalFees; } + get totalWithdrawals(): number { return this.data.totalWithdrawals; } + get pendingPayouts(): number { return this.data.pendingPayouts; } + get canWithdraw(): boolean { return this.data.canWithdraw; } + get withdrawalBlockReason(): string | undefined { return this.data.withdrawalBlockReason; } + /** UI-specific: Formatted balance */ get formattedBalance(): string { - return `$${this.balance.toFixed(2)}`; + return CurrencyDisplay.format(this.balance, this.currency); } /** UI-specific: Formatted total revenue */ get formattedTotalRevenue(): string { - return `$${this.totalRevenue.toFixed(2)}`; + return CurrencyDisplay.format(this.totalRevenue, this.currency); } /** UI-specific: Formatted total fees */ get formattedTotalFees(): string { - return `$${this.totalFees.toFixed(2)}`; + return CurrencyDisplay.format(this.totalFees, this.currency); } /** UI-specific: Formatted pending payouts */ get formattedPendingPayouts(): string { - return `$${this.pendingPayouts.toFixed(2)}`; + return CurrencyDisplay.format(this.pendingPayouts, this.currency); } /** UI-specific: Filtered transactions by type */ getFilteredTransactions(type: 'all' | 'sponsorship' | 'membership' | 'withdrawal' | 'prize'): WalletTransactionViewModel[] { return type === 'all' ? this.transactions : this.transactions.filter(t => t.type === type); } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/MediaViewModel.ts b/apps/website/lib/view-models/MediaViewModel.ts index 1d131e607..466fd85cb 100644 --- a/apps/website/lib/view-models/MediaViewModel.ts +++ b/apps/website/lib/view-models/MediaViewModel.ts @@ -1,8 +1,5 @@ -import type { MediaViewData } from '@/lib/view-data/MediaViewData'; - import { ViewModel } from "../contracts/view-models/ViewModel"; - -type MediaAssetViewData = MediaViewData['assets'][number]; +import type { MediaAssetViewData } from "../view-data/MediaViewData"; /** * Media View Model @@ -11,23 +8,20 @@ type MediaAssetViewData = MediaViewData['assets'][number]; * Represents a single media asset card in the UI. */ export class MediaViewModel extends ViewModel { - id: string; - src: string; - title: string; - category: string; - date?: string; - dimensions?: string; + private readonly data: MediaAssetViewData; - constructor(viewData: MediaAssetViewData) { + constructor(data: MediaAssetViewData) { super(); - this.id = viewData.id; - this.src = viewData.src; - this.title = viewData.title; - this.category = viewData.category; - if (viewData.date !== undefined) this.date = viewData.date; - if (viewData.dimensions !== undefined) this.dimensions = viewData.dimensions; + this.data = data; } + get id(): string { return this.data.id; } + get src(): string { return this.data.src; } + get title(): string { return this.data.title; } + get category(): string { return this.data.category; } + get date(): string | undefined { return this.data.date; } + get dimensions(): string | undefined { return this.data.dimensions; } + /** UI-specific: Combined subtitle used by MediaCard */ get subtitle(): string { return `${this.category}${this.dimensions ? ` • ${this.dimensions}` : ''}`; diff --git a/apps/website/lib/view-models/MembershipFeeViewModel.ts b/apps/website/lib/view-models/MembershipFeeViewModel.ts index 77ae8466e..498f29e55 100644 --- a/apps/website/lib/view-models/MembershipFeeViewModel.ts +++ b/apps/website/lib/view-models/MembershipFeeViewModel.ts @@ -1,34 +1,34 @@ -import type { MembershipFeeDTO } from '@/lib/types/generated'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import { MembershipFeeTypeDisplay } from "../display-objects/MembershipFeeTypeDisplay"; +import type { MembershipFeeViewData } from "../view-data/MembershipFeeViewData"; export class MembershipFeeViewModel extends ViewModel { - id!: string; - leagueId!: string; - seasonId?: string; - type!: string; - amount!: number; - enabled!: boolean; - createdAt!: Date; - updatedAt!: Date; + private readonly data: MembershipFeeViewData; - constructor(dto: MembershipFeeDTO) { - Object.assign(this, dto); + constructor(data: MembershipFeeViewData) { + super(); + this.data = data; } + get id(): string { return this.data.id; } + get leagueId(): string { return this.data.leagueId; } + get seasonId(): string | undefined { return this.data.seasonId; } + get type(): string { return this.data.type; } + get amount(): number { return this.data.amount; } + get enabled(): boolean { return this.data.enabled; } + get createdAt(): string { return this.data.createdAt; } + get updatedAt(): string { return this.data.updatedAt; } + /** UI-specific: Formatted amount */ get formattedAmount(): string { - return `€${this.amount.toFixed(2)}`; // Assuming EUR + return CurrencyDisplay.format(this.amount, 'EUR'); } /** UI-specific: Type display */ get typeDisplay(): string { - switch (this.type) { - case 'season': return 'Per Season'; - case 'monthly': return 'Monthly'; - case 'per_race': return 'Per Race'; - default: return this.type; - } + return MembershipFeeTypeDisplay.format(this.type); } /** UI-specific: Status display */ @@ -43,11 +43,11 @@ export class MembershipFeeViewModel extends ViewModel { /** UI-specific: Formatted created date */ get formattedCreatedAt(): string { - return this.createdAt.toLocaleString(); + return DateDisplay.formatShort(this.createdAt); } /** UI-specific: Formatted updated date */ get formattedUpdatedAt(): string { - return this.updatedAt.toLocaleString(); + return DateDisplay.formatShort(this.updatedAt); } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/NotificationSettingsViewModel.ts b/apps/website/lib/view-models/NotificationSettingsViewModel.ts new file mode 100644 index 000000000..bfb0056d4 --- /dev/null +++ b/apps/website/lib/view-models/NotificationSettingsViewModel.ts @@ -0,0 +1,21 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { NotificationSettingsViewData } from "../view-data/NotificationSettingsViewData"; + +export class NotificationSettingsViewModel extends ViewModel { + emailNewSponsorships: boolean; + emailWeeklyReport: boolean; + emailRaceAlerts: boolean; + emailPaymentAlerts: boolean; + emailNewOpportunities: boolean; + emailContractExpiry: boolean; + + constructor(data: NotificationSettingsViewData) { + super(); + this.emailNewSponsorships = data.emailNewSponsorships; + this.emailWeeklyReport = data.emailWeeklyReport; + this.emailRaceAlerts = data.emailRaceAlerts; + this.emailPaymentAlerts = data.emailPaymentAlerts; + this.emailNewOpportunities = data.emailNewOpportunities; + this.emailContractExpiry = data.emailContractExpiry; + } +} diff --git a/apps/website/lib/view-models/OnboardingViewModel.ts b/apps/website/lib/view-models/OnboardingViewModel.ts index ad4aa9a6b..5079d36d9 100644 --- a/apps/website/lib/view-models/OnboardingViewModel.ts +++ b/apps/website/lib/view-models/OnboardingViewModel.ts @@ -1,5 +1,22 @@ import { ViewModel } from "../contracts/view-models/ViewModel"; -export interface OnboardingViewModel extends ViewModel { +/** + * ViewData for Onboarding + * This is the JSON-serializable input for the Template. + */ +export interface OnboardingViewData { isAlreadyOnboarded: boolean; +} + +export class OnboardingViewModel extends ViewModel { + private readonly data: OnboardingViewData; + + constructor(data: OnboardingViewData) { + super(); + this.data = data; + } + + get isAlreadyOnboarded(): boolean { + return this.data.isAlreadyOnboarded; + } } \ No newline at end of file diff --git a/apps/website/lib/view-models/PaymentMethodViewModel.ts b/apps/website/lib/view-models/PaymentMethodViewModel.ts new file mode 100644 index 000000000..c9a6945de --- /dev/null +++ b/apps/website/lib/view-models/PaymentMethodViewModel.ts @@ -0,0 +1,22 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { PaymentMethodViewData } from "@/lib/view-data/BillingViewData"; + +export class PaymentMethodViewModel extends ViewModel { + private readonly data: PaymentMethodViewData; + + constructor(data: PaymentMethodViewData) { + super(); + this.data = data; + } + + get id(): string { return this.data.id; } + get type(): string { return this.data.type; } + get last4(): string { return this.data.last4; } + get brand(): string | undefined { return this.data.brand; } + get isDefault(): boolean { return this.data.isDefault; } + get expiryMonth(): number | undefined { return this.data.expiryMonth; } + get expiryYear(): number | undefined { return this.data.expiryYear; } + get bankName(): string | undefined { return this.data.bankName; } + get displayLabel(): string { return this.data.displayLabel; } + get expiryDisplay(): string | null { return this.data.expiryDisplay; } +} diff --git a/apps/website/lib/view-models/PaymentViewModel.ts b/apps/website/lib/view-models/PaymentViewModel.ts index bd41adc1a..7bd2a7222 100644 --- a/apps/website/lib/view-models/PaymentViewModel.ts +++ b/apps/website/lib/view-models/PaymentViewModel.ts @@ -1,33 +1,40 @@ -import type { PaymentDTO } from '@/lib/types/generated/PaymentDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import { PaymentTypeDisplay } from "../display-objects/PaymentTypeDisplay"; +import { PayerTypeDisplay } from "../display-objects/PayerTypeDisplay"; +import { StatusDisplay } from "../display-objects/StatusDisplay"; +import type { PaymentViewData } from "../view-data/PaymentViewData"; export class PaymentViewModel extends ViewModel { - id!: string; - type!: string; - amount!: number; - platformFee!: number; - netAmount!: number; - payerId!: string; - payerType!: string; - leagueId!: string; - seasonId?: string; - status!: string; - createdAt!: Date; - completedAt?: Date; + private readonly data: PaymentViewData; - constructor(dto: PaymentDTO) { - Object.assign(this, dto); + constructor(data: PaymentViewData) { + super(); + this.data = data; } + get id(): string { return this.data.id; } + get type(): string { return this.data.type; } + get amount(): number { return this.data.amount; } + get platformFee(): number { return this.data.platformFee; } + get netAmount(): number { return this.data.netAmount; } + get payerId(): string { return this.data.payerId; } + get payerType(): string { return this.data.payerType; } + get leagueId(): string { return this.data.leagueId; } + get seasonId(): string | undefined { return this.data.seasonId; } + get status(): string { return this.data.status; } + get createdAt(): string { return this.data.createdAt; } + get completedAt(): string | undefined { return this.data.completedAt; } + /** UI-specific: Formatted amount */ get formattedAmount(): string { - return `€${this.amount.toFixed(2)}`; // Assuming EUR currency + return CurrencyDisplay.format(this.amount, 'EUR'); } /** UI-specific: Formatted net amount */ get formattedNetAmount(): string { - return `€${this.netAmount.toFixed(2)}`; + return CurrencyDisplay.format(this.netAmount, 'EUR'); } /** UI-specific: Status color */ @@ -43,26 +50,26 @@ export class PaymentViewModel extends ViewModel { /** UI-specific: Formatted created date */ get formattedCreatedAt(): string { - return this.createdAt.toLocaleString(); + return DateDisplay.formatShort(this.createdAt); } /** UI-specific: Formatted completed date */ get formattedCompletedAt(): string { - return this.completedAt ? this.completedAt.toLocaleString() : 'Not completed'; + return this.completedAt ? DateDisplay.formatShort(this.completedAt) : 'Not completed'; } /** UI-specific: Status display */ get statusDisplay(): string { - return this.status.charAt(0).toUpperCase() + this.status.slice(1); + return StatusDisplay.transactionStatus(this.status); } /** UI-specific: Type display */ get typeDisplay(): string { - return this.type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()); + return PaymentTypeDisplay.format(this.type); } /** UI-specific: Payer type display */ get payerTypeDisplay(): string { - return this.payerType.charAt(0).toUpperCase() + this.payerType.slice(1); + return PayerTypeDisplay.format(this.payerType); } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/PrivacySettingsViewModel.ts b/apps/website/lib/view-models/PrivacySettingsViewModel.ts new file mode 100644 index 000000000..6a8458258 --- /dev/null +++ b/apps/website/lib/view-models/PrivacySettingsViewModel.ts @@ -0,0 +1,17 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { PrivacySettingsViewData } from "../view-data/PrivacySettingsViewData"; + +export class PrivacySettingsViewModel extends ViewModel { + publicProfile: boolean; + showStats: boolean; + showActiveSponsorships: boolean; + allowDirectContact: boolean; + + constructor(data: PrivacySettingsViewData) { + super(); + this.publicProfile = data.publicProfile; + this.showStats = data.showStats; + this.showActiveSponsorships = data.showActiveSponsorships; + this.allowDirectContact = data.allowDirectContact; + } +} diff --git a/apps/website/lib/view-models/PrizeViewModel.ts b/apps/website/lib/view-models/PrizeViewModel.ts index e05a068f3..73dfed8a7 100644 --- a/apps/website/lib/view-models/PrizeViewModel.ts +++ b/apps/website/lib/view-models/PrizeViewModel.ts @@ -1,59 +1,44 @@ -import type { PrizeDTO } from '@/lib/types/generated/PrizeDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import { FinishDisplay } from "../display-objects/FinishDisplay"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import { PrizeTypeDisplay } from "../display-objects/PrizeTypeDisplay"; +import type { PrizeViewData } from "../view-data/PrizeViewData"; export class PrizeViewModel extends ViewModel { - id!: string; - leagueId!: string; - seasonId!: string; - position!: number; - name!: string; - amount!: number; - type!: string; - description?: string; - awarded!: boolean; - awardedTo?: string; - awardedAt?: Date; - createdAt!: Date; + private readonly data: PrizeViewData; - constructor(dto: PrizeDTO) { - this.id = dto.id; - this.leagueId = dto.leagueId; - this.seasonId = dto.seasonId; - this.position = dto.position; - this.name = dto.name; - this.amount = dto.amount; - this.type = dto.type; - this.description = dto.description; - this.awarded = dto.awarded; - this.awardedTo = dto.awardedTo; - this.awardedAt = dto.awardedAt ? new Date(dto.awardedAt) : undefined; - this.createdAt = new Date(dto.createdAt); + constructor(data: PrizeViewData) { + super(); + this.data = data; } + get id(): string { return this.data.id; } + get leagueId(): string { return this.data.leagueId; } + get seasonId(): string { return this.data.seasonId; } + get position(): number { return this.data.position; } + get name(): string { return this.data.name; } + get amount(): number { return this.data.amount; } + get type(): string { return this.data.type; } + get description(): string | undefined { return this.data.description; } + get awarded(): boolean { return this.data.awarded; } + get awardedTo(): string | undefined { return this.data.awardedTo; } + get awardedAt(): string | undefined { return this.data.awardedAt; } + get createdAt(): string { return this.data.createdAt; } + /** UI-specific: Formatted amount */ get formattedAmount(): string { - return `€${this.amount.toFixed(2)}`; // Assuming EUR + return CurrencyDisplay.format(this.amount, 'EUR'); } /** UI-specific: Position display */ get positionDisplay(): string { - switch (this.position) { - case 1: return '1st Place'; - case 2: return '2nd Place'; - case 3: return '3rd Place'; - default: return `${this.position}th Place`; - } + return FinishDisplay.format(this.position); } /** UI-specific: Type display */ get typeDisplay(): string { - switch (this.type) { - case 'cash': return 'Cash Prize'; - case 'merchandise': return 'Merchandise'; - case 'other': return 'Other'; - default: return this.type; - } + return PrizeTypeDisplay.format(this.type); } /** UI-specific: Status display */ @@ -73,11 +58,11 @@ export class PrizeViewModel extends ViewModel { /** UI-specific: Formatted awarded date */ get formattedAwardedAt(): string { - return this.awardedAt ? this.awardedAt.toLocaleString() : 'Not awarded'; + return this.awardedAt ? DateDisplay.formatShort(this.awardedAt) : 'Not awarded'; } /** UI-specific: Formatted created date */ get formattedCreatedAt(): string { - return this.createdAt.toLocaleString(); + return DateDisplay.formatShort(this.createdAt); } } diff --git a/apps/website/lib/view-models/ProfileOverviewSubViewModels.ts b/apps/website/lib/view-models/ProfileOverviewSubViewModels.ts new file mode 100644 index 000000000..c0445fea4 --- /dev/null +++ b/apps/website/lib/view-models/ProfileOverviewSubViewModels.ts @@ -0,0 +1,93 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; + +export interface ProfileOverviewDriverSummaryViewModel extends ViewModel { + id: string; + name: string; + country: string; + avatarUrl: string; + iracingId: string | null; + joinedAt: string; + rating: number | null; + globalRank: number | null; + consistency: number | null; + bio: string | null; + totalDrivers: number | null; +} + +export interface ProfileOverviewStatsViewModel extends ViewModel { + totalRaces: number; + wins: number; + podiums: number; + dnfs: number; + avgFinish: number | null; + bestFinish: number | null; + worstFinish: number | null; + finishRate: number | null; + winRate: number | null; + podiumRate: number | null; + percentile: number | null; + rating: number | null; + consistency: number | null; + overallRank: number | null; +} + +export interface ProfileOverviewFinishDistributionViewModel extends ViewModel { + totalRaces: number; + wins: number; + podiums: number; + topTen: number; + dnfs: number; + other: number; +} + +export interface ProfileOverviewTeamMembershipViewModel extends ViewModel { + teamId: string; + teamName: string; + teamTag: string | null; + role: string; + joinedAt: string; + isCurrent: boolean; +} + +export interface ProfileOverviewSocialFriendSummaryViewModel extends ViewModel { + id: string; + name: string; + country: string; + avatarUrl: string; +} + +export interface ProfileOverviewSocialSummaryViewModel extends ViewModel { + friendsCount: number; + friends: ProfileOverviewSocialFriendSummaryViewModel[]; +} + +export type ProfileOverviewSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord'; + +export type ProfileOverviewAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary'; + +export interface ProfileOverviewAchievementViewModel extends ViewModel { + id: string; + title: string; + description: string; + icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap'; + rarity: ProfileOverviewAchievementRarity; + earnedAt: string; +} + +export interface ProfileOverviewSocialHandleViewModel extends ViewModel { + platform: ProfileOverviewSocialPlatform; + handle: string; + url: string; +} + +export interface ProfileOverviewExtendedProfileViewModel extends ViewModel { + socialHandles: ProfileOverviewSocialHandleViewModel[]; + achievements: ProfileOverviewAchievementViewModel[]; + racingStyle: string; + favoriteTrack: string; + favoriteCar: string; + timezone: string; + availableHours: string; + lookingForTeam: boolean; + openToRequests: boolean; +} diff --git a/apps/website/lib/view-models/ProfileOverviewViewModel.ts b/apps/website/lib/view-models/ProfileOverviewViewModel.ts index cea9155fd..a0c464e11 100644 --- a/apps/website/lib/view-models/ProfileOverviewViewModel.ts +++ b/apps/website/lib/view-models/ProfileOverviewViewModel.ts @@ -1,120 +1,26 @@ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { ProfileOverviewViewData } from "../view-data/ProfileOverviewViewData"; +import type { + ProfileOverviewDriverSummaryViewModel, + ProfileOverviewStatsViewModel, + ProfileOverviewFinishDistributionViewModel, + ProfileOverviewTeamMembershipViewModel, + ProfileOverviewSocialSummaryViewModel, + ProfileOverviewExtendedProfileViewModel +} from "./ProfileOverviewSubViewModels"; -export interface ProfileOverviewDriverSummaryViewModel extends ViewModel { - id: string; - name: string; - country: string; - avatarUrl: string; - iracingId: string | null; - joinedAt: string; - rating: number | null; - globalRank: number | null; - consistency: number | null; - bio: string | null; - totalDrivers: number | null; +export class ProfileOverviewViewModel extends ViewModel { + private readonly data: ProfileOverviewViewData; + + constructor(data: ProfileOverviewViewData) { + super(); + this.data = data; + } + + get currentDriver(): ProfileOverviewDriverSummaryViewModel | null { return this.data.currentDriver; } + get stats(): ProfileOverviewStatsViewModel | null { return this.data.stats; } + get finishDistribution(): ProfileOverviewFinishDistributionViewModel | null { return this.data.finishDistribution; } + get teamMemberships(): ProfileOverviewTeamMembershipViewModel[] { return this.data.teamMemberships; } + get socialSummary(): ProfileOverviewSocialSummaryViewModel { return this.data.socialSummary; } + get extendedProfile(): ProfileOverviewExtendedProfileViewModel | null { return this.data.extendedProfile; } } - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export interface ProfileOverviewStatsViewModel extends ViewModel { - totalRaces: number; - wins: number; - podiums: number; - dnfs: number; - avgFinish: number | null; - bestFinish: number | null; - worstFinish: number | null; - finishRate: number | null; - winRate: number | null; - podiumRate: number | null; - percentile: number | null; - rating: number | null; - consistency: number | null; - overallRank: number | null; -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export interface ProfileOverviewFinishDistributionViewModel extends ViewModel { - totalRaces: number; - wins: number; - podiums: number; - topTen: number; - dnfs: number; - other: number; -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export interface ProfileOverviewTeamMembershipViewModel extends ViewModel { - teamId: string; - teamName: string; - teamTag: string | null; - role: string; - joinedAt: string; - isCurrent: boolean; -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export interface ProfileOverviewSocialFriendSummaryViewModel extends ViewModel { - id: string; - name: string; - country: string; - avatarUrl: string; -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export interface ProfileOverviewSocialSummaryViewModel extends ViewModel { - friendsCount: number; - friends: ProfileOverviewSocialFriendSummaryViewModel[]; -} - -export type ProfileOverviewSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord'; - -export type ProfileOverviewAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary'; - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export interface ProfileOverviewAchievementViewModel extends ViewModel { - id: string; - title: string; - description: string; - icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap'; - rarity: ProfileOverviewAchievementRarity; - earnedAt: string; -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export interface ProfileOverviewSocialHandleViewModel extends ViewModel { - platform: ProfileOverviewSocialPlatform; - handle: string; - url: string; -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export interface ProfileOverviewExtendedProfileViewModel extends ViewModel { - socialHandles: ProfileOverviewSocialHandleViewModel[]; - achievements: ProfileOverviewAchievementViewModel[]; - racingStyle: string; - favoriteTrack: string; - favoriteCar: string; - timezone: string; - availableHours: string; - lookingForTeam: boolean; - openToRequests: boolean; -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export interface ProfileOverviewViewModel extends ViewModel { - currentDriver: ProfileOverviewDriverSummaryViewModel | null; - stats: ProfileOverviewStatsViewModel | null; - finishDistribution: ProfileOverviewFinishDistributionViewModel | null; - teamMemberships: ProfileOverviewTeamMembershipViewModel[]; - socialSummary: ProfileOverviewSocialSummaryViewModel; - extendedProfile: ProfileOverviewExtendedProfileViewModel | null; -} \ No newline at end of file diff --git a/apps/website/lib/view-models/ProtestDriverViewModel.ts b/apps/website/lib/view-models/ProtestDriverViewModel.ts index 03fa37a98..8b1b17c3d 100644 --- a/apps/website/lib/view-models/ProtestDriverViewModel.ts +++ b/apps/website/lib/view-models/ProtestDriverViewModel.ts @@ -1,15 +1,16 @@ -import { DriverSummaryDTO } from '@/lib/types/generated/DriverSummaryDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { ProtestDriverViewData } from "../view-data/ProtestDriverViewData"; export class ProtestDriverViewModel extends ViewModel { - constructor(private readonly dto: DriverSummaryDTO) {} + constructor(private readonly data: ProtestDriverViewData) { + super(); + } get id(): string { - return this.dto.id; + return this.data.id; } get name(): string { - return this.dto.name; + return this.data.name; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/ProtestViewModel.ts b/apps/website/lib/view-models/ProtestViewModel.ts index d4df0732a..3bcd13680 100644 --- a/apps/website/lib/view-models/ProtestViewModel.ts +++ b/apps/website/lib/view-models/ProtestViewModel.ts @@ -1,103 +1,30 @@ -import { ProtestDTO } from '@/lib/types/generated/ProtestDTO'; -import { RaceProtestDTO } from '@/lib/types/generated/RaceProtestDTO'; -import { DateDisplay } from '../display-objects/DateDisplay'; -import { StatusDisplay } from '../display-objects/StatusDisplay'; - -/** - * Protest view model - * Represents a race protest - */ +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; +import { StatusDisplay } from '@/lib/display-objects/StatusDisplay'; import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { ProtestViewData } from "@/lib/view-data/ProtestViewData"; export class ProtestViewModel extends ViewModel { - 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; + private readonly data: ProtestViewData; - constructor(dto: ProtestDTO | RaceProtestDTO) { - this.id = dto.id; - - // Type narrowing for raceId - if ('raceId' in dto) { - this.raceId = dto.raceId; - } else { - this.raceId = ''; - } - - this.protestingDriverId = dto.protestingDriverId; - this.accusedDriverId = dto.accusedDriverId; - - // Type narrowing for description - if ('description' in dto && typeof dto.description === 'string') { - this.description = dto.description; - } else { - this.description = ''; - } - - // Type narrowing for submittedAt and filedAt - if ('submittedAt' in dto && typeof dto.submittedAt === 'string') { - this.submittedAt = dto.submittedAt; - } else if ('filedAt' in dto && typeof dto.filedAt === 'string') { - this.submittedAt = dto.filedAt; - } else { - this.submittedAt = ''; - } - - if ('filedAt' in dto && typeof dto.filedAt === 'string') { - this.filedAt = dto.filedAt; - } else if ('submittedAt' in dto && typeof dto.submittedAt === 'string') { - this.filedAt = dto.submittedAt; - } - - // Handle different DTO structures - if ('status' in dto && typeof dto.status === 'string') { - this.status = dto.status; - } else { - this.status = 'pending'; - } - - // Handle incident data - if ('incident' in dto && dto.incident) { - const incident = dto.incident as { lap?: number; description?: string }; - this.incident = { - lap: typeof incident.lap === 'number' ? incident.lap : undefined, - description: typeof incident.description === 'string' ? incident.description : undefined - }; - } else if (('lap' in dto && typeof (dto as { lap?: number }).lap === 'number') || - ('description' in dto && typeof (dto as { description?: string }).description === 'string')) { - this.incident = { - lap: 'lap' in dto ? (dto as { lap?: number }).lap : undefined, - description: 'description' in dto ? (dto as { description?: string }).description : undefined - }; - } else { - this.incident = null; - } - - if ('proofVideoUrl' in dto) { - this.proofVideoUrl = (dto as { proofVideoUrl?: string }).proofVideoUrl || null; - } - if ('comment' in dto) { - this.comment = (dto as { comment?: string }).comment || null; - } - - // Status and decision metadata are not part of the protest DTO in this build; they default to a pending, unreviewed protest - if (!('status' in dto)) { - this.status = 'pending'; - } - this.reviewedAt = undefined; - this.decisionNotes = undefined; + constructor(data: ProtestViewData) { + super(); + this.data = data; } + get id(): string { return this.data.id; } + get raceId(): string { return this.data.raceId; } + get protestingDriverId(): string { return this.data.protestingDriverId; } + get accusedDriverId(): string { return this.data.accusedDriverId; } + get description(): string { return this.data.description; } + get submittedAt(): string { return this.data.submittedAt; } + get filedAt(): string | undefined { return this.data.filedAt; } + get status(): string { return this.data.status; } + get reviewedAt(): string | undefined { return this.data.reviewedAt; } + get decisionNotes(): string | undefined { return this.data.decisionNotes; } + get incident(): { lap?: number; description?: string } | null | undefined { return this.data.incident; } + get proofVideoUrl(): string | null | undefined { return this.data.proofVideoUrl; } + get comment(): string | null | undefined { return this.data.comment; } + /** UI-specific: Formatted submitted date */ get formattedSubmittedAt(): string { return DateDisplay.formatShort(this.submittedAt); diff --git a/apps/website/lib/view-models/RaceDetailEntryViewModel.ts b/apps/website/lib/view-models/RaceDetailEntryViewModel.ts index f279b3090..fca278367 100644 --- a/apps/website/lib/view-models/RaceDetailEntryViewModel.ts +++ b/apps/website/lib/view-models/RaceDetailEntryViewModel.ts @@ -1,21 +1,18 @@ -import { RaceDetailEntryDTO } from '@/lib/types/generated/RaceDetailEntryDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RaceDetailEntryViewData } from "../view-data/RaceDetailEntryViewData"; export class RaceDetailEntryViewModel extends ViewModel { - id: string; - name: string; - country: string; - avatarUrl: string; - isCurrentUser: boolean; - rating: number | null; + private readonly data: RaceDetailEntryViewData; - constructor(dto: RaceDetailEntryDTO, currentDriverId: string, rating?: number) { - this.id = dto.id; - this.name = dto.name; - this.country = dto.country; - this.avatarUrl = dto.avatarUrl || ''; - this.isCurrentUser = dto.id === currentDriverId; - this.rating = rating ?? null; + constructor(data: RaceDetailEntryViewData) { + super(); + this.data = data; } + + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get country(): string { return this.data.country; } + get avatarUrl(): string { return this.data.avatarUrl; } + get isCurrentUser(): boolean { return this.data.isCurrentUser; } + get rating(): number | null { return this.data.rating; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/RaceDetailUserResultViewModel.ts b/apps/website/lib/view-models/RaceDetailUserResultViewModel.ts index 0114591dd..2a9cd730d 100644 --- a/apps/website/lib/view-models/RaceDetailUserResultViewModel.ts +++ b/apps/website/lib/view-models/RaceDetailUserResultViewModel.ts @@ -1,28 +1,24 @@ -import { RaceDetailUserResultDTO } from '@/lib/types/generated/RaceDetailUserResultDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { DurationDisplay } from "../display-objects/DurationDisplay"; +import type { RaceDetailUserResultViewData } from "../view-data/RaceDetailUserResultViewData"; export class RaceDetailUserResultViewModel extends ViewModel { - position!: number; - startPosition!: number; - incidents!: number; - fastestLap!: number; - positionChange!: number; - isPodium!: boolean; - isClean!: boolean; - ratingChange!: number; + private readonly data: RaceDetailUserResultViewData; - constructor(dto: RaceDetailUserResultDTO) { - this.position = dto.position; - this.startPosition = dto.startPosition; - this.incidents = dto.incidents; - this.fastestLap = dto.fastestLap; - this.positionChange = dto.positionChange; - this.isPodium = dto.isPodium; - this.isClean = dto.isClean; - this.ratingChange = dto.ratingChange ?? 0; + constructor(data: RaceDetailUserResultViewData) { + super(); + this.data = data; } + get position(): number { return this.data.position; } + get startPosition(): number { return this.data.startPosition; } + get incidents(): number { return this.data.incidents; } + get fastestLap(): number { return this.data.fastestLap; } + get positionChange(): number { return this.data.positionChange; } + get isPodium(): boolean { return this.data.isPodium; } + get isClean(): boolean { return this.data.isClean; } + get ratingChange(): number { return this.data.ratingChange; } + /** UI-specific: Display for position change */ get positionChangeDisplay(): string { if (this.positionChange > 0) return `+${this.positionChange}`; @@ -58,9 +54,6 @@ export class RaceDetailUserResultViewModel extends ViewModel { /** UI-specific: Formatted lap time */ get lapTimeFormatted(): string { if (this.fastestLap <= 0) return '--:--.---'; - const minutes = Math.floor(this.fastestLap / 60); - const seconds = Math.floor(this.fastestLap % 60); - const milliseconds = Math.floor((this.fastestLap % 1) * 1000); - return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`; + return DurationDisplay.formatSeconds(this.fastestLap); } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/RaceDetailsLeagueViewModel.ts b/apps/website/lib/view-models/RaceDetailsLeagueViewModel.ts new file mode 100644 index 000000000..cf65c8ede --- /dev/null +++ b/apps/website/lib/view-models/RaceDetailsLeagueViewModel.ts @@ -0,0 +1,11 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RaceDetailsLeagueViewData } from "../view-data/RaceDetailsViewData"; + +export class RaceDetailsLeagueViewModel extends ViewModel { + private readonly data: RaceDetailsLeagueViewData; + constructor(data: RaceDetailsLeagueViewData) { super(); this.data = data; } + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get description(): string | null | undefined { return this.data.description; } + get settings(): unknown { return this.data.settings; } +} diff --git a/apps/website/lib/view-models/RaceDetailsRaceViewModel.ts b/apps/website/lib/view-models/RaceDetailsRaceViewModel.ts new file mode 100644 index 000000000..1104c450c --- /dev/null +++ b/apps/website/lib/view-models/RaceDetailsRaceViewModel.ts @@ -0,0 +1,13 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RaceDetailsRaceViewData } from "../view-data/RaceDetailsViewData"; + +export class RaceDetailsRaceViewModel extends ViewModel { + private readonly data: RaceDetailsRaceViewData; + constructor(data: RaceDetailsRaceViewData) { super(); this.data = data; } + get id(): string { return this.data.id; } + get track(): string { return this.data.track; } + get car(): string { return this.data.car; } + get scheduledAt(): string { return this.data.scheduledAt; } + get status(): string { return this.data.status; } + get sessionType(): string { return this.data.sessionType; } +} diff --git a/apps/website/lib/view-models/RaceDetailsRegistrationViewModel.ts b/apps/website/lib/view-models/RaceDetailsRegistrationViewModel.ts new file mode 100644 index 000000000..6f2025f16 --- /dev/null +++ b/apps/website/lib/view-models/RaceDetailsRegistrationViewModel.ts @@ -0,0 +1,9 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RaceDetailsRegistrationViewData } from "../view-data/RaceDetailsViewData"; + +export class RaceDetailsRegistrationViewModel extends ViewModel { + private readonly data: RaceDetailsRegistrationViewData; + constructor(data: RaceDetailsRegistrationViewData) { super(); this.data = data; } + get canRegister(): boolean { return this.data.canRegister; } + get isUserRegistered(): boolean { return this.data.isUserRegistered; } +} diff --git a/apps/website/lib/view-models/RaceDetailsViewModel.ts b/apps/website/lib/view-models/RaceDetailsViewModel.ts index 1cbff10f4..f0b6f93b8 100644 --- a/apps/website/lib/view-models/RaceDetailsViewModel.ts +++ b/apps/website/lib/view-models/RaceDetailsViewModel.ts @@ -1,41 +1,29 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; import { RaceDetailEntryViewModel } from './RaceDetailEntryViewModel'; import { RaceDetailUserResultViewModel } from './RaceDetailUserResultViewModel'; +import { RaceDetailsRaceViewModel } from './RaceDetailsRaceViewModel'; +import { RaceDetailsLeagueViewModel } from './RaceDetailsLeagueViewModel'; +import { RaceDetailsRegistrationViewModel } from './RaceDetailsRegistrationViewModel'; +import type { RaceDetailsViewData } from "../view-data/RaceDetailsViewData"; -import { ViewModel } from "../contracts/view-models/ViewModel"; +export class RaceDetailsViewModel extends ViewModel { + private readonly data: RaceDetailsViewData; + readonly race: RaceDetailsRaceViewModel | null; + readonly league: RaceDetailsLeagueViewModel | null; + readonly entryList: RaceDetailEntryViewModel[]; + readonly registration: RaceDetailsRegistrationViewModel; + readonly userResult: RaceDetailUserResultViewModel | null; -export type RaceDetailsRaceViewModel = ViewModel & { - id: string; - track: string; - car: string; - scheduledAt: string; - status: string; - sessionType: string; -}; + constructor(data: RaceDetailsViewData) { + super(); + this.data = data; + this.race = data.race ? new RaceDetailsRaceViewModel(data.race) : null; + this.league = data.league ? new RaceDetailsLeagueViewModel(data.league) : null; + this.entryList = data.entryList.map(e => new RaceDetailEntryViewModel(e)); + this.registration = new RaceDetailsRegistrationViewModel(data.registration); + this.userResult = data.userResult ? new RaceDetailUserResultViewModel(data.userResult) : null; + } -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export type RaceDetailsLeagueViewModel = ViewModel & { - id: string; - name: string; - description?: string | null; - settings?: unknown; -}; - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export type RaceDetailsRegistrationViewModel = ViewModel & { - canRegister: boolean; - isUserRegistered: boolean; -}; - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export type RaceDetailsViewModel = ViewModel & { - race: RaceDetailsRaceViewModel | null; - league: RaceDetailsLeagueViewModel | null; - entryList: RaceDetailEntryViewModel[]; - registration: RaceDetailsRegistrationViewModel; - userResult: RaceDetailUserResultViewModel | null; - canReopenRace: boolean; - error?: string; -}; \ No newline at end of file + get canReopenRace(): boolean { return this.data.canReopenRace; } + get error(): string | undefined { return this.data.error; } +} diff --git a/apps/website/lib/view-models/RaceListItemViewModel.ts b/apps/website/lib/view-models/RaceListItemViewModel.ts index a17d5c3b6..9f9396870 100644 --- a/apps/website/lib/view-models/RaceListItemViewModel.ts +++ b/apps/website/lib/view-models/RaceListItemViewModel.ts @@ -1,65 +1,40 @@ -// DTO matching the backend RacesPageDataRaceDTO -export interface RaceListItemDTO { - id: string; - track: string; - car: string; - scheduledAt: string; - status: string; - leagueId: string; - leagueName: string; - strengthOfField?: number | null; - isUpcoming: boolean; - isLive: boolean; - isPast: boolean; -} - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { RaceStatusDisplay } from "../display-objects/RaceStatusDisplay"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import type { RaceListItemViewData } from "../view-data/RaceListItemViewData"; export class RaceListItemViewModel extends ViewModel { - id: string; - track: string; - car: string; - scheduledAt: string; - status: string; - leagueId: string; - leagueName: string; - strengthOfField: number | null; - isUpcoming: boolean; - isLive: boolean; - isPast: boolean; + private readonly data: RaceListItemViewData; - constructor(dto: RaceListItemDTO) { - this.id = dto.id; - this.track = dto.track; - this.car = dto.car; - this.scheduledAt = dto.scheduledAt; - this.status = dto.status; - this.leagueId = dto.leagueId; - this.leagueName = dto.leagueName; - this.strengthOfField = dto.strengthOfField ?? null; - this.isUpcoming = dto.isUpcoming; - this.isLive = dto.isLive; - this.isPast = dto.isPast; + constructor(data: RaceListItemViewData) { + super(); + this.data = data; } + get id(): string { return this.data.id; } + get track(): string { return this.data.track; } + get car(): string { return this.data.car; } + get scheduledAt(): string { return this.data.scheduledAt; } + get status(): string { return this.data.status; } + get leagueId(): string { return this.data.leagueId; } + get leagueName(): string { return this.data.leagueName; } + get strengthOfField(): number | null { return this.data.strengthOfField; } + get isUpcoming(): boolean { return this.data.isUpcoming; } + get isLive(): boolean { return this.data.isLive; } + get isPast(): boolean { return this.data.isPast; } + get title(): string { return `${this.track} - ${this.car}`; } /** UI-specific: Formatted scheduled time */ get formattedScheduledTime(): string { - return new Date(this.scheduledAt).toLocaleString(); + return DateDisplay.formatDateTime(this.scheduledAt); } /** UI-specific: Badge variant for status */ get statusBadgeVariant(): string { - switch (this.status) { - case 'scheduled': return 'info'; - case 'running': return 'success'; - case 'completed': return 'secondary'; - case 'cancelled': return 'danger'; - default: return 'default'; - } + return RaceStatusDisplay.getVariant(this.status); } /** UI-specific: Time until start in minutes */ diff --git a/apps/website/lib/view-models/RaceResultViewModel.ts b/apps/website/lib/view-models/RaceResultViewModel.ts index 6072a6695..b5fa09957 100644 --- a/apps/website/lib/view-models/RaceResultViewModel.ts +++ b/apps/website/lib/view-models/RaceResultViewModel.ts @@ -1,24 +1,29 @@ -import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO'; -import { FinishDisplay } from '../display-objects/FinishDisplay'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { FinishDisplay } from '../display-objects/FinishDisplay'; +import { DurationDisplay } from '../display-objects/DurationDisplay'; +import type { RaceResultViewData } from "../view-data/RaceResultViewData"; export class RaceResultViewModel extends ViewModel { - driverId!: string; - driverName!: string; - avatarUrl!: string; - position!: number; - startPosition!: number; - incidents!: number; - fastestLap!: number; - positionChange!: number; - isPodium!: boolean; - isClean!: boolean; + private readonly data: RaceResultViewData; - constructor(dto: RaceResultDTO) { - Object.assign(this, dto); + constructor(data: RaceResultViewData) { + super(); + this.data = data; } + get driverId(): string { return this.data.driverId; } + get driverName(): string { return this.data.driverName; } + get avatarUrl(): string { return this.data.avatarUrl; } + get position(): number { return this.data.position; } + get startPosition(): number { return this.data.startPosition; } + get incidents(): number { return this.data.incidents; } + get fastestLap(): number { return this.data.fastestLap; } + get positionChange(): number { return this.data.positionChange; } + get isPodium(): boolean { return this.data.isPodium; } + get isClean(): boolean { return this.data.isClean; } + get id(): string { return this.data.id; } + get raceId(): string { return this.data.raceId; } + /** UI-specific: Display for position change */ get positionChangeDisplay(): string { if (this.positionChange > 0) return `+${this.positionChange}`; @@ -58,10 +63,7 @@ export class RaceResultViewModel extends ViewModel { /** UI-specific: Formatted lap time */ get lapTimeFormatted(): string { if (this.fastestLap <= 0) return '--:--.---'; - const minutes = Math.floor(this.fastestLap / 60); - const seconds = Math.floor(this.fastestLap % 60); - const milliseconds = Math.floor((this.fastestLap % 1) * 1000); - return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`; + return DurationDisplay.formatSeconds(this.fastestLap); } /** Required by ResultsTable */ @@ -87,9 +89,4 @@ export class RaceResultViewModel extends ViewModel { } return undefined; } - - // Note: The generated DTO doesn't have id or raceId - // These will need to be added when the OpenAPI spec is updated - id: string = ''; - raceId: string = ''; -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/RaceResultsDetailViewModel.ts b/apps/website/lib/view-models/RaceResultsDetailViewModel.ts index 415492b7a..4f1b6d7fa 100644 --- a/apps/website/lib/view-models/RaceResultsDetailViewModel.ts +++ b/apps/website/lib/view-models/RaceResultsDetailViewModel.ts @@ -1,36 +1,27 @@ -import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO'; -import { RaceResultsDetailDTO } from '@/lib/types/generated/RaceResultsDetailDTO'; -import { RaceResultViewModel } from './RaceResultViewModel'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { RaceResultViewModel } from './RaceResultViewModel'; +import type { RaceResultsDetailViewData } from "../view-data/RaceResultsDetailViewData"; export class RaceResultsDetailViewModel extends ViewModel { - raceId: string; - track: string; - currentUserId: string; + private readonly data: RaceResultsDetailViewData; + readonly results: RaceResultViewModel[]; - constructor(dto: RaceResultsDetailDTO & { results?: RaceResultDTO[] }, currentUserId: string) { + constructor(data: RaceResultsDetailViewData) { super(); - this.raceId = dto.raceId; - this.track = dto.track; - this.currentUserId = currentUserId; - - // Map results if provided - if (dto.results) { - this.results = dto.results.map(r => new RaceResultViewModel(r)); - } + this.data = data; + this.results = data.results.map(r => new RaceResultViewModel(r)); } - // Note: The generated DTO is incomplete - // These fields will need to be added when the OpenAPI spec is updated - results: RaceResultViewModel[] = []; - league?: { id: string; name: string }; - race?: { id: string; track: string; scheduledAt: string }; - drivers: { id: string; name: string }[] = []; - pointsSystem: Record = {}; - fastestLapTime: number = 0; - penalties: { driverId: string; type: string; value?: number }[] = []; - currentDriverId: string = ''; + get raceId(): string { return this.data.raceId; } + get track(): string { return this.data.track; } + get currentUserId(): string { return this.data.currentUserId; } + get league() { return this.data.league; } + get race() { return this.data.race; } + get drivers() { return this.data.drivers; } + get pointsSystem() { return this.data.pointsSystem; } + get fastestLapTime(): number { return this.data.fastestLapTime; } + get penalties() { return this.data.penalties; } + get currentDriverId(): string { return this.data.currentDriverId; } /** UI-specific: Results sorted by position */ get resultsByPosition(): RaceResultViewModel[] { @@ -63,4 +54,4 @@ export class RaceResultsDetailViewModel extends ViewModel { averageIncidents: total > 0 ? totalIncidents / total : 0 }; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/RaceStatsViewModel.ts b/apps/website/lib/view-models/RaceStatsViewModel.ts index ee5eaafb9..8473cd9cb 100644 --- a/apps/website/lib/view-models/RaceStatsViewModel.ts +++ b/apps/website/lib/view-models/RaceStatsViewModel.ts @@ -1,20 +1,19 @@ -import type { RaceStatsDTO } from '@/lib/types/generated/RaceStatsDTO'; - -/** - * Race stats view model - * Represents race statistics for display - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RaceStatsViewData } from "../view-data/RaceStatsViewData"; export class RaceStatsViewModel extends ViewModel { - totalRaces: number; + private readonly data: RaceStatsViewData; - constructor(dto: RaceStatsDTO) { - this.totalRaces = dto.totalRaces; + constructor(data: RaceStatsViewData) { + super(); + this.data = data; } + get totalRaces(): number { return this.data.totalRaces; } + /** UI-specific: Formatted total races */ get formattedTotalRaces(): string { + // Client-only formatting return this.totalRaces.toLocaleString(); } } diff --git a/apps/website/lib/view-models/RaceStewardingViewModel.ts b/apps/website/lib/view-models/RaceStewardingViewModel.ts index 2cc593a5f..df1e6d889 100644 --- a/apps/website/lib/view-models/RaceStewardingViewModel.ts +++ b/apps/website/lib/view-models/RaceStewardingViewModel.ts @@ -1,75 +1,20 @@ -// DTO interfaces matching the API responses -interface RaceDetailDTO { - race: { - id: string; - track: string; - scheduledAt: string; - status: string; - } | null; - league: { - id: string; - name: string; - } | null; -} - -interface RaceProtestsDTO { - protests: Array<{ - id: string; - protestingDriverId: string; - accusedDriverId: string; - incident: { - lap: number; - description: string; - }; - filedAt: string; - status: string; - decisionNotes?: string; - proofVideoUrl?: string; - }>; - driverMap: Record; -} - -interface RacePenaltiesDTO { - penalties: Array<{ - id: string; - driverId: string; - type: string; - value: number; - reason: string; - notes?: string; - }>; - driverMap: Record; -} - -interface RaceStewardingDTO { - raceDetail: RaceDetailDTO; - protests: RaceProtestsDTO; - penalties: RacePenaltiesDTO; -} - -/** - * Race Stewarding View Model - * Represents all data needed for race stewarding (protests, penalties, race info) - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RaceStewardingViewData } from "../view-data/RaceStewardingViewData"; export class RaceStewardingViewModel extends ViewModel { - race: RaceDetailDTO['race']; - league: RaceDetailDTO['league']; - protests: RaceProtestsDTO['protests']; - penalties: RacePenaltiesDTO['penalties']; - driverMap: Record; + private readonly data: RaceStewardingViewData; - constructor(dto: RaceStewardingDTO) { - this.race = dto.raceDetail.race; - this.league = dto.raceDetail.league; - this.protests = dto.protests.protests; - this.penalties = dto.penalties.penalties; - - // Merge driver maps from protests and penalties - this.driverMap = { ...dto.protests.driverMap, ...dto.penalties.driverMap }; + constructor(data: RaceStewardingViewData) { + super(); + this.data = data; } + get race() { return this.data.race; } + get league() { return this.data.league; } + get protests() { return this.data.protests; } + get penalties() { return this.data.penalties; } + get driverMap() { return this.data.driverMap; } + /** UI-specific: Pending protests */ get pendingProtests() { return this.protests.filter(p => p.status === 'pending' || p.status === 'under_review'); diff --git a/apps/website/lib/view-models/RaceViewModel.ts b/apps/website/lib/view-models/RaceViewModel.ts index d152aa9d8..977250afc 100644 --- a/apps/website/lib/view-models/RaceViewModel.ts +++ b/apps/website/lib/view-models/RaceViewModel.ts @@ -1,63 +1,27 @@ -import { RaceDTO } from '@/lib/types/generated/RaceDTO'; -import { RacesPageDataRaceDTO } from '@/lib/types/generated/RacesPageDataRaceDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { RaceViewData } from "../view-data/RaceViewData"; export class RaceViewModel extends ViewModel { - constructor( - private readonly dto: RaceDTO | RacesPageDataRaceDTO, - private readonly _status?: string, - private readonly _registeredCount?: number, - private readonly _strengthOfField?: number - ) {} + private readonly data: RaceViewData; - get id(): string { - return this.dto.id; + constructor(data: RaceViewData) { + super(); + this.data = data; } - get name(): string { - if ('name' in this.dto) { - return this.dto.name; - } - return ''; - } - - get date(): string { - if ('date' in this.dto) { - return this.dto.date; - } - if ('scheduledAt' in this.dto) { - return this.dto.scheduledAt; - } - return ''; - } - - get scheduledAt(): string { - return this.date; - } - - get track(): string { - return 'track' in this.dto ? this.dto.track || '' : ''; - } - - get car(): string { - return 'car' in this.dto ? this.dto.car || '' : ''; - } - - get status(): string | undefined { - return this._status || ('status' in this.dto ? this.dto.status : undefined); - } - - get registeredCount(): number | undefined { - return this._registeredCount; - } - - get strengthOfField(): number | undefined { - return this._strengthOfField || ('strengthOfField' in this.dto ? this.dto.strengthOfField : undefined); - } + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get date(): string { return this.data.date; } + get scheduledAt(): string { return this.data.date; } + get track(): string { return this.data.track; } + get car(): string { return this.data.car; } + get status(): string | undefined { return this.data.status; } + get registeredCount(): number | undefined { return this.data.registeredCount; } + get strengthOfField(): number | undefined { return this.data.strengthOfField; } /** UI-specific: Formatted date */ get formattedDate(): string { + // Client-only formatting return new Date(this.date).toLocaleDateString(); } } diff --git a/apps/website/lib/view-models/RaceWithSOFViewModel.ts b/apps/website/lib/view-models/RaceWithSOFViewModel.ts index 960b04960..e8aaf1bd5 100644 --- a/apps/website/lib/view-models/RaceWithSOFViewModel.ts +++ b/apps/website/lib/view-models/RaceWithSOFViewModel.ts @@ -1,16 +1,15 @@ -import { RaceWithSOFDTO } from '@/lib/types/generated/RaceWithSOFDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RaceWithSOFViewData } from "../view-data/RaceWithSOFViewData"; export class RaceWithSOFViewModel extends ViewModel { - id: string; - track: string; - strengthOfField: number | null; + private readonly data: RaceWithSOFViewData; - constructor(dto: RaceWithSOFDTO) { + constructor(data: RaceWithSOFViewData) { super(); - this.id = dto.id; - this.track = dto.track; - this.strengthOfField = 'strengthOfField' in dto ? dto.strengthOfField ?? null : null; + 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; } } \ No newline at end of file diff --git a/apps/website/lib/view-models/RacesPageViewModel.ts b/apps/website/lib/view-models/RacesPageViewModel.ts index ee1d2241f..571a818a3 100644 --- a/apps/website/lib/view-models/RacesPageViewModel.ts +++ b/apps/website/lib/view-models/RacesPageViewModel.ts @@ -1,56 +1,19 @@ import { RaceListItemViewModel } from './RaceListItemViewModel'; -import type { RacesPageDataRaceDTO } from '@/lib/types/generated/RacesPageDataRaceDTO'; - -// DTO matching the backend RacesPageDataDTO -interface RacesPageDTO { - races: RacesPageDataRaceDTO[]; -} +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { RacesPageViewData } from '../view-data/RacesPageViewData'; /** * Races page view model * Represents the races page data with all races in a single list */ -import { ViewModel } from "../contracts/view-models/ViewModel"; - export class RacesPageViewModel extends ViewModel { - races: RaceListItemViewModel[]; + private readonly data: RacesPageViewData; + readonly races: RaceListItemViewModel[]; - constructor(dto: RacesPageDTO) { - this.races = dto.races.map((r) => { - const status = 'status' in r ? r.status : 'unknown'; - - const isUpcoming = - 'isUpcoming' in r ? r.isUpcoming : - (status === 'upcoming' || status === 'scheduled'); - - const isLive = - 'isLive' in r ? r.isLive : - (status === 'live' || status === 'running'); - - const isPast = - 'isPast' in r ? r.isPast : - (status === 'completed' || status === 'finished' || status === 'cancelled'); - - // Build the RaceListItemDTO from the input with proper type checking - const scheduledAt = 'scheduledAt' in r ? r.scheduledAt : - ('date' in r ? (r as { date?: string }).date : ''); - - const normalized = { - id: r.id, - track: 'track' in r ? r.track : '', - car: 'car' in r ? r.car : '', - scheduledAt: scheduledAt || '', - status: status, - leagueId: 'leagueId' in r ? r.leagueId : '', - leagueName: 'leagueName' in r ? r.leagueName : '', - strengthOfField: 'strengthOfField' in r ? (r as { strengthOfField?: number }).strengthOfField ?? null : null, - isUpcoming: Boolean(isUpcoming), - isLive: Boolean(isLive), - isPast: Boolean(isPast), - }; - - return new RaceListItemViewModel(normalized); - }); + constructor(data: RacesPageViewData) { + super(); + this.data = data; + this.races = data.races.map((r) => new RaceListItemViewModel(r)); } /** UI-specific: Total races */ @@ -87,4 +50,4 @@ export class RacesPageViewModel extends ViewModel { get completedRaces(): RaceListItemViewModel[] { return this.races.filter(r => r.status === 'completed'); } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/RecordEngagementInputViewModel.ts b/apps/website/lib/view-models/RecordEngagementInputViewModel.ts index 29dcf9788..7d7bd98ea 100644 --- a/apps/website/lib/view-models/RecordEngagementInputViewModel.ts +++ b/apps/website/lib/view-models/RecordEngagementInputViewModel.ts @@ -1,17 +1,17 @@ /** * Record engagement input view model * Represents input data for recording an engagement event - * - * Note: No matching generated DTO available yet */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RecordEngagementInputViewData } from "../view-data/RecordEngagementInputViewData"; export class RecordEngagementInputViewModel extends ViewModel { eventType: string; userId?: string; metadata?: Record; - constructor(data: { eventType: string; userId?: string; metadata?: Record }) { + constructor(data: RecordEngagementInputViewData) { + super(); this.eventType = data.eventType; this.userId = data.userId; this.metadata = data.metadata; @@ -31,4 +31,4 @@ export class RecordEngagementInputViewModel extends ViewModel { get metadataKeysCount(): number { return this.metadata ? Object.keys(this.metadata).length : 0; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/RecordEngagementOutputViewModel.ts b/apps/website/lib/view-models/RecordEngagementOutputViewModel.ts index 006a4def6..09f352a21 100644 --- a/apps/website/lib/view-models/RecordEngagementOutputViewModel.ts +++ b/apps/website/lib/view-models/RecordEngagementOutputViewModel.ts @@ -1,18 +1,13 @@ -import type { RecordEngagementOutputDTO } from '@/lib/types/generated/RecordEngagementOutputDTO'; - -/** - * Record engagement output view model - * Represents the result of recording an engagement event for UI consumption - */ import { ViewModel } from "../contracts/view-models/ViewModel"; export class RecordEngagementOutputViewModel extends ViewModel { eventId: string; engagementWeight: number; - constructor(dto: RecordEngagementOutputDTO) { - this.eventId = dto.eventId; - this.engagementWeight = dto.engagementWeight; + constructor(eventId: string, engagementWeight: number) { + super(); + this.eventId = eventId; + this.engagementWeight = engagementWeight; } /** UI-specific: Formatted event ID for display */ diff --git a/apps/website/lib/view-models/RecordPageViewInputViewModel.ts b/apps/website/lib/view-models/RecordPageViewInputViewModel.ts index e37cda736..8be303fe4 100644 --- a/apps/website/lib/view-models/RecordPageViewInputViewModel.ts +++ b/apps/website/lib/view-models/RecordPageViewInputViewModel.ts @@ -1,16 +1,16 @@ /** * Record page view input view model * Represents input data for recording a page view - * - * Note: No matching generated DTO available yet */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RecordPageViewInputViewData } from "../view-data/RecordPageViewInputViewData"; export class RecordPageViewInputViewModel extends ViewModel { path: string; userId?: string; - constructor(data: { path: string; userId?: string }) { + constructor(data: RecordPageViewInputViewData) { + super(); this.path = data.path; this.userId = data.userId; } @@ -24,4 +24,4 @@ export class RecordPageViewInputViewModel extends ViewModel { get hasUserContext(): boolean { return this.userId !== undefined && this.userId !== ''; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/RecordPageViewOutputViewModel.ts b/apps/website/lib/view-models/RecordPageViewOutputViewModel.ts index 1ddbd01c2..5d6f53d40 100644 --- a/apps/website/lib/view-models/RecordPageViewOutputViewModel.ts +++ b/apps/website/lib/view-models/RecordPageViewOutputViewModel.ts @@ -1,16 +1,12 @@ -import type { RecordPageViewOutputDTO } from '@/lib/types/generated/RecordPageViewOutputDTO'; - -/** - * Record page view output view model - * Represents the result of recording a page view for UI consumption - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RecordPageViewOutputViewData } from "../view-data/RecordPageViewOutputViewData"; export class RecordPageViewOutputViewModel extends ViewModel { pageViewId: string; - constructor(dto: RecordPageViewOutputDTO) { - this.pageViewId = dto.pageViewId; + constructor(data: RecordPageViewOutputViewData) { + super(); + this.pageViewId = data.pageViewId; } /** UI-specific: Formatted page view ID for display */ diff --git a/apps/website/lib/view-models/RemoveMemberViewModel.ts b/apps/website/lib/view-models/RemoveMemberViewModel.ts index 4e7c95977..9d9540404 100644 --- a/apps/website/lib/view-models/RemoveMemberViewModel.ts +++ b/apps/website/lib/view-models/RemoveMemberViewModel.ts @@ -1,21 +1,21 @@ -import { RemoveLeagueMemberOutputDTO } from '@/lib/types/generated/RemoveLeagueMemberOutputDTO'; - /** * View Model for Remove Member Result * * Represents the result of removing a member from a league in a UI-ready format. */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { RemoveMemberViewData } from "../view-data/RemoveMemberViewData"; export class RemoveMemberViewModel extends ViewModel { success: boolean; - constructor(dto: RemoveLeagueMemberOutputDTO) { - this.success = dto.success; + constructor(data: RemoveMemberViewData) { + super(); + this.success = data.success; } /** UI-specific: Success message */ get successMessage(): string { return this.success ? 'Member removed successfully!' : 'Failed to remove member.'; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/RenewalAlertViewModel.ts b/apps/website/lib/view-models/RenewalAlertViewModel.ts index c3902d8a8..fa80bbe4e 100644 --- a/apps/website/lib/view-models/RenewalAlertViewModel.ts +++ b/apps/website/lib/view-models/RenewalAlertViewModel.ts @@ -1,9 +1,7 @@ -/** - * Renewal Alert View Model - * - * View model for upcoming renewal alerts. - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import type { RenewalAlertViewData } from "../view-data/RenewalAlertViewData"; export class RenewalAlertViewModel extends ViewModel { id: string; @@ -12,7 +10,8 @@ export class RenewalAlertViewModel extends ViewModel { renewDate: Date; price: number; - constructor(data: any) { + constructor(data: RenewalAlertViewData) { + super(); this.id = data.id; this.name = data.name; this.type = data.type; @@ -21,11 +20,11 @@ export class RenewalAlertViewModel extends ViewModel { } get formattedPrice(): string { - return `$${this.price}`; + return CurrencyDisplay.format(this.price); } get formattedRenewDate(): string { - return this.renewDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + return DateDisplay.formatShort(this.renewDate); } get typeIcon() { @@ -48,4 +47,4 @@ export class RenewalAlertViewModel extends ViewModel { get isUrgent(): boolean { return this.daysUntilRenewal <= 30; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/ScoringConfigurationViewModel.ts b/apps/website/lib/view-models/ScoringConfigurationViewModel.ts index bb86bc661..d482e7308 100644 --- a/apps/website/lib/view-models/ScoringConfigurationViewModel.ts +++ b/apps/website/lib/view-models/ScoringConfigurationViewModel.ts @@ -1,19 +1,6 @@ -import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; -import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel'; - -export interface CustomPointsConfig { - racePoints: number[]; - poleBonusPoints: number; - fastestLapPoints: number; - leaderLapPoints: number; -} - -/** - * ScoringConfigurationViewModel - * - * View model for scoring configuration including presets and custom points - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import { LeagueScoringPresetViewModel } from './LeagueScoringPresetViewModel'; +import type { ScoringConfigurationViewData, CustomPointsConfig } from "../view-data/ScoringConfigurationViewData"; export class ScoringConfigurationViewModel extends ViewModel { readonly patternId?: string; @@ -21,16 +8,13 @@ export class ScoringConfigurationViewModel extends ViewModel { readonly customPoints?: CustomPointsConfig; readonly currentPreset?: LeagueScoringPresetViewModel; - constructor( - config: LeagueConfigFormModel['scoring'], - presets: LeagueScoringPresetViewModel[], - customPoints?: CustomPointsConfig - ) { - this.patternId = config.patternId; - this.customScoringEnabled = config.customScoringEnabled || false; - this.customPoints = customPoints; - this.currentPreset = config.patternId - ? presets.find(p => p.id === config.patternId) + constructor(data: ScoringConfigurationViewData) { + super(); + this.patternId = data.config.patternId; + this.customScoringEnabled = data.config.customScoringEnabled || false; + this.customPoints = data.customPoints; + this.currentPreset = data.config.patternId + ? new LeagueScoringPresetViewModel(data.presets.find(p => p.id === data.config.patternId)!) : undefined; } @@ -49,4 +33,4 @@ export class ScoringConfigurationViewModel extends ViewModel { leaderLapPoints: 0, }; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/SponsorDashboardViewModel.ts b/apps/website/lib/view-models/SponsorDashboardViewModel.ts index a5c1e0109..83544bb55 100644 --- a/apps/website/lib/view-models/SponsorDashboardViewModel.ts +++ b/apps/website/lib/view-models/SponsorDashboardViewModel.ts @@ -1,19 +1,19 @@ -import { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO'; - /** * Sponsor Dashboard View Model * * Represents dashboard data for a sponsor with UI-specific transformations. */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { SponsorDashboardViewData } from "../view-data/SponsorDashboardViewData"; export class SponsorDashboardViewModel extends ViewModel { sponsorId: string; sponsorName: string; - constructor(dto: SponsorDashboardDTO) { - this.sponsorId = dto.sponsorId; - this.sponsorName = dto.sponsorName; + constructor(data: SponsorDashboardViewData) { + super(); + this.sponsorId = data.sponsorId; + this.sponsorName = data.sponsorName; } /** UI-specific: Welcome message */ diff --git a/apps/website/lib/view-models/SponsorProfileViewModel.ts b/apps/website/lib/view-models/SponsorProfileViewModel.ts new file mode 100644 index 000000000..9c5be729e --- /dev/null +++ b/apps/website/lib/view-models/SponsorProfileViewModel.ts @@ -0,0 +1,44 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { SponsorProfileViewData } from "../view-data/SponsorProfileViewData"; + +export class SponsorProfileViewModel extends ViewModel { + 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; + }; + + constructor(data: SponsorProfileViewData) { + super(); + this.companyName = data.companyName; + this.contactName = data.contactName; + this.contactEmail = data.contactEmail; + this.contactPhone = data.contactPhone; + this.website = data.website; + this.description = data.description; + this.logoUrl = data.logoUrl; + this.industry = data.industry; + this.address = data.address; + this.taxId = data.taxId; + this.socialLinks = data.socialLinks; + } + + get fullAddress(): string { + return `${this.address.street}, ${this.address.city}, ${this.address.postalCode}, ${this.address.country}`; + } +} diff --git a/apps/website/lib/view-models/SponsorSettingsViewModel.ts b/apps/website/lib/view-models/SponsorSettingsViewModel.ts index 3a847b4c4..6cd15ce17 100644 --- a/apps/website/lib/view-models/SponsorSettingsViewModel.ts +++ b/apps/website/lib/view-models/SponsorSettingsViewModel.ts @@ -4,100 +4,20 @@ * View model for sponsor settings data. */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import { SponsorProfileViewModel } from "./SponsorProfileViewModel"; +import { NotificationSettingsViewModel } from "./NotificationSettingsViewModel"; +import { PrivacySettingsViewModel } from "./PrivacySettingsViewModel"; +import type { SponsorSettingsViewData } from "../view-data/SponsorSettingsViewData"; export class SponsorSettingsViewModel extends ViewModel { profile: SponsorProfileViewModel; notifications: NotificationSettingsViewModel; privacy: PrivacySettingsViewModel; - constructor(data: { profile: unknown; notifications: unknown; privacy: unknown }) { + constructor(data: SponsorSettingsViewData) { + super(); this.profile = new SponsorProfileViewModel(data.profile); this.notifications = new NotificationSettingsViewModel(data.notifications); this.privacy = new PrivacySettingsViewModel(data.privacy); } } - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export class SponsorProfileViewModel extends ViewModel { - 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; - }; - - constructor(data: unknown) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const d = data as any; - this.companyName = d.companyName; - this.contactName = d.contactName; - this.contactEmail = d.contactEmail; - this.contactPhone = d.contactPhone; - this.website = d.website; - this.description = d.description; - this.logoUrl = d.logoUrl; - this.industry = d.industry; - this.address = d.address; - this.taxId = d.taxId; - this.socialLinks = d.socialLinks; - } - - get fullAddress(): string { - return `${this.address.street}, ${this.address.city}, ${this.address.postalCode}, ${this.address.country}`; - } -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export class NotificationSettingsViewModel extends ViewModel { - emailNewSponsorships: boolean; - emailWeeklyReport: boolean; - emailRaceAlerts: boolean; - emailPaymentAlerts: boolean; - emailNewOpportunities: boolean; - emailContractExpiry: boolean; - - constructor(data: unknown) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const d = data as any; - this.emailNewSponsorships = d.emailNewSponsorships; - this.emailWeeklyReport = d.emailWeeklyReport; - this.emailRaceAlerts = d.emailRaceAlerts; - this.emailPaymentAlerts = d.emailPaymentAlerts; - this.emailNewOpportunities = d.emailNewOpportunities; - this.emailContractExpiry = d.emailContractExpiry; - } -} - -import { ViewModel } from "../contracts/view-models/ViewModel"; - -export class PrivacySettingsViewModel extends ViewModel { - publicProfile: boolean; - showStats: boolean; - showActiveSponsorships: boolean; - allowDirectContact: boolean; - - constructor(data: unknown) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const d = data as any; - this.publicProfile = d.publicProfile; - this.showStats = d.showStats; - this.showActiveSponsorships = d.showActiveSponsorships; - this.allowDirectContact = d.allowDirectContact; - } -} \ No newline at end of file diff --git a/apps/website/lib/view-models/SponsorSponsorshipsViewModel.ts b/apps/website/lib/view-models/SponsorSponsorshipsViewModel.ts index 4e580c3ab..cd51fb6f5 100644 --- a/apps/website/lib/view-models/SponsorSponsorshipsViewModel.ts +++ b/apps/website/lib/view-models/SponsorSponsorshipsViewModel.ts @@ -1,26 +1,19 @@ -import type { SponsorSponsorshipsDTO } from '@/lib/types/generated/SponsorSponsorshipsDTO'; -import { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel'; - -/** - * Sponsor Sponsorships View Model - * - * View model for sponsor sponsorships data with UI-specific transformations. - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel'; +import type { SponsorSponsorshipsViewData } from "../view-data/SponsorSponsorshipsViewData"; export class SponsorSponsorshipsViewModel extends ViewModel { sponsorId: string; sponsorName: string; + sponsorships: SponsorshipDetailViewModel[]; - constructor(dto: SponsorSponsorshipsDTO) { - this.sponsorId = dto.sponsorId; - this.sponsorName = dto.sponsorName; + constructor(data: SponsorSponsorshipsViewData) { + super(); + this.sponsorId = data.sponsorId; + this.sponsorName = data.sponsorName; + this.sponsorships = (data.sponsorships || []).map(s => new SponsorshipDetailViewModel(s)); } - // Note: The generated DTO doesn't have sponsorships array - // This will need to be added when the OpenAPI spec is updated - sponsorships: SponsorshipDetailViewModel[] = []; - /** UI-specific: Total sponsorships count */ get totalCount(): number { return this.sponsorships.length; @@ -51,4 +44,4 @@ export class SponsorSponsorshipsViewModel extends ViewModel { const firstCurrency = this.sponsorships[0]?.currency || 'USD'; return `${firstCurrency} ${this.totalInvestment.toLocaleString()}`; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/SponsorViewModel.ts b/apps/website/lib/view-models/SponsorViewModel.ts index 8b71d52b8..5f5885d0e 100644 --- a/apps/website/lib/view-models/SponsorViewModel.ts +++ b/apps/website/lib/view-models/SponsorViewModel.ts @@ -1,26 +1,19 @@ -// Note: No generated DTO available for Sponsor yet -interface SponsorDTO { - id: string; - name: string; - logoUrl?: string; - websiteUrl?: string; -} - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { SponsorViewData } from "../view-data/SponsorViewData"; export class SponsorViewModel extends ViewModel { - id: string; - name: string; - declare logoUrl?: string; - declare websiteUrl?: string; + private readonly data: SponsorViewData; - constructor(dto: SponsorDTO) { - this.id = dto.id; - this.name = dto.name; - if (dto.logoUrl !== undefined) this.logoUrl = dto.logoUrl; - if (dto.websiteUrl !== undefined) this.websiteUrl = dto.websiteUrl; + constructor(data: SponsorViewData) { + super(); + this.data = data; } + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get logoUrl(): string | undefined { return this.data.logoUrl; } + get websiteUrl(): string | undefined { return this.data.websiteUrl; } + /** UI-specific: Display name */ get displayName(): string { return this.name; @@ -35,4 +28,4 @@ export class SponsorViewModel extends ViewModel { get websiteLinkText(): string { return 'Visit Website'; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/SponsorshipDetailViewModel.ts b/apps/website/lib/view-models/SponsorshipDetailViewModel.ts index a6c8e56c1..fa17da981 100644 --- a/apps/website/lib/view-models/SponsorshipDetailViewModel.ts +++ b/apps/website/lib/view-models/SponsorshipDetailViewModel.ts @@ -1,6 +1,6 @@ -import { SponsorshipDetailDTO } from '@/lib/types/generated/SponsorshipDetailDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import type { SponsorshipDetailViewData } from "../view-data/SponsorshipDetailViewData"; export class SponsorshipDetailViewModel extends ViewModel { id: string; @@ -8,29 +8,35 @@ export class SponsorshipDetailViewModel extends ViewModel { leagueName: string; seasonId: string; seasonName: string; + tier: 'main' | 'secondary'; + status: string; + amount: number; + currency: string; + type: string; + entityName: string; + price: number; + impressions: number; - constructor(dto: SponsorshipDetailDTO) { - this.id = dto.id; - this.leagueId = dto.leagueId; - this.leagueName = dto.leagueName; - this.seasonId = dto.seasonId; - this.seasonName = dto.seasonName; + constructor(data: SponsorshipDetailViewData) { + super(); + this.id = data.id; + this.leagueId = data.leagueId; + this.leagueName = data.leagueName; + this.seasonId = data.seasonId; + this.seasonName = data.seasonName; + this.tier = data.tier; + this.status = data.status; + this.amount = data.amount; + this.currency = data.currency; + this.type = data.type; + this.entityName = data.entityName; + this.price = data.price; + this.impressions = data.impressions; } - // Note: The generated DTO is incomplete - // These fields will need to be added when the OpenAPI spec is updated - tier: 'main' | 'secondary' = 'secondary'; - status: string = 'active'; - amount: number = 0; - currency: string = 'USD'; - type: string = 'league'; - entityName: string = ''; - price: number = 0; - impressions: number = 0; - /** UI-specific: Formatted amount */ get formattedAmount(): string { - return `${this.currency} ${this.amount.toLocaleString()}`; + return CurrencyDisplay.format(this.amount, this.currency); } /** UI-specific: Tier badge variant */ @@ -52,4 +58,4 @@ export class SponsorshipDetailViewModel extends ViewModel { get statusDisplay(): string { return this.status.charAt(0).toUpperCase() + this.status.slice(1); } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/SponsorshipPricingViewModel.ts b/apps/website/lib/view-models/SponsorshipPricingViewModel.ts index 25e920a91..d0e109688 100644 --- a/apps/website/lib/view-models/SponsorshipPricingViewModel.ts +++ b/apps/website/lib/view-models/SponsorshipPricingViewModel.ts @@ -1,36 +1,27 @@ -// Note: No generated DTO available for SponsorshipPricing yet -interface SponsorshipPricingDTO { - mainSlotPrice: number; - secondarySlotPrice: number; - currency: string; -} - -/** - * Sponsorship Pricing View Model - * - * View model for sponsorship pricing data with UI-specific transformations. - */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import type { SponsorshipPricingViewData } from "../view-data/SponsorshipPricingViewData"; export class SponsorshipPricingViewModel extends ViewModel { mainSlotPrice: number; secondarySlotPrice: number; currency: string; - constructor(dto: SponsorshipPricingDTO) { - this.mainSlotPrice = dto.mainSlotPrice; - this.secondarySlotPrice = dto.secondarySlotPrice; - this.currency = dto.currency; + constructor(data: SponsorshipPricingViewData) { + super(); + this.mainSlotPrice = data.mainSlotPrice; + this.secondarySlotPrice = data.secondarySlotPrice; + this.currency = data.currency; } /** UI-specific: Formatted main slot price */ get formattedMainSlotPrice(): string { - return `${this.currency} ${this.mainSlotPrice.toLocaleString()}`; + return CurrencyDisplay.format(this.mainSlotPrice, this.currency); } /** UI-specific: Formatted secondary slot price */ get formattedSecondarySlotPrice(): string { - return `${this.currency} ${this.secondarySlotPrice.toLocaleString()}`; + return CurrencyDisplay.format(this.secondarySlotPrice, this.currency); } /** UI-specific: Price difference */ @@ -40,7 +31,7 @@ export class SponsorshipPricingViewModel extends ViewModel { /** UI-specific: Formatted price difference */ get formattedPriceDifference(): string { - return `${this.currency} ${this.priceDifference.toLocaleString()}`; + return CurrencyDisplay.format(this.priceDifference, this.currency); } /** UI-specific: Discount percentage for secondary slot */ @@ -48,4 +39,4 @@ export class SponsorshipPricingViewModel extends ViewModel { if (this.mainSlotPrice === 0) return 0; return Math.round((1 - this.secondarySlotPrice / this.mainSlotPrice) * 100); } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/SponsorshipRequestViewModel.ts b/apps/website/lib/view-models/SponsorshipRequestViewModel.ts index e037405c8..6b3ca03b9 100644 --- a/apps/website/lib/view-models/SponsorshipRequestViewModel.ts +++ b/apps/website/lib/view-models/SponsorshipRequestViewModel.ts @@ -1,6 +1,7 @@ -import type { SponsorshipRequestDTO } from '@/lib/types/generated/SponsorshipRequestDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import type { SponsorshipRequestViewData } from "../view-data/SponsorshipRequestViewData"; export class SponsorshipRequestViewModel extends ViewModel { id: string; @@ -16,33 +17,30 @@ export class SponsorshipRequestViewModel extends ViewModel { platformFee: number; netAmount: number; - constructor(dto: SponsorshipRequestDTO) { - this.id = dto.id; - this.sponsorId = dto.sponsorId; - this.sponsorName = dto.sponsorName; - if (dto.sponsorLogo !== undefined) this.sponsorLogo = dto.sponsorLogo; - // Backend currently returns tier as string; normalize to our supported tiers. - this.tier = dto.tier === 'main' ? 'main' : 'secondary'; - this.offeredAmount = dto.offeredAmount; - this.currency = dto.currency; - this.formattedAmount = dto.formattedAmount; - if (dto.message !== undefined) this.message = dto.message; - this.createdAt = new Date(dto.createdAt); - this.platformFee = dto.platformFee; - this.netAmount = dto.netAmount; + constructor(data: SponsorshipRequestViewData) { + super(); + this.id = data.id; + this.sponsorId = data.sponsorId; + this.sponsorName = data.sponsorName; + this.sponsorLogo = data.sponsorLogo; + this.tier = data.tier; + this.offeredAmount = data.offeredAmount; + this.currency = data.currency; + this.formattedAmount = data.formattedAmount; + this.message = data.message; + this.createdAt = new Date(data.createdAt); + this.platformFee = data.platformFee; + this.netAmount = data.netAmount; } /** UI-specific: Formatted date */ get formattedDate(): string { - return this.createdAt.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - }); + return DateDisplay.formatMonthDay(this.createdAt); } /** UI-specific: Net amount in dollars */ get netAmountDollars(): string { - return `$${(this.netAmount / 100).toFixed(2)}`; + return CurrencyDisplay.format(this.netAmount / 100, 'USD'); } /** UI-specific: Tier display */ diff --git a/apps/website/lib/view-models/SponsorshipViewModel.ts b/apps/website/lib/view-models/SponsorshipViewModel.ts index db657a965..9e3e938a5 100644 --- a/apps/website/lib/view-models/SponsorshipViewModel.ts +++ b/apps/website/lib/view-models/SponsorshipViewModel.ts @@ -1,38 +1,14 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; import { CurrencyDisplay } from '../display-objects/CurrencyDisplay'; import { DateDisplay } from '../display-objects/DateDisplay'; import { NumberDisplay } from '../display-objects/NumberDisplay'; - -/** - * Interface for sponsorship data input - */ -export interface SponsorshipDataInput { - 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; -} +import type { SponsorshipViewData } from "../view-data/SponsorshipViewData"; /** * Sponsorship View Model * * View model for individual sponsorship data. */ -import { ViewModel } from "../contracts/view-models/ViewModel"; - export class SponsorshipViewModel extends ViewModel { id: string; type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform'; @@ -53,7 +29,8 @@ export class SponsorshipViewModel extends ViewModel { entityOwner?: string; applicationMessage?: string; - constructor(data: SponsorshipDataInput) { + constructor(data: SponsorshipViewData) { + super(); this.id = data.id; this.type = data.type; this.entityId = data.entityId; @@ -119,4 +96,4 @@ export class SponsorshipViewModel extends ViewModel { const end = DateDisplay.formatMonthYear(this.endDate); return `${start} - ${end}`; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/StandingEntryViewModel.ts b/apps/website/lib/view-models/StandingEntryViewModel.ts index 8d53fd99d..c160088b7 100644 --- a/apps/website/lib/view-models/StandingEntryViewModel.ts +++ b/apps/website/lib/view-models/StandingEntryViewModel.ts @@ -1,62 +1,48 @@ -import { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { FinishDisplay } from "../display-objects/FinishDisplay"; +import type { StandingEntryViewData } from "../view-data/StandingEntryViewData"; export class StandingEntryViewModel extends ViewModel { - driverId: string; - position: number; - points: number; - wins: number; - podiums: number; - races: number; + private readonly data: StandingEntryViewData; - private leaderPoints: number; - private nextPoints: number; - private currentUserId: string; - private previousPosition?: number; - - constructor(dto: LeagueStandingDTO, leaderPoints: number, nextPoints: number, currentUserId: string, previousPosition?: number) { - this.driverId = dto.driverId; - this.position = dto.position; - this.points = dto.points; - this.wins = dto.wins ?? 0; - this.podiums = dto.podiums ?? 0; - this.races = dto.races ?? 0; - this.leaderPoints = leaderPoints; - this.nextPoints = nextPoints; - this.currentUserId = currentUserId; - this.previousPosition = previousPosition; + constructor(data: StandingEntryViewData) { + super(); + this.data = data; } + get driverId(): string { return this.data.driverId; } + get position(): number { return this.data.position; } + get points(): number { return this.data.points; } + get wins(): number { return this.data.wins; } + get podiums(): number { return this.data.podiums; } + get races(): number { return this.data.races; } + get driver(): any { return this.data.driver; } + /** UI-specific: Badge for position display */ get positionBadge(): string { - return this.position.toString(); + return FinishDisplay.format(this.position); } - // Note: The generated DTO is incomplete - // These fields will need to be added when the OpenAPI spec is updated - driver?: any; - /** UI-specific: Points difference to leader */ get pointsGapToLeader(): number { - return this.points - this.leaderPoints; + return this.points - this.data.leaderPoints; } /** UI-specific: Points difference to next position */ get pointsGapToNext(): number { - return this.points - this.nextPoints; + return this.points - this.data.nextPoints; } /** UI-specific: Whether this entry is the current user */ get isCurrentUser(): boolean { - return this.driverId === this.currentUserId; + return this.driverId === this.data.currentUserId; } /** UI-specific: Trend compared to previous */ get trend(): 'up' | 'down' | 'same' { - if (!this.previousPosition) return 'same'; - if (this.position < this.previousPosition) return 'up'; - if (this.position > this.previousPosition) return 'down'; + if (!this.data.previousPosition) return 'same'; + if (this.position < this.data.previousPosition) return 'up'; + if (this.position > this.data.previousPosition) return 'down'; return 'same'; } diff --git a/apps/website/lib/view-models/TeamCardViewModel.ts b/apps/website/lib/view-models/TeamCardViewModel.ts index d5c4be567..9fac4ba45 100644 --- a/apps/website/lib/view-models/TeamCardViewModel.ts +++ b/apps/website/lib/view-models/TeamCardViewModel.ts @@ -1,18 +1,9 @@ -import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO'; - -interface TeamCardDTO { - id: string; - name: string; - tag: string; - description: string; - logoUrl?: string; -} - /** * Team card view model * UI representation of a team on the landing page. */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { TeamCardViewData } from "@/lib/view-data/TeamCardViewData"; export class TeamCardViewModel extends ViewModel { readonly id: string; @@ -21,11 +12,12 @@ export class TeamCardViewModel extends ViewModel { readonly description: string; readonly logoUrl?: string; - constructor(dto: TeamCardDTO | TeamListItemDTO) { - this.id = dto.id; - this.name = dto.name; - this.tag = dto.tag; - this.description = dto.description; - this.logoUrl = 'logoUrl' in dto ? dto.logoUrl : undefined; + constructor(data: TeamCardViewData) { + super(); + this.id = data.id; + this.name = data.name; + this.tag = data.tag; + this.description = data.description; + this.logoUrl = data.logoUrl; } } diff --git a/apps/website/lib/view-models/TeamDetailsViewModel.ts b/apps/website/lib/view-models/TeamDetailsViewModel.ts index e7b4bec2b..f10340086 100644 --- a/apps/website/lib/view-models/TeamDetailsViewModel.ts +++ b/apps/website/lib/view-models/TeamDetailsViewModel.ts @@ -1,52 +1,28 @@ -import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { TeamDetailsViewData } from "../view-data/TeamDetailsViewData"; export class TeamDetailsViewModel extends ViewModel { - id!: string; - name!: string; - tag!: string; - description?: string; - ownerId!: string; - leagues!: string[]; - createdAt: string | undefined; - specialization: string | undefined; - region: string | undefined; - languages: string[] | undefined; - category: string | undefined; - membership: { role: string; joinedAt: string; isActive: boolean } | null; - private _canManage: boolean; - private currentUserId: string; + private readonly data: TeamDetailsViewData; - constructor(dto: GetTeamDetailsOutputDTO, currentUserId: string) { - this.id = dto.team.id; - this.name = dto.team.name; - this.tag = dto.team.tag; - this.description = dto.team.description; - this.ownerId = dto.team.ownerId; - this.leagues = dto.team.leagues; - this.createdAt = dto.team.createdAt; - - const teamExtras = dto.team as typeof dto.team & { - specialization?: string; - region?: string; - languages?: string[]; - category?: string; - }; - - this.specialization = teamExtras.specialization ?? undefined; - this.region = teamExtras.region ?? undefined; - this.languages = teamExtras.languages ?? undefined; - this.category = teamExtras.category ?? undefined; - this.membership = dto.membership ? { - role: dto.membership.role, - joinedAt: dto.membership.joinedAt, - isActive: dto.membership.isActive - } : null; - this._canManage = dto.canManage; - this.currentUserId = currentUserId; + constructor(data: TeamDetailsViewData) { + super(); + this.data = data; } + get id(): string { return this.data.team.id; } + get name(): string { return this.data.team.name; } + get tag(): string { return this.data.team.tag; } + get description(): string | undefined { return this.data.team.description; } + get ownerId(): string { return this.data.team.ownerId; } + get leagues(): string[] { return this.data.team.leagues; } + get createdAt(): string | undefined { return this.data.team.createdAt; } + get specialization(): string | undefined { return this.data.team.specialization; } + get region(): string | undefined { return this.data.team.region; } + get languages(): string[] | undefined { return this.data.team.languages; } + get category(): string | undefined { return this.data.team.category; } + get membership() { return this.data.membership; } + get currentUserId(): string { return this.data.currentUserId; } + /** UI-specific: Whether current user is owner */ get isOwner(): boolean { return this.membership?.role === 'owner'; @@ -54,7 +30,7 @@ export class TeamDetailsViewModel extends ViewModel { /** UI-specific: Whether can manage team */ get canManage(): boolean { - return this._canManage; + return this.data.canManage; } /** UI-specific: Whether current user is member */ @@ -66,4 +42,4 @@ export class TeamDetailsViewModel extends ViewModel { get userRole(): string { return this.membership?.role || 'none'; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/TeamJoinRequestViewModel.ts b/apps/website/lib/view-models/TeamJoinRequestViewModel.ts index 1f7ce440f..56dc6761c 100644 --- a/apps/website/lib/view-models/TeamJoinRequestViewModel.ts +++ b/apps/website/lib/view-models/TeamJoinRequestViewModel.ts @@ -1,34 +1,25 @@ -import type { TeamJoinRequestDTO } from '@/lib/types/generated/TeamJoinRequestDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import type { TeamJoinRequestViewData } from "../view-data/TeamJoinRequestViewData"; export class TeamJoinRequestViewModel extends ViewModel { - requestId: string; - driverId: string; - driverName: string; - teamId: string; - requestStatus: string; - requestedAt: string; - avatarUrl: string; + private readonly data: TeamJoinRequestViewData; - private readonly currentUserId: string; - private readonly isOwner: boolean; - - constructor(dto: TeamJoinRequestDTO, currentUserId: string, isOwner: boolean) { - this.requestId = dto.requestId; - this.driverId = dto.driverId; - this.driverName = dto.driverName; - this.teamId = dto.teamId; - this.requestStatus = dto.status; - this.requestedAt = dto.requestedAt; - this.avatarUrl = dto.avatarUrl || ''; - this.currentUserId = currentUserId; - this.isOwner = isOwner; + constructor(data: TeamJoinRequestViewData) { + super(); + this.data = data; } - get id(): string { - return this.requestId; - } + get id(): string { return this.data.requestId; } + get requestId(): string { return this.data.requestId; } + get driverId(): string { return this.data.driverId; } + get driverName(): string { return this.data.driverName; } + get teamId(): string { return this.data.teamId; } + get requestStatus(): string { return this.data.status; } + get requestedAt(): string { return this.data.requestedAt; } + get avatarUrl(): string { return this.data.avatarUrl || ''; } + get currentUserId(): string { return this.data.currentUserId; } + get isOwner(): boolean { return this.data.isOwner; } get status(): string { if (this.requestStatus === 'pending') return 'Pending'; @@ -44,7 +35,7 @@ export class TeamJoinRequestViewModel extends ViewModel { /** UI-specific: Formatted requested date */ get formattedRequestedAt(): string { - return new Date(this.requestedAt).toLocaleString(); + return DateDisplay.formatDateTime(this.requestedAt); } /** UI-specific: Status color */ diff --git a/apps/website/lib/view-models/TeamMemberViewModel.ts b/apps/website/lib/view-models/TeamMemberViewModel.ts index 1ccf41ac2..ea4f305a6 100644 --- a/apps/website/lib/view-models/TeamMemberViewModel.ts +++ b/apps/website/lib/view-models/TeamMemberViewModel.ts @@ -1,6 +1,6 @@ -import type { TeamMemberDTO } from '@/lib/types/generated/TeamMemberDTO'; - -type TeamMemberRole = 'owner' | 'manager' | 'member'; +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import type { TeamMemberViewData, TeamMemberRole } from "../view-data/TeamMemberViewData"; function normalizeTeamRole(role: string): TeamMemberRole { if (role === 'owner' || role === 'manager' || role === 'member') return role; @@ -9,30 +9,23 @@ function normalizeTeamRole(role: string): TeamMemberRole { return 'member'; } -import { ViewModel } from "../contracts/view-models/ViewModel"; - export class TeamMemberViewModel extends ViewModel { - driverId: string; - driverName: string; - role: TeamMemberRole; - joinedAt: string; - isActive: boolean; - avatarUrl: string; + private readonly data: TeamMemberViewData; - private currentUserId: string; - private teamOwnerId: string; - - constructor(dto: TeamMemberDTO, currentUserId: string, teamOwnerId: string) { - this.driverId = dto.driverId; - this.driverName = dto.driverName; - this.role = normalizeTeamRole(dto.role); - this.joinedAt = dto.joinedAt; - this.isActive = dto.isActive; - this.avatarUrl = dto.avatarUrl || ''; - this.currentUserId = currentUserId; - this.teamOwnerId = teamOwnerId; + constructor(data: TeamMemberViewData) { + super(); + this.data = data; } + get driverId(): string { return this.data.driverId; } + get driverName(): string { return this.data.driverName; } + get role(): TeamMemberRole { return normalizeTeamRole(this.data.role); } + get joinedAt(): string { return this.data.joinedAt; } + get isActive(): boolean { return this.data.isActive; } + get avatarUrl(): string { return this.data.avatarUrl || ''; } + get currentUserId(): string { return this.data.currentUserId; } + get teamOwnerId(): string { return this.data.teamOwnerId; } + /** UI-specific: Role badge variant */ get roleBadgeVariant(): string { switch (this.role) { @@ -60,6 +53,6 @@ export class TeamMemberViewModel extends ViewModel { /** UI-specific: Formatted joined date */ get formattedJoinedAt(): string { - return new Date(this.joinedAt).toLocaleDateString(); + return DateDisplay.formatShort(this.joinedAt); } } diff --git a/apps/website/lib/view-models/TeamSummaryViewModel.ts b/apps/website/lib/view-models/TeamSummaryViewModel.ts index ead7affbe..c9962dc6c 100644 --- a/apps/website/lib/view-models/TeamSummaryViewModel.ts +++ b/apps/website/lib/view-models/TeamSummaryViewModel.ts @@ -1,48 +1,32 @@ -import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { TeamSummaryViewData } from "../view-data/TeamSummaryViewData"; export class TeamSummaryViewModel extends ViewModel { - id: string; - name: string; - tag: string; - memberCount: number; - description?: string; - totalWins: number = 0; - totalRaces: number = 0; - performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro' = 'intermediate'; - isRecruiting: boolean = false; - specialization: 'endurance' | 'sprint' | 'mixed' | undefined; - region: string | undefined; - languages: string[] = []; - leagues: string[] = []; - logoUrl: string | undefined; - rating: number | undefined; - category: string | undefined; + private readonly data: TeamSummaryViewData; + private readonly maxMembers = 10; // Assuming max members - private maxMembers = 10; // Assuming max members - - constructor(dto: TeamListItemDTO) { - this.id = dto.id; - this.name = dto.name; - this.tag = dto.tag; - this.memberCount = dto.memberCount; - this.description = dto.description; - this.specialization = dto.specialization as 'endurance' | 'sprint' | 'mixed' | undefined; - this.region = dto.region; - this.languages = dto.languages ?? []; - this.leagues = dto.leagues; - - // Map stats fields from DTO - this.totalWins = dto.totalWins ?? 0; - this.totalRaces = dto.totalRaces ?? 0; - this.performanceLevel = (dto.performanceLevel as 'beginner' | 'intermediate' | 'advanced' | 'pro') ?? 'intermediate'; - this.logoUrl = dto.logoUrl; - this.rating = dto.rating; - this.category = dto.category; - this.isRecruiting = dto.isRecruiting ?? false; + constructor(data: TeamSummaryViewData) { + super(); + this.data = data; } + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get tag(): string { return this.data.tag; } + get memberCount(): number { return this.data.memberCount; } + get description(): string | undefined { return this.data.description; } + get totalWins(): number { return this.data.totalWins; } + get totalRaces(): number { return this.data.totalRaces; } + get performanceLevel(): string { return this.data.performanceLevel; } + get isRecruiting(): boolean { return this.data.isRecruiting; } + get specialization(): string | undefined { return this.data.specialization; } + get region(): string | undefined { return this.data.region; } + get languages(): string[] { return this.data.languages; } + get leagues(): string[] { return this.data.leagues; } + get logoUrl(): string | undefined { return this.data.logoUrl; } + get rating(): number | undefined { return this.data.rating; } + get category(): string | undefined { return this.data.category; } + /** UI-specific: Whether team is full */ get isFull(): boolean { return this.memberCount >= this.maxMembers; diff --git a/apps/website/lib/view-models/UpcomingRaceCardViewModel.ts b/apps/website/lib/view-models/UpcomingRaceCardViewModel.ts index 0fdb1af02..80be3bf8b 100644 --- a/apps/website/lib/view-models/UpcomingRaceCardViewModel.ts +++ b/apps/website/lib/view-models/UpcomingRaceCardViewModel.ts @@ -1,34 +1,27 @@ -interface UpcomingRaceCardDTO { - id: string; - track: string; - car: string; - scheduledAt: string; -} +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import type { UpcomingRaceCardViewData } from "../view-data/UpcomingRaceCardViewData"; /** * Upcoming race card view model * UI representation of an upcoming race on the landing page. */ -import { ViewModel } from "../contracts/view-models/ViewModel"; - export class UpcomingRaceCardViewModel extends ViewModel { readonly id: string; readonly track: string; readonly car: string; readonly scheduledAt: string; - constructor(dto: UpcomingRaceCardDTO) { - this.id = dto.id; - this.track = dto.track; - this.car = dto.car; - this.scheduledAt = dto.scheduledAt; + constructor(data: UpcomingRaceCardViewData) { + super(); + this.id = data.id; + this.track = data.track; + this.car = data.car; + this.scheduledAt = data.scheduledAt; } /** UI-specific: formatted date label */ get formattedDate(): string { - return new Date(this.scheduledAt).toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - }); + return DateDisplay.formatMonthDay(this.scheduledAt); } } diff --git a/apps/website/lib/view-models/UpdateAvatarViewModel.ts b/apps/website/lib/view-models/UpdateAvatarViewModel.ts index 67045b7fe..dfe55ba61 100644 --- a/apps/website/lib/view-models/UpdateAvatarViewModel.ts +++ b/apps/website/lib/view-models/UpdateAvatarViewModel.ts @@ -1,23 +1,19 @@ -// Note: No generated DTO available for UpdateAvatar yet -interface UpdateAvatarDTO { - success: boolean; - error?: string; -} - /** * Update Avatar View Model * * Represents the result of an avatar update operation */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { UpdateAvatarViewData } from "../view-data/UpdateAvatarViewData"; export class UpdateAvatarViewModel extends ViewModel { success: boolean; error?: string; - constructor(dto: UpdateAvatarDTO) { - this.success = dto.success; - if (dto.error !== undefined) this.error = dto.error; + constructor(data: UpdateAvatarViewData) { + super(); + this.success = data.success; + this.error = data.error; } /** UI-specific: Whether update was successful */ @@ -29,4 +25,4 @@ export class UpdateAvatarViewModel extends ViewModel { get hasError(): boolean { return !!this.error; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/UpdateTeamViewModel.ts b/apps/website/lib/view-models/UpdateTeamViewModel.ts index 8fbb05b09..f5277d33f 100644 --- a/apps/website/lib/view-models/UpdateTeamViewModel.ts +++ b/apps/website/lib/view-models/UpdateTeamViewModel.ts @@ -4,16 +4,18 @@ * Represents the result of updating a team in a UI-ready format. */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { UpdateTeamViewData } from "../view-data/UpdateTeamViewData"; export class UpdateTeamViewModel extends ViewModel { success: boolean; - constructor(dto: { success: boolean }) { - this.success = dto.success; + constructor(data: UpdateTeamViewData) { + super(); + this.success = data.success; } /** UI-specific: Success message */ get successMessage(): string { return this.success ? 'Team updated successfully!' : 'Failed to update team.'; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/UploadMediaViewModel.ts b/apps/website/lib/view-models/UploadMediaViewModel.ts index b815586bd..0afb7e5c7 100644 --- a/apps/website/lib/view-models/UploadMediaViewModel.ts +++ b/apps/website/lib/view-models/UploadMediaViewModel.ts @@ -1,17 +1,10 @@ -// Note: No generated DTO available for UploadMedia yet -interface UploadMediaDTO { - success: boolean; - mediaId?: string; - url?: string; - error?: string; -} - /** * Upload Media View Model * * Represents the result of a media upload operation */ import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { UploadMediaViewData } from "../view-data/UploadMediaViewData"; export class UploadMediaViewModel extends ViewModel { success: boolean; @@ -19,11 +12,12 @@ export class UploadMediaViewModel extends ViewModel { url?: string; error?: string; - constructor(dto: UploadMediaDTO) { - this.success = dto.success; - if (dto.mediaId !== undefined) this.mediaId = dto.mediaId; - if (dto.url !== undefined) this.url = dto.url; - if (dto.error !== undefined) this.error = dto.error; + constructor(data: UploadMediaViewData) { + super(); + this.success = data.success; + this.mediaId = data.mediaId; + this.url = data.url; + this.error = data.error; } /** UI-specific: Whether upload was successful */ @@ -35,4 +29,4 @@ export class UploadMediaViewModel extends ViewModel { get hasError(): boolean { return !!this.error; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/UserListViewModel.ts b/apps/website/lib/view-models/UserListViewModel.ts new file mode 100644 index 000000000..a339b6d65 --- /dev/null +++ b/apps/website/lib/view-models/UserListViewModel.ts @@ -0,0 +1,43 @@ +import { ViewModel } from "../contracts/view-models/ViewModel"; +import { AdminUserViewModel } from "./AdminUserViewModel"; +import type { AdminUserViewData } from '@/lib/view-data/AdminUserViewData'; + +/** + * 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; + } +} diff --git a/apps/website/lib/view-models/UserProfileViewModel.ts b/apps/website/lib/view-models/UserProfileViewModel.ts index 64608674e..bd6c948ab 100644 --- a/apps/website/lib/view-models/UserProfileViewModel.ts +++ b/apps/website/lib/view-models/UserProfileViewModel.ts @@ -1,29 +1,20 @@ -// Note: No generated DTO available for UserProfile yet -interface UserProfileDTO { - id: string; - name: string; - avatarUrl?: string; - iracingId?: string; - rating?: number; -} - import { ViewModel } from "../contracts/view-models/ViewModel"; +import type { UserProfileViewData } from "../view-data/UserProfileViewData"; export class UserProfileViewModel extends ViewModel { - id: string; - name: string; - avatarUrl?: string; - iracingId?: string; - rating?: number; + private readonly data: UserProfileViewData; - constructor(dto: UserProfileDTO) { - this.id = dto.id; - this.name = dto.name; - if (dto.avatarUrl !== undefined) this.avatarUrl = dto.avatarUrl; - if (dto.iracingId !== undefined) this.iracingId = dto.iracingId; - if (dto.rating !== undefined) this.rating = dto.rating; + constructor(data: UserProfileViewData) { + super(); + this.data = data; } + get id(): string { return this.data.id; } + get name(): string { return this.data.name; } + get avatarUrl(): string | undefined { return this.data.avatarUrl; } + get iracingId(): string | undefined { return this.data.iracingId; } + get rating(): number | undefined { return this.data.rating; } + /** UI-specific: Formatted rating */ get formattedRating(): string { return this.rating ? this.rating.toFixed(0) : 'Unrated'; @@ -47,4 +38,4 @@ export class UserProfileViewModel extends ViewModel { get avatarInitials(): string { return this.name.split(' ').map(n => n[0]).join('').toUpperCase(); } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/WalletTransactionViewModel.ts b/apps/website/lib/view-models/WalletTransactionViewModel.ts index e4c4fb129..4f9b65e4c 100644 --- a/apps/website/lib/view-models/WalletTransactionViewModel.ts +++ b/apps/website/lib/view-models/WalletTransactionViewModel.ts @@ -1,46 +1,31 @@ -// Export the DTO type that WalletTransactionViewModel expects -export type FullTransactionDto = { - id: string; - type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize' | 'deposit'; - description: string; - amount: number; - fee: number; - netAmount: number; - date: Date; - status: 'completed' | 'pending' | 'failed'; - reference?: string; -}; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import { DateDisplay } from "../display-objects/DateDisplay"; +import { TransactionTypeDisplay } from "../display-objects/TransactionTypeDisplay"; +import type { WalletTransactionViewData } from "../view-data/WalletTransactionViewData"; export class WalletTransactionViewModel extends ViewModel { - id: string; - type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize' | 'deposit'; - description: string; - amount: number; - fee: number; - netAmount: number; - date: Date; - status: 'completed' | 'pending' | 'failed'; - reference?: string; + private readonly data: WalletTransactionViewData; - constructor(dto: FullTransactionDto) { + constructor(data: WalletTransactionViewData) { super(); - this.id = dto.id; - this.type = dto.type; - this.description = dto.description; - this.amount = dto.amount; - this.fee = dto.fee; - this.netAmount = dto.netAmount; - this.date = dto.date; - this.status = dto.status; - this.reference = dto.reference; + this.data = data; } + get id(): string { return this.data.id; } + get type(): string { return this.data.type; } + get description(): string { return this.data.description; } + get amount(): number { return this.data.amount; } + get fee(): number { return this.data.fee; } + get netAmount(): number { return this.data.netAmount; } + get date(): string { return this.data.date; } + get status(): string { return this.data.status; } + get reference(): string | undefined { return this.data.reference; } + /** UI-specific: Formatted amount with sign */ get formattedAmount(): string { const sign = this.amount > 0 ? '+' : ''; - return `${sign}$${Math.abs(this.amount).toFixed(2)}`; + return `${sign}${CurrencyDisplay.format(Math.abs(this.amount))}`; } /** UI-specific: Amount color */ @@ -50,16 +35,16 @@ export class WalletTransactionViewModel extends ViewModel { /** UI-specific: Type display */ get typeDisplay(): string { - return this.type.charAt(0).toUpperCase() + this.type.slice(1); + return TransactionTypeDisplay.format(this.type); } /** UI-specific: Formatted date */ get formattedDate(): string { - return this.date.toLocaleDateString(); + return DateDisplay.formatShort(this.date); } /** UI-specific: Is incoming */ get isIncoming(): boolean { return this.amount > 0; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/WalletViewModel.ts b/apps/website/lib/view-models/WalletViewModel.ts index 3854919a2..d05c3672c 100644 --- a/apps/website/lib/view-models/WalletViewModel.ts +++ b/apps/website/lib/view-models/WalletViewModel.ts @@ -1,37 +1,30 @@ -import { WalletDTO } from '@/lib/types/generated/WalletDTO'; import { WalletTransactionViewModel } from './WalletTransactionViewModel'; - import { ViewModel } from "../contracts/view-models/ViewModel"; +import { CurrencyDisplay } from "../display-objects/CurrencyDisplay"; +import type { WalletViewData } from "../view-data/WalletViewData"; export class WalletViewModel extends ViewModel { - id: string; - leagueId: string; - balance: number; - totalRevenue: number; - totalPlatformFees: number; - totalWithdrawn: number; - createdAt: string; - currency: string; + private readonly data: WalletViewData; + readonly transactions: WalletTransactionViewModel[]; - constructor(dto: WalletDTO & { transactions?: any[] }) { - this.id = dto.id; - this.leagueId = dto.leagueId; - this.balance = dto.balance; - this.totalRevenue = dto.totalRevenue; - this.totalPlatformFees = dto.totalPlatformFees; - this.totalWithdrawn = dto.totalWithdrawn; - this.createdAt = dto.createdAt; - this.currency = dto.currency; - - // Map transactions if provided - this.transactions = dto.transactions?.map(t => new WalletTransactionViewModel(t)) || []; + constructor(data: WalletViewData) { + super(); + this.data = data; + this.transactions = data.transactions?.map(t => new WalletTransactionViewModel(t)) || []; } - transactions: WalletTransactionViewModel[] = []; + get id(): string { return this.data.id; } + get leagueId(): string { return this.data.leagueId; } + get balance(): number { return this.data.balance; } + get totalRevenue(): number { return this.data.totalRevenue; } + get totalPlatformFees(): number { return this.data.totalPlatformFees; } + get totalWithdrawn(): number { return this.data.totalWithdrawn; } + get createdAt(): string { return this.data.createdAt; } + get currency(): string { return this.data.currency; } /** UI-specific: Formatted balance */ get formattedBalance(): string { - return `${this.currency} ${this.balance.toFixed(2)}`; + return CurrencyDisplay.format(this.balance, this.currency); } /** UI-specific: Balance color */ @@ -48,4 +41,4 @@ export class WalletViewModel extends ViewModel { get totalTransactions(): number { return this.transactions.length; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/auth/ForgotPasswordInterfaces.ts b/apps/website/lib/view-models/auth/ForgotPasswordInterfaces.ts new file mode 100644 index 000000000..fa7a99d49 --- /dev/null +++ b/apps/website/lib/view-models/auth/ForgotPasswordInterfaces.ts @@ -0,0 +1,16 @@ +export interface ForgotPasswordFormField { + value: string; + error?: string; + touched: boolean; + validating: boolean; +} + +export interface ForgotPasswordFormState { + fields: { + email: ForgotPasswordFormField; + }; + isValid: boolean; + isSubmitting: boolean; + submitError?: string; + submitCount: number; +} diff --git a/apps/website/lib/view-models/auth/ForgotPasswordViewModel.ts b/apps/website/lib/view-models/auth/ForgotPasswordViewModel.ts index bf0b542a4..8fa103b40 100644 --- a/apps/website/lib/view-models/auth/ForgotPasswordViewModel.ts +++ b/apps/website/lib/view-models/auth/ForgotPasswordViewModel.ts @@ -1,28 +1,13 @@ +import { ViewModel } from "../../contracts/view-models/ViewModel"; +import type { ForgotPasswordFormState } from "./ForgotPasswordInterfaces"; + /** * Forgot Password ViewModel * * Client-side state management for forgot password flow. * Immutable, class-based, contains only UI state. */ - -export interface ForgotPasswordFormField { - value: string; - error?: string; - touched: boolean; - validating: boolean; -} - -export interface ForgotPasswordFormState { - fields: { - email: ForgotPasswordFormField; - }; - isValid: boolean; - isSubmitting: boolean; - submitError?: string; - submitCount: number; -} - -export class ForgotPasswordViewModel { +export class ForgotPasswordViewModel extends ViewModel { constructor( public readonly returnTo: string, public readonly formState: ForgotPasswordFormState, @@ -31,7 +16,9 @@ export class ForgotPasswordViewModel { public readonly magicLink: string | null = null, public readonly mutationPending: boolean = false, public readonly mutationError: string | null = null - ) {} + ) { + super(); + } withFormState(formState: ForgotPasswordFormState): ForgotPasswordViewModel { return new ForgotPasswordViewModel( @@ -76,4 +63,4 @@ export class ForgotPasswordViewModel { get submitError(): string | undefined { return this.formState.submitError || this.mutationError || undefined; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/auth/LoginInterfaces.ts b/apps/website/lib/view-models/auth/LoginInterfaces.ts new file mode 100644 index 000000000..7aaba2979 --- /dev/null +++ b/apps/website/lib/view-models/auth/LoginInterfaces.ts @@ -0,0 +1,23 @@ +export interface LoginFormField { + value: string | boolean; + error?: string; + touched: boolean; + validating: boolean; +} + +export interface LoginFormState { + fields: { + email: LoginFormField; + password: LoginFormField; + rememberMe: LoginFormField; + }; + isValid: boolean; + isSubmitting: boolean; + submitError?: string; + submitCount: number; +} + +export interface LoginUIState { + showPassword: boolean; + showErrorDetails: boolean; +} diff --git a/apps/website/lib/view-models/auth/LoginViewModel.ts b/apps/website/lib/view-models/auth/LoginViewModel.ts index 4fa1deca4..c120ab42a 100644 --- a/apps/website/lib/view-models/auth/LoginViewModel.ts +++ b/apps/website/lib/view-models/auth/LoginViewModel.ts @@ -1,35 +1,13 @@ +import { ViewModel } from "../../contracts/view-models/ViewModel"; +import type { LoginFormState, LoginUIState } from "./LoginInterfaces"; + /** * Login ViewModel * * Client-side state management for login flow. * Immutable, class-based, contains only UI state. */ - -export interface LoginFormField { - value: string | boolean; - error?: string; - touched: boolean; - validating: boolean; -} - -export interface LoginFormState { - fields: { - email: LoginFormField; - password: LoginFormField; - rememberMe: LoginFormField; - }; - isValid: boolean; - isSubmitting: boolean; - submitError?: string; - submitCount: number; -} - -export interface LoginUIState { - showPassword: boolean; - showErrorDetails: boolean; -} - -export class LoginViewModel { +export class LoginViewModel extends ViewModel { constructor( public readonly returnTo: string, public readonly hasInsufficientPermissions: boolean, @@ -37,7 +15,9 @@ export class LoginViewModel { public readonly uiState: LoginUIState, public readonly mutationPending: boolean = false, public readonly mutationError: string | null = null - ) {} + ) { + super(); + } // Immutable updates withFormState(formState: LoginFormState): LoginViewModel { @@ -93,4 +73,4 @@ export class LoginViewModel { get formFields() { return this.formState.fields; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/auth/ResetPasswordInterfaces.ts b/apps/website/lib/view-models/auth/ResetPasswordInterfaces.ts new file mode 100644 index 000000000..900067733 --- /dev/null +++ b/apps/website/lib/view-models/auth/ResetPasswordInterfaces.ts @@ -0,0 +1,22 @@ +export interface ResetPasswordFormField { + value: string; + error?: string; + touched: boolean; + validating: boolean; +} + +export interface ResetPasswordFormState { + fields: { + newPassword: ResetPasswordFormField; + confirmPassword: ResetPasswordFormField; + }; + isValid: boolean; + isSubmitting: boolean; + submitError?: string; + submitCount: number; +} + +export interface ResetPasswordUIState { + showPassword: boolean; + showConfirmPassword: boolean; +} diff --git a/apps/website/lib/view-models/auth/ResetPasswordViewModel.ts b/apps/website/lib/view-models/auth/ResetPasswordViewModel.ts index 8b13c55dd..e8c5e2f4c 100644 --- a/apps/website/lib/view-models/auth/ResetPasswordViewModel.ts +++ b/apps/website/lib/view-models/auth/ResetPasswordViewModel.ts @@ -1,34 +1,13 @@ +import { ViewModel } from "../../contracts/view-models/ViewModel"; +import type { ResetPasswordFormState, ResetPasswordUIState } from "./ResetPasswordInterfaces"; + /** * Reset Password ViewModel * * Client-side state management for reset password flow. * Immutable, class-based, contains only UI state. */ - -export interface ResetPasswordFormField { - value: string; - error?: string; - touched: boolean; - validating: boolean; -} - -export interface ResetPasswordFormState { - fields: { - newPassword: ResetPasswordFormField; - confirmPassword: ResetPasswordFormField; - }; - isValid: boolean; - isSubmitting: boolean; - submitError?: string; - submitCount: number; -} - -export interface ResetPasswordUIState { - showPassword: boolean; - showConfirmPassword: boolean; -} - -export class ResetPasswordViewModel { +export class ResetPasswordViewModel extends ViewModel { constructor( public readonly token: string, public readonly returnTo: string, @@ -38,7 +17,9 @@ export class ResetPasswordViewModel { public readonly successMessage: string | null = null, public readonly mutationPending: boolean = false, public readonly mutationError: string | null = null - ) {} + ) { + super(); + } withFormState(formState: ResetPasswordFormState): ResetPasswordViewModel { return new ResetPasswordViewModel( @@ -99,4 +80,4 @@ export class ResetPasswordViewModel { get submitError(): string | undefined { return this.formState.submitError || this.mutationError || undefined; } -} \ No newline at end of file +} diff --git a/apps/website/lib/view-models/auth/SignupInterfaces.ts b/apps/website/lib/view-models/auth/SignupInterfaces.ts new file mode 100644 index 000000000..a8dd39ceb --- /dev/null +++ b/apps/website/lib/view-models/auth/SignupInterfaces.ts @@ -0,0 +1,25 @@ +export interface SignupFormField { + value: string; + error?: string; + touched: boolean; + validating: boolean; +} + +export interface SignupFormState { + fields: { + firstName: SignupFormField; + lastName: SignupFormField; + email: SignupFormField; + password: SignupFormField; + confirmPassword: SignupFormField; + }; + isValid: boolean; + isSubmitting: boolean; + submitError?: string; + submitCount: number; +} + +export interface SignupUIState { + showPassword: boolean; + showConfirmPassword: boolean; +} diff --git a/apps/website/lib/view-models/auth/SignupViewModel.ts b/apps/website/lib/view-models/auth/SignupViewModel.ts index d71165e01..1036d8283 100644 --- a/apps/website/lib/view-models/auth/SignupViewModel.ts +++ b/apps/website/lib/view-models/auth/SignupViewModel.ts @@ -1,44 +1,22 @@ +import { ViewModel } from "../../contracts/view-models/ViewModel"; +import type { SignupFormState, SignupUIState } from "./SignupInterfaces"; + /** * Signup ViewModel * * Client-side state management for signup flow. * Immutable, class-based, contains only UI state. */ - -export interface SignupFormField { - value: string; - error?: string; - touched: boolean; - validating: boolean; -} - -export interface SignupFormState { - fields: { - firstName: SignupFormField; - lastName: SignupFormField; - email: SignupFormField; - password: SignupFormField; - confirmPassword: SignupFormField; - }; - isValid: boolean; - isSubmitting: boolean; - submitError?: string; - submitCount: number; -} - -export interface SignupUIState { - showPassword: boolean; - showConfirmPassword: boolean; -} - -export class SignupViewModel { +export class SignupViewModel extends ViewModel { constructor( public readonly returnTo: string, public readonly formState: SignupFormState, public readonly uiState: SignupUIState, public readonly mutationPending: boolean = false, public readonly mutationError: string | null = null - ) {} + ) { + super(); + } withFormState(formState: SignupFormState): SignupViewModel { return new SignupViewModel( @@ -77,4 +55,4 @@ export class SignupViewModel { get submitError(): string | undefined { return this.formState.submitError || this.mutationError || undefined; } -} \ No newline at end of file +} diff --git a/apps/website/templates/TeamLeaderboardTemplate.tsx b/apps/website/templates/TeamLeaderboardTemplate.tsx index d679dca3c..33b2db381 100644 --- a/apps/website/templates/TeamLeaderboardTemplate.tsx +++ b/apps/website/templates/TeamLeaderboardTemplate.tsx @@ -1,7 +1,8 @@ 'use client'; import { LeaderboardFiltersBar } from '@/components/leaderboards/LeaderboardFiltersBar'; -import type { SkillLevel, SortBy, TeamLeaderboardViewData } from '@/lib/view-data/TeamLeaderboardViewData'; +import type { SkillLevel, SortBy } from '@/lib/view-data/TeamLeaderboardViewData'; +import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel'; import { Button } from '@/ui/Button'; import { Container } from '@/ui/Container'; import { Heading } from '@/ui/Heading'; @@ -16,7 +17,13 @@ import { Award, ChevronLeft, Users } from 'lucide-react'; import React from 'react'; interface TeamLeaderboardTemplateProps { - viewData: TeamLeaderboardViewData; + viewData: { + teams: TeamSummaryViewModel[]; + searchQuery: string; + filterLevel: SkillLevel | 'all'; + sortBy: SortBy; + filteredAndSortedTeams: TeamSummaryViewModel[]; + }; onSearchChange: (query: string) => void; filterLevelChange: (level: SkillLevel | 'all') => void; onSortChange: (sort: SortBy) => void; diff --git a/eslint-report.json b/eslint-report.json new file mode 100644 index 000000000..dd72e8560 --- /dev/null +++ b/eslint-report.json @@ -0,0 +1 @@ +[{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/ActivityItemViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/AdminUserViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/AvailableLeaguesViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/AvatarGenerationViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/AvatarViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/BillingStatsViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/BillingViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/CompleteOnboardingViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/builders/view-data/CompleteOnboardingViewData","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":103}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { CompleteOnboardingViewData } from '@/lib/builders/view-data/CompleteOnboardingViewData';\nimport { OnboardingStatusDisplay } from '../display-objects/OnboardingStatusDisplay';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * Complete onboarding view model\n * UI representation of onboarding completion result\n *\n * Composes Display Objects and transforms ViewData for UI consumption.\n */\nexport class CompleteOnboardingViewModel extends ViewModel {\n private readonly data: CompleteOnboardingViewData;\n\n constructor(data: CompleteOnboardingViewData) {\n super();\n this.data = data;\n }\n\n get success(): boolean { return this.data.success; }\n get driverId(): string | undefined { return this.data.driverId; }\n get errorMessage(): string | undefined { return this.data.errorMessage; }\n\n /** UI-specific: Status label using Display Object */\n get statusLabel(): string {\n return OnboardingStatusDisplay.statusLabel(this.success);\n }\n\n /** UI-specific: Status variant using Display Object */\n get statusVariant(): string {\n return OnboardingStatusDisplay.statusVariant(this.success);\n }\n\n /** UI-specific: Status icon using Display Object */\n get statusIcon(): string {\n return OnboardingStatusDisplay.statusIcon(this.success);\n }\n\n /** UI-specific: Status message using Display Object */\n get statusMessage(): string {\n return OnboardingStatusDisplay.statusMessage(this.success, this.errorMessage);\n }\n\n /** UI-specific: Whether onboarding was successful */\n get isSuccessful(): boolean {\n return this.success;\n }\n\n /** UI-specific: Whether there was an error */\n get hasError(): boolean {\n return !!this.errorMessage;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/CreateLeagueViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/CreateTeamViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/DashboardStatsViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/DeleteMediaViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/builders/view-data/DeleteMediaViewData","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":89}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { DeleteMediaViewData } from '@/lib/builders/view-data/DeleteMediaViewData';\nimport { ViewModel } from '../contracts/view-models/ViewModel';\n\n/**\n * Delete Media View Model\n *\n * Represents the result of a media deletion operation\n * Composes ViewData for UI consumption.\n */\nexport class DeleteMediaViewModel extends ViewModel {\n private readonly data: DeleteMediaViewData;\n\n constructor(data: DeleteMediaViewData) {\n super();\n this.data = data;\n }\n\n get success(): boolean { return this.data.success; }\n get error(): string | undefined { return this.data.error; }\n\n /** UI-specific: Whether the deletion was successful */\n get isSuccessful(): boolean {\n return this.success;\n }\n\n /** UI-specific: Whether there was an error */\n get hasError(): boolean {\n return !!this.error;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/DriverLeaderboardViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/DriverProfileDriverSummaryViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/DriverProfileViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/DriverRegistrationStatusViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/DriverSummaryViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/DriverTeamViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/DriverViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":14,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":23,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Driver view model\n * UI representation of a driver\n *\n * Note: client-only ViewModel created from ViewData (never DTO).\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { RatingDisplay } from \"../display-objects/RatingDisplay\";\n\n/**\n * ViewData for Driver\n * This is the JSON-serializable input for the Template.\n */\nexport interface DriverViewData {\n id: string;\n name: string;\n avatarUrl: string | null;\n iracingId?: string;\n rating?: number;\n country?: string;\n bio?: string;\n joinedAt?: string;\n}\n\nexport class DriverViewModel extends ViewModel {\n private readonly data: DriverViewData;\n\n constructor(data: DriverViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get name(): string { return this.data.name; }\n get avatarUrl(): string { return this.data.avatarUrl || ''; }\n get iracingId(): string | undefined { return this.data.iracingId; }\n get rating(): number | undefined { return this.data.rating; }\n get country(): string | undefined { return this.data.country; }\n get bio(): string | undefined { return this.data.bio; }\n get joinedAt(): string | undefined { return this.data.joinedAt; }\n\n /** UI-specific: Whether driver has an iRacing ID */\n get hasIracingId(): boolean {\n return !!this.iracingId;\n }\n\n /** UI-specific: Formatted rating */\n get formattedRating(): string {\n return this.rating !== undefined ? RatingDisplay.format(this.rating) : \"Unrated\";\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/EmailSignupViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/HomeDiscoveryViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/ImportRaceResultsSummaryViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/InvoiceViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueAdminRosterJoinRequestViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":7,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":14,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueAdminRosterJoinRequest\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueAdminRosterJoinRequestViewData {\n id: string;\n leagueId: string;\n driverId: string;\n driverName: string;\n requestedAtIso: string;\n message?: string;\n}\n\nexport class LeagueAdminRosterJoinRequestViewModel extends ViewModel {\n private readonly data: LeagueAdminRosterJoinRequestViewData;\n\n constructor(data: LeagueAdminRosterJoinRequestViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get leagueId(): string { return this.data.leagueId; }\n get driverId(): string { return this.data.driverId; }\n get driverName(): string { return this.data.driverName; }\n get requestedAtIso(): string { return this.data.requestedAtIso; }\n get message(): string | undefined { return this.data.message; }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueAdminRosterMemberViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/MembershipRole","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":66},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":9,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":14,"endColumn":2}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { MembershipRole } from '@/lib/types/MembershipRole';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueAdminRosterMember\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueAdminRosterMemberViewData {\n driverId: string;\n driverName: string;\n role: MembershipRole;\n joinedAtIso: string;\n}\n\nexport class LeagueAdminRosterMemberViewModel extends ViewModel {\n private readonly data: LeagueAdminRosterMemberViewData;\n\n constructor(data: LeagueAdminRosterMemberViewData) {\n super();\n this.data = data;\n }\n\n get driverId(): string { return this.data.driverId; }\n get driverName(): string { return this.data.driverName; }\n get role(): MembershipRole { return this.data.role; }\n get joinedAtIso(): string { return this.data.joinedAtIso; }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueAdminScheduleViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":9,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":13,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { LeagueScheduleRaceViewModel } from './LeagueScheduleViewModel';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueAdminSchedule\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueAdminScheduleViewData {\n seasonId: string;\n published: boolean;\n races: any[];\n}\n\nexport class LeagueAdminScheduleViewModel extends ViewModel {\n private readonly data: LeagueAdminScheduleViewData;\n\n constructor(data: LeagueAdminScheduleViewData) {\n super();\n this.data = data;\n }\n\n get seasonId(): string { return this.data.seasonId; }\n get published(): boolean { return this.data.published; }\n get races(): any[] { return this.data.races; }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueAdminViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":12,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { LeagueMemberViewModel, LeagueMemberViewData } from './LeagueMemberViewModel';\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueAdmin\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueAdminViewData {\n config: unknown;\n members: LeagueMemberViewData[];\n joinRequests: any[];\n}\n\nexport class LeagueAdminViewModel extends ViewModel {\n private readonly data: LeagueAdminViewData;\n readonly members: LeagueMemberViewModel[];\n\n constructor(data: LeagueAdminViewData) {\n super();\n this.data = data;\n this.members = data.members.map(m => new LeagueMemberViewModel(m));\n }\n\n get config(): unknown { return this.data.config; }\n get joinRequests(): any[] { return this.data.joinRequests; }\n\n /** UI-specific: Total pending requests count */\n get pendingRequestsCount(): number {\n return this.joinRequests.length;\n }\n\n /** UI-specific: Whether there are any pending requests */\n get hasPendingRequests(): boolean {\n return this.joinRequests.length > 0;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueCardViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":7,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":11,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueCard\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueCardViewData {\n id: string;\n name: string;\n description?: string;\n}\n\nexport class LeagueCardViewModel extends ViewModel {\n private readonly data: LeagueCardViewData;\n\n constructor(data: LeagueCardViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get name(): string { return this.data.name; }\n get description(): string { return this.data.description ?? 'Competitive iRacing league'; }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueDetailPageViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":105},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/LeagueWithCapacityAndScoringDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":105},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":41},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":41},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":2,"endColumn":71},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/LeagueStatsDTO","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":2,"endColumn":71},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":2,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":2,"endColumn":24},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":2,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":2,"endColumn":24},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":3,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":3,"endColumn":83},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/LeagueMembershipsDTO","line":3,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":3,"endColumn":83},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":3,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":3,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":3,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":3,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":4,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":4,"endColumn":79},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/GetDriverOutputDTO","line":4,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":4,"endColumn":79},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":4,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":4,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":4,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":4,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":5,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":5,"endColumn":87},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/LeagueScoringConfigDTO","line":5,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":5,"endColumn":87},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":5,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":5,"endColumn":32},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":5,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":5,"endColumn":32},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":76,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":76,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":79,"column":18,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":79,"endColumn":40},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":82,"column":12,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":82,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":118,"column":13,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":118,"endColumn":44},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":119,"column":12,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":119,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":120,"column":20,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":120,"endColumn":42},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":121,"column":14,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":121,"endColumn":32},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":122,"column":18,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":122,"endColumn":38},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":124,"column":18,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":124,"endColumn":32},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":219,"column":11,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":219,"endColumn":20},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":220,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":220,"endColumn":19},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":223,"column":23,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":223,"endColumn":32},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":227,"column":11,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":227,"endColumn":20},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":228,"column":13,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":228,"endColumn":22},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":230,"column":18,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":230,"endColumn":27}],"suppressedMessages":[],"errorCount":35,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';\nimport { LeagueStatsDTO } from '@/lib/types/generated/LeagueStatsDTO';\nimport { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';\nimport { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';\nimport { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';\nimport { RaceViewModel } from './RaceViewModel';\nimport { DriverViewModel } from './DriverViewModel';\n\n// Sponsor info type\nexport interface SponsorInfo {\n id: string;\n name: string;\n logoUrl?: string;\n websiteUrl?: string;\n tier: 'main' | 'secondary';\n tagline?: string;\n}\n\n// Driver summary for management section\nexport interface DriverSummary {\n driver: DriverViewModel;\n rating: number | null;\n rank: number | null;\n}\n\n// League membership with role\nexport interface LeagueMembershipWithRole {\n driverId: string;\n role: 'owner' | 'admin' | 'steward' | 'member';\n status: 'active' | 'inactive';\n joinedAt: string;\n}\n\n// Helper interfaces for type narrowing\ninterface LeagueSettings {\n maxDrivers?: number;\n}\n\ninterface SocialLinks {\n discordUrl?: string;\n youtubeUrl?: string;\n websiteUrl?: string;\n}\n\ninterface LeagueStatsExtended {\n averageSOF?: number;\n averageRating?: number;\n completedRaces?: number;\n totalRaces?: number;\n}\n\ninterface MembershipsContainer {\n members?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>;\n memberships?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>;\n}\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport class LeagueDetailPageViewModel extends ViewModel {\n // League basic info\n id: string;\n name: string;\n description?: string;\n ownerId: string;\n createdAt: string;\n settings: {\n maxDrivers?: number;\n };\n socialLinks: {\n discordUrl?: string;\n youtubeUrl?: string;\n websiteUrl?: string;\n } | undefined;\n\n // Owner info\n owner: GetDriverOutputDTO | null;\n\n // Scoring configuration\n scoringConfig: LeagueScoringConfigDTO | null;\n\n // Drivers and memberships\n drivers: GetDriverOutputDTO[];\n memberships: LeagueMembershipWithRole[];\n\n // Races\n allRaces: RaceViewModel[];\n runningRaces: RaceViewModel[];\n\n // Stats\n averageSOF: number | null;\n completedRacesCount: number;\n\n // Sponsors\n sponsors: SponsorInfo[];\n\n // Sponsor insights data\n sponsorInsights: {\n avgViewsPerRace: number;\n totalImpressions: number;\n engagementRate: string;\n estimatedReach: number;\n mainSponsorAvailable: boolean;\n secondarySlotsAvailable: number;\n mainSponsorPrice: number;\n secondaryPrice: number;\n tier: 'premium' | 'standard' | 'starter';\n trustScore: number;\n discordMembers: number;\n monthlyActivity: number;\n };\n\n // Driver summaries for management\n ownerSummary: DriverSummary | null;\n adminSummaries: DriverSummary[];\n stewardSummaries: DriverSummary[];\n\n constructor(\n league: LeagueWithCapacityAndScoringDTO,\n owner: GetDriverOutputDTO | null,\n scoringConfig: LeagueScoringConfigDTO | null,\n drivers: GetDriverOutputDTO[],\n memberships: LeagueMembershipsDTO,\n allRaces: RaceViewModel[],\n leagueStats: LeagueStatsDTO,\n sponsors: SponsorInfo[]\n ) {\n this.id = league.id;\n this.name = league.name;\n this.description = league.description ?? '';\n this.ownerId = league.ownerId;\n this.createdAt = league.createdAt;\n \n // Handle settings with proper type narrowing\n const settings = league.settings as LeagueSettings | undefined;\n const maxDrivers = settings?.maxDrivers;\n this.settings = {\n maxDrivers: maxDrivers,\n };\n \n // Handle social links with proper type narrowing\n const socialLinks = league.socialLinks as SocialLinks | undefined;\n const discordUrl = socialLinks?.discordUrl;\n const youtubeUrl = socialLinks?.youtubeUrl;\n const websiteUrl = socialLinks?.websiteUrl;\n \n this.socialLinks = {\n discordUrl,\n youtubeUrl,\n websiteUrl,\n };\n\n this.owner = owner;\n this.scoringConfig = scoringConfig;\n this.drivers = drivers;\n\n // Handle memberships with proper type narrowing\n const membershipsContainer = memberships as MembershipsContainer;\n const membershipDtos = membershipsContainer.members ?? \n membershipsContainer.memberships ?? \n [];\n\n this.memberships = membershipDtos.map((m) => ({\n driverId: m.driverId,\n role: m.role as 'owner' | 'admin' | 'steward' | 'member',\n status: m.status ?? 'active',\n joinedAt: m.joinedAt,\n }));\n\n this.allRaces = allRaces;\n this.runningRaces = allRaces.filter(r => r.status === 'running');\n\n // Calculate SOF from available data with proper type narrowing\n const statsExtended = leagueStats as LeagueStatsExtended;\n const averageSOF = statsExtended.averageSOF ?? \n statsExtended.averageRating ?? undefined;\n const completedRaces = statsExtended.completedRaces ?? \n statsExtended.totalRaces ?? undefined;\n\n this.averageSOF = typeof averageSOF === 'number' ? averageSOF : null;\n this.completedRacesCount = typeof completedRaces === 'number' ? completedRaces : 0;\n\n this.sponsors = sponsors;\n\n // Calculate sponsor insights\n const memberCount = this.memberships.length;\n const mainSponsorTaken = this.sponsors.some(s => s.tier === 'main');\n const secondaryTaken = this.sponsors.filter(s => s.tier === 'secondary').length;\n\n this.sponsorInsights = {\n avgViewsPerRace: 5400 + memberCount * 50,\n totalImpressions: 45000 + memberCount * 500,\n engagementRate: (3.5 + (memberCount / 50)).toFixed(1),\n estimatedReach: memberCount * 150,\n mainSponsorAvailable: !mainSponsorTaken,\n secondarySlotsAvailable: Math.max(0, 2 - secondaryTaken),\n mainSponsorPrice: 800 + Math.floor(memberCount * 10),\n secondaryPrice: 250 + Math.floor(memberCount * 3),\n tier: (this.averageSOF && this.averageSOF > 3000 ? 'premium' : this.averageSOF && this.averageSOF > 2000 ? 'standard' : 'starter') as 'premium' | 'standard' | 'starter',\n trustScore: Math.min(100, 60 + memberCount + this.completedRacesCount),\n discordMembers: memberCount * 3,\n monthlyActivity: Math.min(100, 40 + this.completedRacesCount * 2),\n };\n\n // Build driver summaries\n this.ownerSummary = this.buildDriverSummary(this.ownerId);\n this.adminSummaries = this.memberships\n .filter(m => m.role === 'admin')\n .slice(0, 3)\n .map(m => this.buildDriverSummary(m.driverId))\n .filter((s): s is DriverSummary => s !== null);\n this.stewardSummaries = this.memberships\n .filter(m => m.role === 'steward')\n .slice(0, 3)\n .map(m => this.buildDriverSummary(m.driverId))\n .filter((s): s is DriverSummary => s !== null);\n }\n\n private buildDriverSummary(driverId: string): DriverSummary | null {\n const driverDto = this.drivers.find(d => d.id === driverId);\n if (!driverDto) return null;\n\n // Handle avatarUrl with proper type checking\n const driverAny = driverDto as { avatarUrl?: unknown };\n const avatarUrl = typeof driverAny.avatarUrl === 'string' ? driverAny.avatarUrl : null;\n\n const driver = new DriverViewModel({\n id: driverDto.id,\n name: driverDto.name,\n avatarUrl: avatarUrl,\n iracingId: driverDto.iracingId,\n });\n\n // Detailed rating and rank data are not wired from the analytics services yet;\n // expose the driver identity only so the UI can still render role assignments.\n return {\n driver,\n rating: null,\n rank: null,\n };\n }\n\n // UI helper methods\n get isSponsorMode(): boolean {\n // League detail pages are rendered in organizer mode in this build; sponsor-specific\n // mode switches will be introduced once sponsor dashboards share this view model.\n return false;\n }\n\n get currentUserMembership(): LeagueMembershipWithRole | null {\n // Current user identity is not available in this view model context yet; callers must\n // pass an explicit membership if they need per-user permissions.\n return null;\n }\n\n get canEndRaces(): boolean {\n return this.currentUserMembership?.role === 'admin' || this.currentUserMembership?.role === 'owner';\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueDetailViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":11,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":15,"endColumn":2},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":68,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":89,"endColumn":2}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { DriverViewModel, DriverViewData } from \"./DriverViewModel\";\nimport { RaceViewModel, RaceViewData } from \"./RaceViewModel\";\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { CurrencyDisplay } from \"../display-objects/CurrencyDisplay\";\nimport { LeagueTierDisplay } from \"../display-objects/LeagueTierDisplay\";\n\n/**\n * ViewData for LeagueDetail\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueDetailViewData {\n league: LeagueViewData;\n drivers: (DriverViewData & { impressions: number })[];\n races: (RaceViewData & { views: number })[];\n}\n\nexport class LeagueDetailViewModel extends ViewModel {\n private readonly data: LeagueDetailViewData;\n readonly league: LeagueViewModel;\n readonly drivers: LeagueDetailDriverViewModel[];\n readonly races: LeagueDetailRaceViewModel[];\n\n constructor(data: LeagueDetailViewData) {\n super();\n this.data = data;\n this.league = new LeagueViewModel(data.league);\n this.drivers = data.drivers.map(driver => new LeagueDetailDriverViewModel(driver));\n this.races = data.races.map(race => new LeagueDetailRaceViewModel(race));\n }\n}\n\nexport class LeagueDetailDriverViewModel extends DriverViewModel {\n private readonly detailData: DriverViewData & { impressions: number };\n\n constructor(data: DriverViewData & { impressions: number }) {\n super(data);\n this.detailData = data;\n }\n\n get impressions(): number {\n return this.detailData.impressions;\n }\n\n get formattedImpressions(): string {\n // Client-only formatting\n return this.impressions.toLocaleString();\n }\n}\n\nexport class LeagueDetailRaceViewModel extends RaceViewModel {\n private readonly detailData: RaceViewData & { views: number };\n\n constructor(data: RaceViewData & { views: number }) {\n super(data);\n this.detailData = data;\n }\n\n get views(): number {\n return this.detailData.views;\n }\n\n get formattedViews(): string {\n // Client-only formatting\n return this.views.toLocaleString();\n }\n}\n\nexport interface LeagueViewData {\n id: string;\n name: string;\n game: string;\n tier: 'premium' | 'standard' | 'starter';\n season: string;\n description: string;\n drivers: number;\n races: number;\n completedRaces: number;\n totalImpressions: number;\n avgViewsPerRace: number;\n engagement: number;\n rating: number;\n seasonStatus: 'active' | 'upcoming' | 'completed';\n seasonDates: { start: string; end: string };\n nextRace?: { name: string; date: string };\n sponsorSlots: {\n main: { available: boolean; price: number; benefits: string[] };\n secondary: { available: number; total: number; price: number; benefits: string[] };\n };\n}\n\nexport class LeagueViewModel extends ViewModel {\n private readonly data: LeagueViewData;\n\n constructor(data: LeagueViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get name(): string { return this.data.name; }\n get game(): string { return this.data.game; }\n get tier(): 'premium' | 'standard' | 'starter' { return this.data.tier; }\n get season(): string { return this.data.season; }\n get description(): string { return this.data.description; }\n get drivers(): number { return this.data.drivers; }\n get races(): number { return this.data.races; }\n get completedRaces(): number { return this.data.completedRaces; }\n get totalImpressions(): number { return this.data.totalImpressions; }\n get avgViewsPerRace(): number { return this.data.avgViewsPerRace; }\n get engagement(): number { return this.data.engagement; }\n get rating(): number { return this.data.rating; }\n get seasonStatus(): 'active' | 'upcoming' | 'completed' { return this.data.seasonStatus; }\n get seasonDates() { return this.data.seasonDates; }\n get nextRace() { return this.data.nextRace; }\n get sponsorSlots() { return this.data.sponsorSlots; }\n\n get formattedTotalImpressions(): string {\n // Client-only formatting\n return this.totalImpressions.toLocaleString();\n }\n\n get formattedAvgViewsPerRace(): string {\n // Client-only formatting\n return this.avgViewsPerRace.toLocaleString();\n }\n\n get projectedTotalViews(): number {\n return Math.round(this.avgViewsPerRace * this.races);\n }\n\n get formattedProjectedTotal(): string {\n // Client-only formatting\n return this.projectedTotalViews.toLocaleString();\n }\n\n get mainSponsorCpm(): number {\n return Math.round((this.sponsorSlots.main.price / this.projectedTotalViews) * 1000);\n }\n\n get formattedMainSponsorCpm(): string {\n return CurrencyDisplay.format(this.mainSponsorCpm);\n }\n\n get racesLeft(): number {\n return this.races - this.completedRaces;\n }\n\n get tierConfig() {\n return LeagueTierDisplay.getDisplay(this.tier);\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":88},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/LeagueJoinRequestDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":88},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":35},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":35},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":13,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":19,"endColumn":2}],"suppressedMessages":[],"errorCount":5,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { LeagueJoinRequestDTO } from '@/lib/types/generated/LeagueJoinRequestDTO';\n\n/**\n * League join request view model\n * Transform from DTO to ViewModel with UI fields\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueJoinRequest\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueJoinRequestViewData {\n id: string;\n leagueId: string;\n driverId: string;\n requestedAt: string;\n isAdmin: boolean;\n}\n\nexport class LeagueJoinRequestViewModel extends ViewModel {\n private readonly data: LeagueJoinRequestViewData;\n\n constructor(data: LeagueJoinRequestViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get leagueId(): string { return this.data.leagueId; }\n get driverId(): string { return this.data.driverId; }\n get requestedAt(): string { return this.data.requestedAt; }\n\n /** UI-specific: Formatted request date */\n get formattedRequestedAt(): string {\n return new Date(this.requestedAt).toLocaleString();\n }\n\n /** UI-specific: Whether the request can be approved by current user */\n get canApprove(): boolean {\n return this.data.isAdmin;\n }\n\n /** UI-specific: Whether the request can be rejected by current user */\n get canReject(): boolean {\n return this.data.isAdmin;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueMemberViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":14,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { LeagueRoleDisplay, LeagueRole } from \"../display-objects/LeagueRoleDisplay\";\n\n/**\n * ViewData for LeagueMember\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueMemberViewData {\n driverId: string;\n currentUserId: string;\n driver?: any;\n role: string;\n joinedAt: string;\n}\n\nexport class LeagueMemberViewModel extends ViewModel {\n private readonly data: LeagueMemberViewData;\n\n constructor(data: LeagueMemberViewData) {\n super();\n this.data = data;\n }\n\n get driverId(): string { return this.data.driverId; }\n get driver(): any { return this.data.driver; }\n get role(): string { return this.data.role; }\n get joinedAt(): string { return this.data.joinedAt; }\n\n /** UI-specific: Formatted join date */\n get formattedJoinedAt(): string {\n // Client-only formatting\n return new Date(this.joinedAt).toLocaleDateString();\n }\n\n /** UI-specific: Badge classes for role */\n get roleBadgeClasses(): string {\n return LeagueRoleDisplay.getLeagueRoleDisplay(this.role as LeagueRole)?.badgeClasses || '';\n }\n\n /** UI-specific: Whether this member is the owner */\n get isOwner(): boolean {\n return this.role === 'owner';\n }\n\n /** UI-specific: Whether this is the current user */\n get isCurrentUser(): boolean {\n return this.driverId === this.data.currentUserId;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueMembershipsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":2,"endColumn":78},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/LeagueMemberDTO","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":2,"endColumn":78},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":2,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":2,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":2,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":2,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":15,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":17,"endColumn":2}],"suppressedMessages":[],"errorCount":5,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { LeagueMemberViewModel } from './LeagueMemberViewModel';\nimport type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';\n\n/**\n * View Model for League Memberships\n *\n * Represents the league's memberships in a UI-ready format.\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueMemberships\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueMembershipsViewData {\n memberships: any[];\n}\n\nexport class LeagueMembershipsViewModel extends ViewModel {\n private readonly data: LeagueMembershipsViewData;\n readonly memberships: LeagueMemberViewModel[];\n\n constructor(data: LeagueMembershipsViewData) {\n super();\n this.data = data;\n this.memberships = data.memberships.map((m) => new LeagueMemberViewModel(m));\n }\n\n /** UI-specific: Number of members */\n get memberCount(): number {\n return this.memberships.length;\n }\n\n /** UI-specific: Whether the league has members */\n get hasMembers(): boolean {\n return this.memberCount > 0;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeaguePageDetailViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":16,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * League Page Detail View Model\n *\n * View model for league page details.\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface LeaguePageDetailViewData {\n id: string;\n name: string;\n description: string;\n ownerId: string;\n ownerName: string;\n isAdmin: boolean;\n mainSponsor: { name: string; logoUrl: string; websiteUrl: string } | null;\n}\n\nexport class LeaguePageDetailViewModel extends ViewModel {\n id: string;\n name: string;\n description: string;\n ownerId: string;\n ownerName: string;\n isAdmin: boolean;\n mainSponsor: { name: string; logoUrl: string; websiteUrl: string } | null;\n\n constructor(data: LeaguePageDetailViewData) {\n super();\n this.id = data.id;\n this.name = data.name;\n this.description = data.description;\n this.ownerId = data.ownerId;\n this.ownerName = data.ownerName;\n this.isAdmin = data.isAdmin;\n this.mainSponsor = data.mainSponsor;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueScheduleViewModel.ts","messages":[{"ruleId":"import/no-duplicates","severity":1,"message":"'/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/contracts/view-models/ViewModel.ts' imported multiple times.","line":6,"column":27,"nodeType":"Literal","endLine":6,"endColumn":63},{"ruleId":"import/no-duplicates","severity":1,"message":"'/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/contracts/view-models/ViewModel.ts' imported multiple times.","line":23,"column":27,"nodeType":"Literal","endLine":23,"endColumn":63},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":29,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":31,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * View Model for League Schedule\n *\n * Service layer maps DTOs into these shapes; UI consumes ViewModels only.\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface LeagueScheduleRaceViewModel extends ViewModel {\n id: string;\n name: string;\n scheduledAt: Date;\n formattedDate: string;\n formattedTime: string;\n isPast: boolean;\n isUpcoming: boolean;\n status: string;\n track?: string;\n car?: string;\n sessionType?: string;\n isRegistered?: boolean;\n}\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueSchedule\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueScheduleViewData {\n races: any[];\n}\n\nexport class LeagueScheduleViewModel extends ViewModel {\n private readonly data: LeagueScheduleViewData;\n readonly races: LeagueScheduleRaceViewModel[];\n\n constructor(data: LeagueScheduleViewData) {\n super();\n this.data = data;\n this.races = data.races;\n }\n\n get raceCount(): number {\n return this.races.length;\n }\n\n get hasRaces(): boolean {\n return this.raceCount > 0;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueScoringChampionshipViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueScoringConfigViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":7,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":12,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueScoringConfig\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueScoringConfigViewData {\n gameName: string;\n scoringPresetName?: string;\n dropPolicySummary?: string;\n championships?: any[];\n}\n\nexport class LeagueScoringConfigViewModel extends ViewModel {\n private readonly data: LeagueScoringConfigViewData;\n\n constructor(data: LeagueScoringConfigViewData) {\n super();\n this.data = data;\n }\n\n get gameName(): string { return this.data.gameName; }\n get scoringPresetName(): string | undefined { return this.data.scoringPresetName; }\n get dropPolicySummary(): string | undefined { return this.data.dropPolicySummary; }\n get championships(): any[] | undefined { return this.data.championships; }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueScoringPresetViewModel.ts","messages":[{"ruleId":"import/no-duplicates","severity":1,"message":"'/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/contracts/view-models/ViewModel.ts' imported multiple times.","line":1,"column":27,"nodeType":"Literal","endLine":1,"endColumn":63},{"ruleId":"import/no-duplicates","severity":1,"message":"'/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/contracts/view-models/ViewModel.ts' imported multiple times.","line":24,"column":27,"nodeType":"Literal","endLine":24,"endColumn":63}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport type LeagueScoringPresetTimingDefaultsViewModel = ViewModel & {\n practiceMinutes: number;\n qualifyingMinutes: number;\n sprintRaceMinutes: number;\n mainRaceMinutes: number;\n sessionCount: number;\n};\n\nexport type LeagueScoringPresetViewModelInput = {\n id: string;\n name: string;\n sessionSummary: string;\n bonusSummary?: string;\n defaultTimings: LeagueScoringPresetTimingDefaultsViewModel;\n};\n\n/**\n * LeagueScoringPresetViewModel\n *\n * View model for league scoring preset configuration\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport class LeagueScoringPresetViewModel extends ViewModel {\n readonly id: string;\n readonly name: string;\n readonly sessionSummary: string;\n readonly bonusSummary?: string;\n readonly defaultTimings: LeagueScoringPresetTimingDefaultsViewModel;\n\n constructor(input: LeagueScoringPresetViewModelInput) {\n this.id = input.id;\n this.name = input.name;\n this.sessionSummary = input.sessionSummary;\n this.bonusSummary = input.bonusSummary;\n this.defaultTimings = input.defaultTimings;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueScoringPresetsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":6,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":9,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * View Model for league scoring presets\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface LeagueScoringPresetsViewData {\n presets: any[];\n totalCount?: number;\n}\n\nexport class LeagueScoringPresetsViewModel extends ViewModel {\n presets: any[];\n totalCount: number;\n\n constructor(data: LeagueScoringPresetsViewData) {\n super();\n this.presets = data.presets;\n this.totalCount = data.totalCount ?? data.presets.length;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueScoringSectionViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/LeagueConfigFormModel","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":80}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';\nimport type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';\nimport type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationViewModel';\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * LeagueScoringSectionViewModel\n *\n * View model for the league scoring section UI state and operations\n */\nexport class LeagueScoringSectionViewModel extends ViewModel {\n readonly form: LeagueConfigFormModel;\n readonly presets: LeagueScoringPresetViewModel[];\n readonly readOnly: boolean;\n readonly patternOnly: boolean;\n readonly championshipsOnly: boolean;\n readonly disabled: boolean;\n readonly currentPreset: LeagueScoringPresetViewModel | null;\n readonly isCustom: boolean;\n\n constructor(\n form: LeagueConfigFormModel,\n presets: LeagueScoringPresetViewModel[],\n options: {\n readOnly?: boolean;\n patternOnly?: boolean;\n championshipsOnly?: boolean;\n } = {}\n ) {\n super();\n this.form = form;\n this.presets = presets;\n this.readOnly = options.readOnly || false;\n this.patternOnly = options.patternOnly || false;\n this.championshipsOnly = options.championshipsOnly || false;\n this.disabled = this.readOnly;\n this.currentPreset = form.scoring.patternId \n ? presets.find(p => p.id === form.scoring.patternId) || null\n : null;\n this.isCustom = form.scoring.customScoringEnabled || false;\n }\n\n /**\n * Get default custom points configuration\n */\n static getDefaultCustomPoints(): CustomPointsConfig {\n return {\n racePoints: [25, 18, 15, 12, 10, 8, 6, 4, 2, 1],\n poleBonusPoints: 1,\n fastestLapPoints: 1,\n leaderLapPoints: 0,\n };\n }\n\n /**\n * Check if form can be modified\n */\n canModify(): boolean {\n return !this.readOnly;\n }\n\n /**\n * Get available presets for display\n */\n getAvailablePresets(): LeagueScoringPresetViewModel[] {\n return this.presets;\n }\n\n /**\n * Get championships configuration for display\n */\n getChampionshipsConfig() {\n const isTeamsMode = this.form.structure.mode === 'fixedTeams';\n \n return [\n {\n key: 'enableDriverChampionship' as const,\n label: 'Driver Standings',\n description: 'Track individual driver points',\n enabled: this.form.championships.enableDriverChampionship,\n available: true,\n },\n {\n key: 'enableTeamChampionship' as const,\n label: 'Team Standings',\n description: 'Combined team points',\n enabled: this.form.championships.enableTeamChampionship,\n available: isTeamsMode,\n unavailableHint: 'Teams mode only',\n },\n {\n key: 'enableNationsChampionship' as const,\n label: 'Nations Cup',\n description: 'By nationality',\n enabled: this.form.championships.enableNationsChampionship,\n available: true,\n },\n {\n key: 'enableTrophyChampionship' as const,\n label: 'Trophy Cup',\n description: 'Special category',\n enabled: this.form.championships.enableTrophyChampionship,\n available: true,\n },\n ];\n }\n\n /**\n * Get panel visibility based on flags\n */\n shouldShowPatternPanel(): boolean {\n return !this.championshipsOnly;\n }\n\n shouldShowChampionshipsPanel(): boolean {\n return !this.patternOnly;\n }\n\n /**\n * Get the active custom points configuration\n */\n getActiveCustomPoints(): CustomPointsConfig {\n // This would be stored separately in the form model\n // For now, return defaults\n return LeagueScoringSectionViewModel.getDefaultCustomPoints();\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueSeasonSummaryViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueSettingsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/LeagueConfigFormModel","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":80},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":2,"endColumn":92},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/LeagueScoringPresetDTO","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":2,"endColumn":92},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":2,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":2,"endColumn":37},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":2,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":2,"endColumn":37},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":15,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":26,"endColumn":2},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":23,"column":12,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":23,"endColumn":34}],"suppressedMessages":[],"errorCount":7,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';\nimport type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';\nimport { DriverSummaryViewModel } from './DriverSummaryViewModel';\n\n/**\n * View Model for league settings page\n * Combines league config, presets, owner, and members\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueSettings\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueSettingsViewData {\n league: {\n id: string;\n name: string;\n ownerId: string;\n createdAt: string;\n };\n config: LeagueConfigFormModel;\n presets: LeagueScoringPresetDTO[];\n owner: any | null;\n members: any[];\n}\n\nexport class LeagueSettingsViewModel extends ViewModel {\n private readonly data: LeagueSettingsViewData;\n readonly owner: DriverSummaryViewModel | null;\n readonly members: DriverSummaryViewModel[];\n\n constructor(data: LeagueSettingsViewData) {\n super();\n this.data = data;\n this.owner = data.owner ? new DriverSummaryViewModel(data.owner) : null;\n this.members = data.members.map(m => new DriverSummaryViewModel(m));\n }\n\n get league() { return this.data.league; }\n get config() { return this.data.config; }\n get presets() { return this.data.presets; }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueStandingsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":12,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { StandingEntryViewModel, StandingEntryViewData } from './StandingEntryViewModel';\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueStandings\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueStandingsViewData {\n standings: StandingEntryViewData[];\n drivers: any[];\n memberships: any[];\n}\n\nexport class LeagueStandingsViewModel extends ViewModel {\n private readonly data: LeagueStandingsViewData;\n readonly standings: StandingEntryViewModel[];\n\n constructor(data: LeagueStandingsViewData) {\n super();\n this.data = data;\n this.standings = data.standings.map(s => new StandingEntryViewModel(s));\n }\n\n get drivers(): any[] { return this.data.drivers; }\n get memberships(): any[] { return this.data.memberships; }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueStatsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":12,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":14,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * View Model for League Statistics\n *\n * Represents the total number of leagues in a UI-ready format.\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueStats\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueStatsViewData {\n totalLeagues: number;\n}\n\nexport class LeagueStatsViewModel extends ViewModel {\n private readonly data: LeagueStatsViewData;\n\n constructor(data: LeagueStatsViewData) {\n super();\n this.data = data;\n }\n\n get totalLeagues(): number { return this.data.totalLeagues; }\n\n /** UI-specific: Formatted total leagues display */\n get formattedTotalLeagues(): string {\n // Client-only formatting\n return this.totalLeagues.toLocaleString();\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueStewardingViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":11,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":14,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * League Stewarding View Model\n * Represents all data needed for league stewarding across all races\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueStewarding\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueStewardingViewData {\n racesWithData: RaceWithProtests[];\n driverMap: Record;\n}\n\nexport class LeagueStewardingViewModel extends ViewModel {\n private readonly data: LeagueStewardingViewData;\n\n constructor(data: LeagueStewardingViewData) {\n super();\n this.data = data;\n }\n\n get racesWithData(): RaceWithProtests[] { return this.data.racesWithData; }\n get driverMap() { return this.data.driverMap; }\n\n /** UI-specific: Total pending protests count */\n get totalPending(): number {\n return this.racesWithData.reduce((sum, r) => sum + r.pendingProtests.length, 0);\n }\n\n /** UI-specific: Total resolved protests count */\n get totalResolved(): number {\n return this.racesWithData.reduce((sum, r) => sum + r.resolvedProtests.length, 0);\n }\n\n /** UI-specific: Total penalties count */\n get totalPenalties(): number {\n return this.racesWithData.reduce((sum, r) => sum + r.penalties.length, 0);\n }\n\n /** UI-specific: Filtered races for pending tab */\n get pendingRaces(): RaceWithProtests[] {\n return this.racesWithData.filter(r => r.pendingProtests.length > 0);\n }\n\n /** UI-specific: Filtered races for history tab */\n get historyRaces(): RaceWithProtests[] {\n return this.racesWithData.filter(r => r.resolvedProtests.length > 0 || r.penalties.length > 0);\n }\n\n /** UI-specific: All drivers for quick penalty modal */\n get allDrivers(): Array<{ id: string; name: string; avatarUrl?: string; iracingId?: string; rating?: number }> {\n return Object.values(this.driverMap);\n }\n}\n\nexport interface RaceWithProtests {\n race: {\n id: string;\n track: string;\n scheduledAt: Date;\n };\n pendingProtests: Protest[];\n resolvedProtests: Protest[];\n penalties: Penalty[];\n}\n\nexport interface Protest {\n id: string;\n protestingDriverId: string;\n accusedDriverId: string;\n incident: {\n lap: number;\n description: string;\n };\n filedAt: string;\n status: string;\n decisionNotes?: string;\n proofVideoUrl?: string;\n}\n\nexport interface Penalty {\n id: string;\n driverId: string;\n type: string;\n value: number;\n reason: string;\n notes?: string;\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueSummaryViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":7,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":33,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for LeagueSummary\n * This is the JSON-serializable input for the Template.\n */\nexport interface LeagueSummaryViewData {\n id: string;\n name: string;\n description: string | null;\n logoUrl: string | null;\n ownerId: string;\n createdAt: string;\n maxDrivers: number;\n usedDriverSlots: number;\n activeDriversCount?: number;\n nextRaceAt?: string;\n maxTeams?: number;\n usedTeamSlots?: number;\n structureSummary: string;\n scoringPatternSummary?: string;\n timingSummary: string;\n category?: string | null;\n scoring?: {\n gameId: string;\n gameName: string;\n primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';\n scoringPresetId: string;\n scoringPresetName: string;\n dropPolicySummary: string;\n scoringPatternSummary: string;\n };\n}\n\nexport class LeagueSummaryViewModel extends ViewModel {\n private readonly data: LeagueSummaryViewData;\n\n constructor(data: LeagueSummaryViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get name(): string { return this.data.name; }\n get description(): string | null { return this.data.description; }\n get logoUrl(): string | null { return this.data.logoUrl; }\n get ownerId(): string { return this.data.ownerId; }\n get createdAt(): string { return this.data.createdAt; }\n get maxDrivers(): number { return this.data.maxDrivers; }\n get usedDriverSlots(): number { return this.data.usedDriverSlots; }\n get activeDriversCount(): number | undefined { return this.data.activeDriversCount; }\n get nextRaceAt(): string | undefined { return this.data.nextRaceAt; }\n get maxTeams(): number | undefined { return this.data.maxTeams; }\n get usedTeamSlots(): number | undefined { return this.data.usedTeamSlots; }\n get structureSummary(): string { return this.data.structureSummary; }\n get scoringPatternSummary(): string | undefined { return this.data.scoringPatternSummary; }\n get timingSummary(): string { return this.data.timingSummary; }\n get category(): string | null | undefined { return this.data.category; }\n get scoring() { return this.data.scoring; }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/LeagueWalletViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":16,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":26,"endColumn":4},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":28,"column":20,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":28,"endColumn":23},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":29,"column":21,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":29,"endColumn":24},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":30,"column":25,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":30,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":31,"column":22,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":31,"endColumn":25},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":32,"column":29,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":32,"endColumn":32},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":33,"column":27,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":33,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":34,"column":25,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":34,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":35,"column":24,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":35,"endColumn":27},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":36,"column":34,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":36,"endColumn":37}],"suppressedMessages":[],"errorCount":10,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { WalletTransactionViewModel } from './WalletTransactionViewModel';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport class LeagueWalletViewModel extends ViewModel {\n balance: number;\n currency: string;\n totalRevenue: number;\n totalFees: number;\n totalWithdrawals: number;\n pendingPayouts: number;\n transactions: WalletTransactionViewModel[];\n canWithdraw: boolean;\n withdrawalBlockReason?: string;\n\n constructor(dto: {\n balance: number;\n currency: string;\n totalRevenue: number;\n totalFees: number;\n totalWithdrawals: number;\n pendingPayouts: number;\n transactions: WalletTransactionViewModel[];\n canWithdraw: boolean;\n withdrawalBlockReason?: string;\n }) {\n super();\n this.balance = dto.balance;\n this.currency = dto.currency;\n this.totalRevenue = dto.totalRevenue;\n this.totalFees = dto.totalFees;\n this.totalWithdrawals = dto.totalWithdrawals;\n this.pendingPayouts = dto.pendingPayouts;\n this.transactions = dto.transactions;\n this.canWithdraw = dto.canWithdraw;\n this.withdrawalBlockReason = dto.withdrawalBlockReason;\n }\n\n /** UI-specific: Formatted balance */\n get formattedBalance(): string {\n return `$${this.balance.toFixed(2)}`;\n }\n\n /** UI-specific: Formatted total revenue */\n get formattedTotalRevenue(): string {\n return `$${this.totalRevenue.toFixed(2)}`;\n }\n\n /** UI-specific: Formatted total fees */\n get formattedTotalFees(): string {\n return `$${this.totalFees.toFixed(2)}`;\n }\n\n /** UI-specific: Formatted pending payouts */\n get formattedPendingPayouts(): string {\n return `$${this.pendingPayouts.toFixed(2)}`;\n }\n\n /** UI-specific: Filtered transactions by type */\n getFilteredTransactions(type: 'all' | 'sponsorship' | 'membership' | 'withdrawal' | 'prize'): WalletTransactionViewModel[] {\n return type === 'all' ? this.transactions : this.transactions.filter(t => t.type === type);\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/MediaViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":5,"column":1,"nodeType":"TSTypeAliasDeclaration","messageId":"noViewDataDefinition","endLine":5,"endColumn":59}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { MediaViewData } from '@/lib/view-data/MediaViewData';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\ntype MediaAssetViewData = MediaViewData['assets'][number];\n\n/**\n * Media View Model\n *\n * Client-only ViewModel created from ViewData (never DTO).\n * Represents a single media asset card in the UI.\n */\nexport class MediaViewModel extends ViewModel {\n private readonly data: MediaAssetViewData;\n\n constructor(data: MediaAssetViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get src(): string { return this.data.src; }\n get title(): string { return this.data.title; }\n get category(): string { return this.data.category; }\n get date(): string | undefined { return this.data.date; }\n get dimensions(): string | undefined { return this.data.dimensions; }\n\n /** UI-specific: Combined subtitle used by MediaCard */\n get subtitle(): string {\n return `${this.category}${this.dimensions ? ` • ${this.dimensions}` : ''}`;\n }\n\n /** UI-specific: Whether any metadata is present */\n get hasMetadata(): boolean {\n return !!this.date || !!this.dimensions;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/MembershipFeeViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":17,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { CurrencyDisplay } from \"../display-objects/CurrencyDisplay\";\n\n/**\n * ViewData for MembershipFee\n * This is the JSON-serializable input for the Template.\n */\nexport interface MembershipFeeViewData {\n id: string;\n leagueId: string;\n seasonId?: string;\n type: string;\n amount: number;\n enabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport class MembershipFeeViewModel extends ViewModel {\n private readonly data: MembershipFeeViewData;\n\n constructor(data: MembershipFeeViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get leagueId(): string { return this.data.leagueId; }\n get seasonId(): string | undefined { return this.data.seasonId; }\n get type(): string { return this.data.type; }\n get amount(): number { return this.data.amount; }\n get enabled(): boolean { return this.data.enabled; }\n get createdAt(): string { return this.data.createdAt; }\n get updatedAt(): string { return this.data.updatedAt; }\n\n /** UI-specific: Formatted amount */\n get formattedAmount(): string {\n return CurrencyDisplay.format(this.amount, 'EUR');\n }\n\n /** UI-specific: Type display */\n get typeDisplay(): string {\n switch (this.type) {\n case 'season': return 'Per Season';\n case 'monthly': return 'Monthly';\n case 'per_race': return 'Per Race';\n default: return this.type;\n }\n }\n\n /** UI-specific: Status display */\n get statusDisplay(): string {\n return this.enabled ? 'Enabled' : 'Disabled';\n }\n\n /** UI-specific: Status color */\n get statusColor(): string {\n return this.enabled ? 'green' : 'red';\n }\n\n /** UI-specific: Formatted created date */\n get formattedCreatedAt(): string {\n return this.createdAt.toLocaleString();\n }\n\n /** UI-specific: Formatted updated date */\n get formattedUpdatedAt(): string {\n return this.updatedAt.toLocaleString();\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/NotificationSettingsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":3,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":10,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface NotificationSettingsViewData {\n emailNewSponsorships: boolean;\n emailWeeklyReport: boolean;\n emailRaceAlerts: boolean;\n emailPaymentAlerts: boolean;\n emailNewOpportunities: boolean;\n emailContractExpiry: boolean;\n}\n\nexport class NotificationSettingsViewModel extends ViewModel {\n emailNewSponsorships: boolean;\n emailWeeklyReport: boolean;\n emailRaceAlerts: boolean;\n emailPaymentAlerts: boolean;\n emailNewOpportunities: boolean;\n emailContractExpiry: boolean;\n\n constructor(data: NotificationSettingsViewData) {\n super();\n this.emailNewSponsorships = data.emailNewSponsorships;\n this.emailWeeklyReport = data.emailWeeklyReport;\n this.emailRaceAlerts = data.emailRaceAlerts;\n this.emailPaymentAlerts = data.emailPaymentAlerts;\n this.emailNewOpportunities = data.emailNewOpportunities;\n this.emailContractExpiry = data.emailContractExpiry;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/OnboardingViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":7,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":9,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for Onboarding\n * This is the JSON-serializable input for the Template.\n */\nexport interface OnboardingViewData {\n isAlreadyOnboarded: boolean;\n}\n\nexport class OnboardingViewModel extends ViewModel {\n private readonly data: OnboardingViewData;\n\n constructor(data: OnboardingViewData) {\n super();\n this.data = data;\n }\n\n get isAlreadyOnboarded(): boolean {\n return this.data.isAlreadyOnboarded;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/PaymentMethodViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/PaymentViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":21,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { CurrencyDisplay } from \"../display-objects/CurrencyDisplay\";\n\n/**\n * ViewData for Payment\n * This is the JSON-serializable input for the Template.\n */\nexport interface PaymentViewData {\n id: string;\n type: string;\n amount: number;\n platformFee: number;\n netAmount: number;\n payerId: string;\n payerType: string;\n leagueId: string;\n seasonId?: string;\n status: string;\n createdAt: string;\n completedAt?: string;\n}\n\nexport class PaymentViewModel extends ViewModel {\n private readonly data: PaymentViewData;\n\n constructor(data: PaymentViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get type(): string { return this.data.type; }\n get amount(): number { return this.data.amount; }\n get platformFee(): number { return this.data.platformFee; }\n get netAmount(): number { return this.data.netAmount; }\n get payerId(): string { return this.data.payerId; }\n get payerType(): string { return this.data.payerType; }\n get leagueId(): string { return this.data.leagueId; }\n get seasonId(): string | undefined { return this.data.seasonId; }\n get status(): string { return this.data.status; }\n get createdAt(): string { return this.data.createdAt; }\n get completedAt(): string | undefined { return this.data.completedAt; }\n\n /** UI-specific: Formatted amount */\n get formattedAmount(): string {\n return CurrencyDisplay.format(this.amount, 'EUR');\n }\n\n /** UI-specific: Formatted net amount */\n get formattedNetAmount(): string {\n return CurrencyDisplay.format(this.netAmount, 'EUR');\n }\n\n /** UI-specific: Status color */\n get statusColor(): string {\n switch (this.status) {\n case 'completed': return 'green';\n case 'pending': return 'yellow';\n case 'failed': return 'red';\n case 'refunded': return 'orange';\n default: return 'gray';\n }\n }\n\n /** UI-specific: Formatted created date */\n get formattedCreatedAt(): string {\n return this.createdAt.toLocaleString();\n }\n\n /** UI-specific: Formatted completed date */\n get formattedCompletedAt(): string {\n return this.completedAt ? this.completedAt.toLocaleString() : 'Not completed';\n }\n\n /** UI-specific: Status display */\n get statusDisplay(): string {\n return this.status.charAt(0).toUpperCase() + this.status.slice(1);\n }\n\n /** UI-specific: Type display */\n get typeDisplay(): string {\n return this.type.replace('_', ' ').replace(/\\b\\w/g, l => l.toUpperCase());\n }\n\n /** UI-specific: Payer type display */\n get payerTypeDisplay(): string {\n return this.payerType.charAt(0).toUpperCase() + this.payerType.slice(1);\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/PrivacySettingsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":3,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":8,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface PrivacySettingsViewData {\n publicProfile: boolean;\n showStats: boolean;\n showActiveSponsorships: boolean;\n allowDirectContact: boolean;\n}\n\nexport class PrivacySettingsViewModel extends ViewModel {\n publicProfile: boolean;\n showStats: boolean;\n showActiveSponsorships: boolean;\n allowDirectContact: boolean;\n\n constructor(data: PrivacySettingsViewData) {\n super();\n this.publicProfile = data.publicProfile;\n this.showStats = data.showStats;\n this.showActiveSponsorships = data.showActiveSponsorships;\n this.allowDirectContact = data.allowDirectContact;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/PrizeViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":9,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":22,"endColumn":2},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":19,"column":3,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":19,"endColumn":12},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":41,"column":7,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":41,"endColumn":16},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":41,"column":58,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":41,"endColumn":67}],"suppressedMessages":[],"errorCount":4,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { CurrencyDisplay } from \"../display-objects/CurrencyDisplay\";\nimport { FinishDisplay } from \"../display-objects/FinishDisplay\";\n\n/**\n * ViewData for Prize\n * This is the JSON-serializable input for the Template.\n */\nexport interface PrizeViewData {\n id: string;\n leagueId: string;\n seasonId: string;\n position: number;\n name: string;\n amount: number;\n type: string;\n description?: string;\n awarded: boolean;\n awardedTo?: string;\n awardedAt?: string;\n createdAt: string;\n}\n\nexport class PrizeViewModel extends ViewModel {\n private readonly data: PrizeViewData;\n\n constructor(data: PrizeViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get leagueId(): string { return this.data.leagueId; }\n get seasonId(): string { return this.data.seasonId; }\n get position(): number { return this.data.position; }\n get name(): string { return this.data.name; }\n get amount(): number { return this.data.amount; }\n get type(): string { return this.data.type; }\n get description(): string | undefined { return this.data.description; }\n get awarded(): boolean { return this.data.awarded; }\n get awardedTo(): string | undefined { return this.data.awardedTo; }\n get awardedAt(): string | undefined { return this.data.awardedAt; }\n get createdAt(): string { return this.data.createdAt; }\n\n /** UI-specific: Formatted amount */\n get formattedAmount(): string {\n return CurrencyDisplay.format(this.amount, 'EUR');\n }\n\n /** UI-specific: Position display */\n get positionDisplay(): string {\n return FinishDisplay.format(this.position);\n }\n\n /** UI-specific: Type display */\n get typeDisplay(): string {\n switch (this.type) {\n case 'cash': return 'Cash Prize';\n case 'merchandise': return 'Merchandise';\n case 'other': return 'Other';\n default: return this.type;\n }\n }\n\n /** UI-specific: Status display */\n get statusDisplay(): string {\n return this.awarded ? 'Awarded' : 'Available';\n }\n\n /** UI-specific: Status color */\n get statusColor(): string {\n return this.awarded ? 'green' : 'blue';\n }\n\n /** UI-specific: Prize description */\n get prizeDescription(): string {\n return `${this.name} - ${this.formattedAmount}`;\n }\n\n /** UI-specific: Formatted awarded date */\n get formattedAwardedAt(): string {\n return this.awardedAt ? this.awardedAt.toLocaleString() : 'Not awarded';\n }\n\n /** UI-specific: Formatted created date */\n get formattedCreatedAt(): string {\n return this.createdAt.toLocaleString();\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/ProfileOverviewViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":99,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":106,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface ProfileOverviewDriverSummaryViewModel extends ViewModel {\n id: string;\n name: string;\n country: string;\n avatarUrl: string;\n iracingId: string | null;\n joinedAt: string;\n rating: number | null;\n globalRank: number | null;\n consistency: number | null;\n bio: string | null;\n totalDrivers: number | null;\n}\n\nexport interface ProfileOverviewStatsViewModel extends ViewModel {\n totalRaces: number;\n wins: number;\n podiums: number;\n dnfs: number;\n avgFinish: number | null;\n bestFinish: number | null;\n worstFinish: number | null;\n finishRate: number | null;\n winRate: number | null;\n podiumRate: number | null;\n percentile: number | null;\n rating: number | null;\n consistency: number | null;\n overallRank: number | null;\n}\n\nexport interface ProfileOverviewFinishDistributionViewModel extends ViewModel {\n totalRaces: number;\n wins: number;\n podiums: number;\n topTen: number;\n dnfs: number;\n other: number;\n}\n\nexport interface ProfileOverviewTeamMembershipViewModel extends ViewModel {\n teamId: string;\n teamName: string;\n teamTag: string | null;\n role: string;\n joinedAt: string;\n isCurrent: boolean;\n}\n\nexport interface ProfileOverviewSocialFriendSummaryViewModel extends ViewModel {\n id: string;\n name: string;\n country: string;\n avatarUrl: string;\n}\n\nexport interface ProfileOverviewSocialSummaryViewModel extends ViewModel {\n friendsCount: number;\n friends: ProfileOverviewSocialFriendSummaryViewModel[];\n}\n\nexport type ProfileOverviewSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';\n\nexport type ProfileOverviewAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary';\n\nexport interface ProfileOverviewAchievementViewModel extends ViewModel {\n id: string;\n title: string;\n description: string;\n icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';\n rarity: ProfileOverviewAchievementRarity;\n earnedAt: string;\n}\n\nexport interface ProfileOverviewSocialHandleViewModel extends ViewModel {\n platform: ProfileOverviewSocialPlatform;\n handle: string;\n url: string;\n}\n\nexport interface ProfileOverviewExtendedProfileViewModel extends ViewModel {\n socialHandles: ProfileOverviewSocialHandleViewModel[];\n achievements: ProfileOverviewAchievementViewModel[];\n racingStyle: string;\n favoriteTrack: string;\n favoriteCar: string;\n timezone: string;\n availableHours: string;\n lookingForTeam: boolean;\n openToRequests: boolean;\n}\n\n/**\n * ViewData for ProfileOverview\n * This is the JSON-serializable input for the Template.\n */\nexport interface ProfileOverviewViewData {\n currentDriver: any | null;\n stats: any | null;\n finishDistribution: any | null;\n teamMemberships: any[];\n socialSummary: any;\n extendedProfile: any | null;\n}\n\nexport class ProfileOverviewViewModel extends ViewModel {\n private readonly data: ProfileOverviewViewData;\n\n constructor(data: ProfileOverviewViewData) {\n super();\n this.data = data;\n }\n\n get currentDriver(): ProfileOverviewDriverSummaryViewModel | null { return this.data.currentDriver; }\n get stats(): ProfileOverviewStatsViewModel | null { return this.data.stats; }\n get finishDistribution(): ProfileOverviewFinishDistributionViewModel | null { return this.data.finishDistribution; }\n get teamMemberships(): ProfileOverviewTeamMembershipViewModel[] { return this.data.teamMemberships; }\n get socialSummary(): ProfileOverviewSocialSummaryViewModel { return this.data.socialSummary; }\n get extendedProfile(): ProfileOverviewExtendedProfileViewModel | null { return this.data.extendedProfile; }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/ProtestDetailViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/ProtestDriverViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":3,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":6,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface ProtestDriverViewData {\n id: string;\n name: string;\n}\n\nexport class ProtestDriverViewModel extends ViewModel {\n constructor(private readonly data: ProtestDriverViewData) {\n super();\n }\n\n get id(): string {\n return this.data.id;\n }\n\n get name(): string {\n return this.data.name;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/ProtestViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceDetailEntryViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":79},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/RaceDetailEntryDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":79},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":9,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":16,"endColumn":2}],"suppressedMessages":[],"errorCount":5,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { RaceDetailEntryDTO } from '@/lib/types/generated/RaceDetailEntryDTO';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for RaceDetailEntry\n * This is the JSON-serializable input for the Template.\n */\nexport interface RaceDetailEntryViewData {\n id: string;\n name: string;\n country: string;\n avatarUrl: string;\n isCurrentUser: boolean;\n rating: number | null;\n}\n\nexport class RaceDetailEntryViewModel extends ViewModel {\n private readonly data: RaceDetailEntryViewData;\n\n constructor(data: RaceDetailEntryViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get name(): string { return this.data.name; }\n get country(): string { return this.data.country; }\n get avatarUrl(): string { return this.data.avatarUrl; }\n get isCurrentUser(): boolean { return this.data.isCurrentUser; }\n get rating(): number | null { return this.data.rating; }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceDetailUserResultViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":17,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { DurationDisplay } from \"../display-objects/DurationDisplay\";\n\n/**\n * ViewData for RaceDetailUserResult\n * This is the JSON-serializable input for the Template.\n */\nexport interface RaceDetailUserResultViewData {\n position: number;\n startPosition: number;\n incidents: number;\n fastestLap: number;\n positionChange: number;\n isPodium: boolean;\n isClean: boolean;\n ratingChange: number;\n}\n\nexport class RaceDetailUserResultViewModel extends ViewModel {\n private readonly data: RaceDetailUserResultViewData;\n\n constructor(data: RaceDetailUserResultViewData) {\n super();\n this.data = data;\n }\n\n get position(): number { return this.data.position; }\n get startPosition(): number { return this.data.startPosition; }\n get incidents(): number { return this.data.incidents; }\n get fastestLap(): number { return this.data.fastestLap; }\n get positionChange(): number { return this.data.positionChange; }\n get isPodium(): boolean { return this.data.isPodium; }\n get isClean(): boolean { return this.data.isClean; }\n get ratingChange(): number { return this.data.ratingChange; }\n\n /** UI-specific: Display for position change */\n get positionChangeDisplay(): string {\n if (this.positionChange > 0) return `+${this.positionChange}`;\n if (this.positionChange < 0) return `${this.positionChange}`;\n return '0';\n }\n\n /** UI-specific: Color for position change */\n get positionChangeColor(): string {\n if (this.positionChange > 0) return 'green';\n if (this.positionChange < 0) return 'red';\n return 'gray';\n }\n\n /** UI-specific: Whether this is the winner */\n get isWinner(): boolean {\n return this.position === 1;\n }\n\n /** UI-specific: Rating change display */\n get ratingChangeDisplay(): string {\n if (this.ratingChange > 0) return `+${this.ratingChange}`;\n return `${this.ratingChange}`;\n }\n\n /** UI-specific: Rating change color */\n get ratingChangeColor(): string {\n if (this.ratingChange > 0) return 'green';\n if (this.ratingChange < 0) return 'red';\n return 'gray';\n }\n\n /** UI-specific: Formatted lap time */\n get lapTimeFormatted(): string {\n if (this.fastestLap <= 0) return '--:--.---';\n const minutes = Math.floor(this.fastestLap / 60);\n const seconds = Math.floor(this.fastestLap % 60);\n const milliseconds = Math.floor((this.fastestLap % 1) * 1000);\n return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceDetailsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":5,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":12,"endColumn":2},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":25,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":30,"endColumn":2},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":41,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":44,"endColumn":2},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":57,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":65,"endColumn":2}],"suppressedMessages":[],"errorCount":4,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { RaceDetailEntryViewModel, RaceDetailEntryViewData } from './RaceDetailEntryViewModel';\nimport { RaceDetailUserResultViewModel, RaceDetailUserResultViewData } from './RaceDetailUserResultViewModel';\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface RaceDetailsRaceViewData {\n id: string;\n track: string;\n car: string;\n scheduledAt: string;\n status: string;\n sessionType: string;\n}\n\nexport class RaceDetailsRaceViewModel extends ViewModel {\n private readonly data: RaceDetailsRaceViewData;\n constructor(data: RaceDetailsRaceViewData) { super(); this.data = data; }\n get id(): string { return this.data.id; }\n get track(): string { return this.data.track; }\n get car(): string { return this.data.car; }\n get scheduledAt(): string { return this.data.scheduledAt; }\n get status(): string { return this.data.status; }\n get sessionType(): string { return this.data.sessionType; }\n}\n\nexport interface RaceDetailsLeagueViewData {\n id: string;\n name: string;\n description?: string | null;\n settings?: unknown;\n}\n\nexport class RaceDetailsLeagueViewModel extends ViewModel {\n private readonly data: RaceDetailsLeagueViewData;\n constructor(data: RaceDetailsLeagueViewData) { super(); this.data = data; }\n get id(): string { return this.data.id; }\n get name(): string { return this.data.name; }\n get description(): string | null | undefined { return this.data.description; }\n get settings(): unknown { return this.data.settings; }\n}\n\nexport interface RaceDetailsRegistrationViewData {\n canRegister: boolean;\n isUserRegistered: boolean;\n}\n\nexport class RaceDetailsRegistrationViewModel extends ViewModel {\n private readonly data: RaceDetailsRegistrationViewData;\n constructor(data: RaceDetailsRegistrationViewData) { super(); this.data = data; }\n get canRegister(): boolean { return this.data.canRegister; }\n get isUserRegistered(): boolean { return this.data.isUserRegistered; }\n}\n\n/**\n * ViewData for RaceDetails\n * This is the JSON-serializable input for the Template.\n */\nexport interface RaceDetailsViewData {\n race: RaceDetailsRaceViewData | null;\n league: RaceDetailsLeagueViewData | null;\n entryList: RaceDetailEntryViewData[];\n registration: RaceDetailsRegistrationViewData;\n userResult: RaceDetailUserResultViewData | null;\n canReopenRace: boolean;\n error?: string;\n}\n\nexport class RaceDetailsViewModel extends ViewModel {\n private readonly data: RaceDetailsViewData;\n readonly race: RaceDetailsRaceViewModel | null;\n readonly league: RaceDetailsLeagueViewModel | null;\n readonly entryList: RaceDetailEntryViewModel[];\n readonly registration: RaceDetailsRegistrationViewModel;\n readonly userResult: RaceDetailUserResultViewModel | null;\n\n constructor(data: RaceDetailsViewData) {\n super();\n this.data = data;\n this.race = data.race ? new RaceDetailsRaceViewModel(data.race) : null;\n this.league = data.league ? new RaceDetailsLeagueViewModel(data.league) : null;\n this.entryList = data.entryList.map(e => new RaceDetailEntryViewModel(e));\n this.registration = new RaceDetailsRegistrationViewModel(data.registration);\n this.userResult = data.userResult ? new RaceDetailUserResultViewModel(data.userResult) : null;\n }\n\n get canReopenRace(): boolean { return this.data.canReopenRace; }\n get error(): string | undefined { return this.data.error; }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceListItemViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":20,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { RaceStatusDisplay } from \"../display-objects/RaceStatusDisplay\";\n\n/**\n * ViewData for RaceListItem\n * This is the JSON-serializable input for the Template.\n */\nexport interface RaceListItemViewData {\n id: string;\n track: string;\n car: string;\n scheduledAt: string;\n status: string;\n leagueId: string;\n leagueName: string;\n strengthOfField: number | null;\n isUpcoming: boolean;\n isLive: boolean;\n isPast: boolean;\n}\n\nexport class RaceListItemViewModel extends ViewModel {\n private readonly data: RaceListItemViewData;\n\n constructor(data: RaceListItemViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get track(): string { return this.data.track; }\n get car(): string { return this.data.car; }\n get scheduledAt(): string { return this.data.scheduledAt; }\n get status(): string { return this.data.status; }\n get leagueId(): string { return this.data.leagueId; }\n get leagueName(): string { return this.data.leagueName; }\n get strengthOfField(): number | null { return this.data.strengthOfField; }\n get isUpcoming(): boolean { return this.data.isUpcoming; }\n get isLive(): boolean { return this.data.isLive; }\n get isPast(): boolean { return this.data.isPast; }\n\n get title(): string {\n return `${this.track} - ${this.car}`;\n }\n\n /** UI-specific: Formatted scheduled time */\n get formattedScheduledTime(): string {\n // Client-only formatting using browser locale\n return new Date(this.scheduledAt).toLocaleString();\n }\n\n /** UI-specific: Badge variant for status */\n get statusBadgeVariant(): string {\n return RaceStatusDisplay.getVariant(this.status);\n }\n\n /** UI-specific: Time until start in minutes */\n get timeUntilStart(): number {\n const now = new Date();\n const scheduled = new Date(this.scheduledAt);\n return Math.max(0, Math.floor((scheduled.getTime() - now.getTime()) / (1000 * 60)));\n }\n\n /** UI-specific: Display for time until start */\n get timeUntilStartDisplay(): string {\n const minutes = this.timeUntilStart;\n if (minutes < 60) return `${minutes}m`;\n const hours = Math.floor(minutes / 60);\n return `${hours}h ${minutes % 60}m`;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceResultViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":9,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":22,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { FinishDisplay } from '../display-objects/FinishDisplay';\nimport { DurationDisplay } from '../display-objects/DurationDisplay';\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for RaceResult\n * This is the JSON-serializable input for the Template.\n */\nexport interface RaceResultViewData {\n driverId: string;\n driverName: string;\n avatarUrl: string;\n position: number;\n startPosition: number;\n incidents: number;\n fastestLap: number;\n positionChange: number;\n isPodium: boolean;\n isClean: boolean;\n id: string;\n raceId: string;\n}\n\nexport class RaceResultViewModel extends ViewModel {\n private readonly data: RaceResultViewData;\n\n constructor(data: RaceResultViewData) {\n super();\n this.data = data;\n }\n\n get driverId(): string { return this.data.driverId; }\n get driverName(): string { return this.data.driverName; }\n get avatarUrl(): string { return this.data.avatarUrl; }\n get position(): number { return this.data.position; }\n get startPosition(): number { return this.data.startPosition; }\n get incidents(): number { return this.data.incidents; }\n get fastestLap(): number { return this.data.fastestLap; }\n get positionChange(): number { return this.data.positionChange; }\n get isPodium(): boolean { return this.data.isPodium; }\n get isClean(): boolean { return this.data.isClean; }\n get id(): string { return this.data.id; }\n get raceId(): string { return this.data.raceId; }\n\n /** UI-specific: Display for position change */\n get positionChangeDisplay(): string {\n if (this.positionChange > 0) return `+${this.positionChange}`;\n if (this.positionChange < 0) return `${this.positionChange}`;\n return '0';\n }\n\n /** UI-specific: Color for position change */\n get positionChangeColor(): string {\n if (this.positionChange > 0) return 'green';\n if (this.positionChange < 0) return 'red';\n return 'gray';\n }\n\n /** UI-specific: Whether this is the winner */\n get isWinner(): boolean {\n return this.position === 1;\n }\n\n /** UI-specific: Whether has fastest lap */\n get hasFastestLap(): boolean {\n return this.fastestLap > 0;\n }\n\n /** UI-specific: Badge for position */\n get positionBadge(): string {\n return FinishDisplay.format(this.position);\n }\n\n /** UI-specific: Color for incidents badge */\n get incidentsBadgeColor(): string {\n if (this.incidents === 0) return 'green';\n if (this.incidents <= 2) return 'yellow';\n return 'red';\n }\n\n /** UI-specific: Formatted lap time */\n get lapTimeFormatted(): string {\n if (this.fastestLap <= 0) return '--:--.---';\n return DurationDisplay.formatSeconds(this.fastestLap);\n }\n\n /** Required by ResultsTable */\n getPositionChange(): number {\n return this.positionChange;\n }\n\n get formattedPosition(): string {\n return FinishDisplay.format(this.position);\n }\n\n get formattedStartPosition(): string {\n return FinishDisplay.format(this.startPosition);\n }\n\n get formattedIncidents(): string {\n return `${this.incidents}x incidents`;\n }\n\n get formattedPositionsGained(): string | undefined {\n if (this.position < this.startPosition) {\n return `+${this.startPosition - this.position} positions`;\n }\n return undefined;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceResultsDetailViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":69},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/RaceResultDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":69},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":23},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":23},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":2,"endColumn":83},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/RaceResultsDetailDTO","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":2,"endColumn":83},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":2,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":2,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":2,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":2,"endColumn":30},{"ruleId":"import/no-duplicates","severity":1,"message":"'/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceResultViewModel.ts' imported multiple times.","line":3,"column":37,"nodeType":"Literal","endLine":3,"endColumn":60,"fix":{"range":[181,339],"text":", RaceResultViewData } from './RaceResultViewModel';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n"}},{"ruleId":"import/no-duplicates","severity":1,"message":"'/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceResultViewModel.ts' imported multiple times.","line":7,"column":36,"nodeType":"Literal","endLine":7,"endColumn":59},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":13,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":25,"endColumn":2}],"suppressedMessages":[],"errorCount":9,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":1,"source":"import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO';\nimport { RaceResultsDetailDTO } from '@/lib/types/generated/RaceResultsDetailDTO';\nimport { RaceResultViewModel } from './RaceResultViewModel';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nimport { RaceResultViewData } from './RaceResultViewModel';\n\n/**\n * ViewData for RaceResultsDetail\n * This is the JSON-serializable input for the Template.\n */\nexport interface RaceResultsDetailViewData {\n raceId: string;\n track: string;\n currentUserId: string;\n results: RaceResultViewData[];\n league?: { id: string; name: string };\n race?: { id: string; track: string; scheduledAt: string };\n drivers: { id: string; name: string }[];\n pointsSystem: Record;\n fastestLapTime: number;\n penalties: { driverId: string; type: string; value?: number }[];\n currentDriverId: string;\n}\n\nexport class RaceResultsDetailViewModel extends ViewModel {\n private readonly data: RaceResultsDetailViewData;\n readonly results: RaceResultViewModel[];\n\n constructor(data: RaceResultsDetailViewData) {\n super();\n this.data = data;\n this.results = data.results.map(r => new RaceResultViewModel(r));\n }\n\n get raceId(): string { return this.data.raceId; }\n get track(): string { return this.data.track; }\n get currentUserId(): string { return this.data.currentUserId; }\n get league() { return this.data.league; }\n get race() { return this.data.race; }\n get drivers() { return this.data.drivers; }\n get pointsSystem() { return this.data.pointsSystem; }\n get fastestLapTime(): number { return this.data.fastestLapTime; }\n get penalties() { return this.data.penalties; }\n get currentDriverId(): string { return this.data.currentDriverId; }\n\n /** UI-specific: Results sorted by position */\n get resultsByPosition(): RaceResultViewModel[] {\n return [...this.results].sort((a, b) => a.position - b.position);\n }\n\n /** UI-specific: Results sorted by fastest lap */\n get resultsByFastestLap(): RaceResultViewModel[] {\n return [...this.results].sort((a, b) => a.fastestLap - b.fastestLap);\n }\n\n /** UI-specific: Clean drivers only */\n get cleanDrivers(): RaceResultViewModel[] {\n return this.results.filter(r => r.isClean);\n }\n\n /** UI-specific: Current user's result */\n get currentUserResult(): RaceResultViewModel | undefined {\n return this.results.find(r => r.driverId === this.currentUserId);\n }\n\n /** UI-specific: Race stats */\n get stats(): { totalDrivers: number; cleanRate: number; averageIncidents: number } {\n const total = this.results.length;\n const clean = this.cleanDrivers.length;\n const totalIncidents = this.results.reduce((sum, r) => sum + r.incidents, 0);\n return {\n totalDrivers: total,\n cleanRate: total > 0 ? (clean / total) * 100 : 0,\n averageIncidents: total > 0 ? totalIncidents / total : 0\n };\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceStatsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":72},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/RaceStatsDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":72},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":27},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":27},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":13,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":15,"endColumn":2}],"suppressedMessages":[],"errorCount":5,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { RaceStatsDTO } from '@/lib/types/generated/RaceStatsDTO';\n\n/**\n * Race stats view model\n * Represents race statistics for display\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for RaceStats\n * This is the JSON-serializable input for the Template.\n */\nexport interface RaceStatsViewData {\n totalRaces: number;\n}\n\nexport class RaceStatsViewModel extends ViewModel {\n private readonly data: RaceStatsViewData;\n\n constructor(data: RaceStatsViewData) {\n super();\n this.data = data;\n }\n\n get totalRaces(): number { return this.data.totalRaces; }\n\n /** UI-specific: Formatted total races */\n get formattedTotalRaces(): string {\n // Client-only formatting\n return this.totalRaces.toLocaleString();\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceStewardingViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":7,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":40,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for RaceStewarding\n * This is the JSON-serializable input for the Template.\n */\nexport interface RaceStewardingViewData {\n race: {\n id: string;\n track: string;\n scheduledAt: string;\n status: string;\n } | null;\n league: {\n id: string;\n name: string;\n } | null;\n protests: Array<{\n id: string;\n protestingDriverId: string;\n accusedDriverId: string;\n incident: {\n lap: number;\n description: string;\n };\n filedAt: string;\n status: string;\n decisionNotes?: string;\n proofVideoUrl?: string;\n }>;\n penalties: Array<{\n id: string;\n driverId: string;\n type: string;\n value: number;\n reason: string;\n notes?: string;\n }>;\n driverMap: Record;\n}\n\nexport class RaceStewardingViewModel extends ViewModel {\n private readonly data: RaceStewardingViewData;\n\n constructor(data: RaceStewardingViewData) {\n super();\n this.data = data;\n }\n\n get race() { return this.data.race; }\n get league() { return this.data.league; }\n get protests() { return this.data.protests; }\n get penalties() { return this.data.penalties; }\n get driverMap() { return this.data.driverMap; }\n\n /** UI-specific: Pending protests */\n get pendingProtests() {\n return this.protests.filter(p => p.status === 'pending' || p.status === 'under_review');\n }\n\n /** UI-specific: Resolved protests */\n get resolvedProtests() {\n return this.protests.filter(p =>\n p.status === 'upheld' ||\n p.status === 'dismissed' ||\n p.status === 'withdrawn'\n );\n }\n\n /** UI-specific: Total pending protests count */\n get pendingCount(): number {\n return this.pendingProtests.length;\n }\n\n /** UI-specific: Total resolved protests count */\n get resolvedCount(): number {\n return this.resolvedProtests.length;\n }\n\n /** UI-specific: Total penalties count */\n get penaltiesCount(): number {\n return this.penalties.length;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RaceWithSOFViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":71},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/RaceWithSOFDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":71},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":24},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":24},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":9,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":13,"endColumn":2}],"suppressedMessages":[],"errorCount":5,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { RaceWithSOFDTO } from '@/lib/types/generated/RaceWithSOFDTO';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * ViewData for RaceWithSOF\n * This is the JSON-serializable input for the Template.\n */\nexport interface RaceWithSOFViewData {\n id: string;\n track: string;\n strengthOfField: number | null;\n}\n\nexport class RaceWithSOFViewModel extends ViewModel {\n private readonly data: RaceWithSOFViewData;\n\n constructor(data: RaceWithSOFViewData) {\n super();\n this.data = data;\n }\n\n get id(): string { return this.data.id; }\n get track(): string { return this.data.track; }\n get strengthOfField(): number | null { return this.data.strengthOfField; }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RacesPageViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RecordEngagementInputViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":7,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":11,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Record engagement input view model\n * Represents input data for recording an engagement event\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface RecordEngagementInputViewData {\n eventType: string;\n userId?: string;\n metadata?: Record;\n}\n\nexport class RecordEngagementInputViewModel extends ViewModel {\n eventType: string;\n userId?: string;\n metadata?: Record;\n\n constructor(data: RecordEngagementInputViewData) {\n super();\n this.eventType = data.eventType;\n this.userId = data.userId;\n this.metadata = data.metadata;\n }\n\n /** UI-specific: Formatted event type for display */\n get displayEventType(): string {\n return this.eventType.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\n }\n\n /** UI-specific: Has metadata */\n get hasMetadata(): boolean {\n return this.metadata !== undefined && Object.keys(this.metadata).length > 0;\n }\n\n /** UI-specific: Metadata keys count */\n get metadataKeysCount(): number {\n return this.metadata ? Object.keys(this.metadata).length : 0;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RecordEngagementOutputViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RecordPageViewInputViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":7,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":10,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Record page view input view model\n * Represents input data for recording a page view\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface RecordPageViewInputViewData {\n path: string;\n userId?: string;\n}\n\nexport class RecordPageViewInputViewModel extends ViewModel {\n path: string;\n userId?: string;\n\n constructor(data: RecordPageViewInputViewData) {\n super();\n this.path = data.path;\n this.userId = data.userId;\n }\n\n /** UI-specific: Formatted path for display */\n get displayPath(): string {\n return this.path.startsWith('/') ? this.path : `/${this.path}`;\n }\n\n /** UI-specific: Has user context */\n get hasUserContext(): boolean {\n return this.userId !== undefined && this.userId !== '';\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RecordPageViewOutputViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":3,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":5,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface RecordPageViewOutputViewData {\n pageViewId: string;\n}\n\nexport class RecordPageViewOutputViewModel extends ViewModel {\n pageViewId: string;\n\n constructor(data: RecordPageViewOutputViewData) {\n super();\n this.pageViewId = data.pageViewId;\n }\n\n /** UI-specific: Formatted page view ID for display */\n get displayPageViewId(): string {\n return `Page View: ${this.pageViewId}`;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RemoveMemberViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":10,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * View Model for Remove Member Result\n *\n * Represents the result of removing a member from a league in a UI-ready format.\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface RemoveMemberViewData {\n success: boolean;\n}\n\nexport class RemoveMemberViewModel extends ViewModel {\n success: boolean;\n\n constructor(data: RemoveMemberViewData) {\n super();\n this.success = data.success;\n }\n\n /** UI-specific: Success message */\n get successMessage(): string {\n return this.success ? 'Member removed successfully!' : 'Failed to remove member.';\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RenewalAlertViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":14,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Renewal Alert View Model\n *\n * View model for upcoming renewal alerts.\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface RenewalAlertViewData {\n id: string;\n name: string;\n type: 'league' | 'team' | 'driver' | 'race' | 'platform';\n renewDate: string;\n price: number;\n}\n\nexport class RenewalAlertViewModel extends ViewModel {\n id: string;\n name: string;\n type: 'league' | 'team' | 'driver' | 'race' | 'platform';\n renewDate: Date;\n price: number;\n\n constructor(data: RenewalAlertViewData) {\n super();\n this.id = data.id;\n this.name = data.name;\n this.type = data.type;\n this.renewDate = new Date(data.renewDate);\n this.price = data.price;\n }\n\n get formattedPrice(): string {\n return `$${this.price}`;\n }\n\n get formattedRenewDate(): string {\n return this.renewDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });\n }\n\n get typeIcon() {\n const icons = {\n league: 'Trophy',\n team: 'Users',\n driver: 'Car',\n race: 'Flag',\n platform: 'Megaphone',\n };\n return icons[this.type] || 'Trophy';\n }\n\n get daysUntilRenewal(): number {\n const now = new Date();\n const diffTime = this.renewDate.getTime() - now.getTime();\n return Math.ceil(diffTime / (1000 * 60 * 60 * 24));\n }\n\n get isUrgent(): boolean {\n return this.daysUntilRenewal <= 30;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/RequestAvatarGenerationViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":107},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/RequestAvatarGenerationOutputDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":107},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":42},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":42},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":17,"column":5,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":26,"endColumn":10},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":18,"column":9,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":18,"endColumn":41},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":29,"column":20,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":29,"endColumn":23},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":31,"column":24,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":31,"endColumn":27},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":31,"column":31,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":31,"endColumn":34},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":31,"column":77,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":31,"endColumn":80},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":33,"column":25,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":33,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":33,"column":32,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":33,"endColumn":35},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":34,"column":25,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":34,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":35,"column":31,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":35,"endColumn":34},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":35,"column":38,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":35,"endColumn":41},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":36,"column":26,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":36,"endColumn":29},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":39,"column":27,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":39,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":39,"column":34,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":39,"endColumn":37},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":40,"column":27,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":40,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":41,"column":27,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":41,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":41,"column":34,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":41,"endColumn":37},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":42,"column":27,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":42,"endColumn":30}],"suppressedMessages":[],"errorCount":22,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';\n\n/**\n * Request Avatar Generation View Model\n *\n * Represents the result of an avatar generation request\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport class RequestAvatarGenerationViewModel extends ViewModel {\n success: boolean;\n requestId?: string;\n avatarUrls?: string[];\n errorMessage?: string;\n\n constructor(\n dto:\n | RequestAvatarGenerationOutputDTO\n | {\n success: boolean;\n requestId?: string;\n avatarUrls?: string[];\n errorMessage?: string;\n avatarUrl?: string;\n error?: string;\n },\n ) {\n super();\n this.success = dto.success;\n\n if ('requestId' in dto && dto.requestId !== undefined) this.requestId = dto.requestId;\n\n if ('avatarUrls' in dto && dto.avatarUrls !== undefined) {\n this.avatarUrls = dto.avatarUrls;\n } else if ('avatarUrl' in dto && dto.avatarUrl !== undefined) {\n this.avatarUrls = [dto.avatarUrl];\n }\n\n if ('errorMessage' in dto && dto.errorMessage !== undefined) {\n this.errorMessage = dto.errorMessage;\n } else if ('error' in dto && dto.error !== undefined) {\n this.errorMessage = dto.error;\n }\n }\n\n /** UI-specific: Whether generation was successful */\n get isSuccessful(): boolean {\n return this.success;\n }\n\n /** UI-specific: Whether there was an error */\n get hasError(): boolean {\n return !!this.errorMessage;\n }\n\n /** UI-specific: Get first avatar URL */\n get firstAvatarUrl(): string | undefined {\n return this.avatarUrls?.[0];\n }\n\n get avatarUrl(): string | undefined {\n return this.firstAvatarUrl;\n }\n\n get error(): string | undefined {\n return this.errorMessage;\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/ScoringConfigurationViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/LeagueConfigFormModel","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":80}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';\nimport type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';\n\nexport interface CustomPointsConfig {\n racePoints: number[];\n poleBonusPoints: number;\n fastestLapPoints: number;\n leaderLapPoints: number;\n}\n\n/**\n * ScoringConfigurationViewModel\n *\n * View model for scoring configuration including presets and custom points\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport class ScoringConfigurationViewModel extends ViewModel {\n readonly patternId?: string;\n readonly customScoringEnabled: boolean;\n readonly customPoints?: CustomPointsConfig;\n readonly currentPreset?: LeagueScoringPresetViewModel;\n\n constructor(\n config: LeagueConfigFormModel['scoring'],\n presets: LeagueScoringPresetViewModel[],\n customPoints?: CustomPointsConfig\n ) {\n this.patternId = config.patternId;\n this.customScoringEnabled = config.customScoringEnabled || false;\n this.customPoints = customPoints;\n this.currentPreset = config.patternId \n ? presets.find(p => p.id === config.patternId)\n : undefined;\n }\n\n /**\n * Get the active points configuration\n */\n getActivePointsConfig(): CustomPointsConfig {\n if (this.customScoringEnabled && this.customPoints) {\n return this.customPoints;\n }\n // Return default points if no custom config\n return {\n racePoints: [25, 18, 15, 12, 10, 8, 6, 4, 2, 1],\n poleBonusPoints: 1,\n fastestLapPoints: 1,\n leaderLapPoints: 0,\n };\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/SessionViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":83},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/AuthenticatedUserDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":83},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":10,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":14,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":14,"endColumn":40},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":14,"column":20,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":14,"endColumn":40},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":16,"column":19,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":16,"endColumn":22},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":17,"column":18,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":17,"endColumn":21},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":18,"column":24,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":18,"endColumn":27},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":21,"column":9,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":21,"endColumn":12},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":22,"column":23,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":22,"endColumn":26},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":25,"column":9,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":25,"endColumn":12},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":26,"column":24,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":26,"endColumn":27},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":29,"column":9,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":29,"endColumn":12},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":30,"column":19,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":30,"endColumn":22}],"suppressedMessages":[],"errorCount":15,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { AuthenticatedUserDTO } from '@/lib/types/generated/AuthenticatedUserDTO';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport class SessionViewModel extends ViewModel {\n userId: string;\n email: string;\n displayName: string;\n avatarUrl?: string | null;\n role?: string;\n driverId?: string;\n isAuthenticated: boolean = true;\n\n constructor(dto: AuthenticatedUserDTO) {\n super();\n this.userId = dto.userId;\n this.email = dto.email;\n this.displayName = dto.displayName;\n\n // Use the optional fields from the DTO\n if (dto.primaryDriverId) {\n this.driverId = dto.primaryDriverId;\n }\n \n if (dto.avatarUrl !== undefined) {\n this.avatarUrl = dto.avatarUrl;\n }\n \n if (dto.role) {\n this.role = dto.role;\n }\n }\n\n /**\n * Compatibility accessor.\n * Some legacy components expect `session.user.*`.\n */\n get user(): {\n userId: string;\n email: string;\n displayName: string;\n primaryDriverId?: string | null;\n avatarUrl?: string | null;\n } {\n return {\n userId: this.userId,\n email: this.email,\n displayName: this.displayName,\n primaryDriverId: this.driverId ?? null,\n avatarUrl: this.avatarUrl,\n };\n }\n\n /** UI-specific: User greeting */\n get greeting(): string {\n return `Hello, ${this.displayName}!`;\n }\n\n /** UI-specific: Avatar initials */\n get avatarInitials(): string {\n if (this.displayName) {\n return this.displayName.split(' ').map(n => n[0]).join('').toUpperCase();\n }\n return (this.email?.[0] ?? '?').toUpperCase();\n }\n\n /** UI-specific: Whether has driver profile */\n get hasDriverProfile(): boolean {\n return !!this.driverId;\n }\n\n /** UI-specific: Authentication status display */\n get authStatusDisplay(): string {\n return this.isAuthenticated ? 'Logged In' : 'Logged Out';\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/SponsorDashboardViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":11,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Sponsor Dashboard View Model\n *\n * Represents dashboard data for a sponsor with UI-specific transformations.\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface SponsorDashboardViewData {\n sponsorId: string;\n sponsorName: string;\n}\n\nexport class SponsorDashboardViewModel extends ViewModel {\n sponsorId: string;\n sponsorName: string;\n\n constructor(data: SponsorDashboardViewData) {\n super();\n this.sponsorId = data.sponsorId;\n this.sponsorName = data.sponsorName;\n }\n\n /** UI-specific: Welcome message */\n get welcomeMessage(): string {\n return `Welcome back, ${this.sponsorName}!`;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/SponsorProfileViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":3,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":24,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface SponsorProfileViewData {\n companyName: string;\n contactName: string;\n contactEmail: string;\n contactPhone: string;\n website: string;\n description: string;\n logoUrl: string | null;\n industry: string;\n address: {\n street: string;\n city: string;\n country: string;\n postalCode: string;\n };\n taxId: string;\n socialLinks: {\n twitter: string;\n linkedin: string;\n instagram: string;\n };\n}\n\nexport class SponsorProfileViewModel extends ViewModel {\n companyName: string;\n contactName: string;\n contactEmail: string;\n contactPhone: string;\n website: string;\n description: string;\n logoUrl: string | null;\n industry: string;\n address: {\n street: string;\n city: string;\n country: string;\n postalCode: string;\n };\n taxId: string;\n socialLinks: {\n twitter: string;\n linkedin: string;\n instagram: string;\n };\n\n constructor(data: SponsorProfileViewData) {\n super();\n this.companyName = data.companyName;\n this.contactName = data.contactName;\n this.contactEmail = data.contactEmail;\n this.contactPhone = data.contactPhone;\n this.website = data.website;\n this.description = data.description;\n this.logoUrl = data.logoUrl;\n this.industry = data.industry;\n this.address = data.address;\n this.taxId = data.taxId;\n this.socialLinks = data.socialLinks;\n }\n\n get fullAddress(): string {\n return `${this.address.street}, ${this.address.city}, ${this.address.postalCode}, ${this.address.country}`;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/SponsorSettingsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":11,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":15,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Sponsor Settings View Model\n *\n * View model for sponsor settings data.\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { SponsorProfileViewModel, SponsorProfileViewData } from \"./SponsorProfileViewModel\";\nimport { NotificationSettingsViewModel, NotificationSettingsViewData } from \"./NotificationSettingsViewModel\";\nimport { PrivacySettingsViewModel, PrivacySettingsViewData } from \"./PrivacySettingsViewModel\";\n\nexport interface SponsorSettingsViewData {\n profile: SponsorProfileViewData;\n notifications: NotificationSettingsViewData;\n privacy: PrivacySettingsViewData;\n}\n\nexport class SponsorSettingsViewModel extends ViewModel {\n profile: SponsorProfileViewModel;\n notifications: NotificationSettingsViewModel;\n privacy: PrivacySettingsViewModel;\n\n constructor(data: SponsorSettingsViewData) {\n super();\n this.profile = new SponsorProfileViewModel(data.profile);\n this.notifications = new NotificationSettingsViewModel(data.notifications);\n this.privacy = new PrivacySettingsViewModel(data.privacy);\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/SponsorSponsorshipsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":9,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":13,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel';\n\n/**\n * Sponsor Sponsorships View Model\n *\n * View model for sponsor sponsorships data with UI-specific transformations.\n */\nexport interface SponsorSponsorshipsViewData {\n sponsorId: string;\n sponsorName: string;\n sponsorships: any[]; // Will be mapped to SponsorshipDetailViewModel\n}\n\nexport class SponsorSponsorshipsViewModel extends ViewModel {\n sponsorId: string;\n sponsorName: string;\n sponsorships: SponsorshipDetailViewModel[];\n\n constructor(data: SponsorSponsorshipsViewData) {\n super();\n this.sponsorId = data.sponsorId;\n this.sponsorName = data.sponsorName;\n this.sponsorships = (data.sponsorships || []).map(s => new SponsorshipDetailViewModel(s));\n }\n\n /** UI-specific: Total sponsorships count */\n get totalCount(): number {\n return this.sponsorships.length;\n }\n\n /** UI-specific: Active sponsorships */\n get activeSponsorships(): SponsorshipDetailViewModel[] {\n return this.sponsorships.filter(s => s.status === 'active');\n }\n\n /** UI-specific: Active count */\n get activeCount(): number {\n return this.activeSponsorships.length;\n }\n\n /** UI-specific: Has sponsorships */\n get hasSponsorships(): boolean {\n return this.sponsorships.length > 0;\n }\n\n /** UI-specific: Total investment */\n get totalInvestment(): number {\n return this.sponsorships.reduce((sum, s) => sum + s.amount, 0);\n }\n\n /** UI-specific: Formatted total investment */\n get formattedTotalInvestment(): string {\n const firstCurrency = this.sponsorships[0]?.currency || 'USD';\n return `${firstCurrency} ${this.totalInvestment.toLocaleString()}`;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/SponsorViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":2,"column":11,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":2,"endColumn":21},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":17,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":17,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":17,"column":20,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":17,"endColumn":30},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":18,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":18,"endColumn":18},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":19,"column":17,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":19,"endColumn":20},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":20,"column":9,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":20,"endColumn":12},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":20,"column":51,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":20,"endColumn":54},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":21,"column":9,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":21,"endColumn":12},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":21,"column":57,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":21,"endColumn":60}],"suppressedMessages":[],"errorCount":9,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Note: No generated DTO available for Sponsor yet\ninterface SponsorDTO {\n id: string;\n name: string;\n logoUrl?: string;\n websiteUrl?: string;\n}\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport class SponsorViewModel extends ViewModel {\n id: string;\n name: string;\n declare logoUrl?: string;\n declare websiteUrl?: string;\n\n constructor(dto: SponsorDTO) {\n this.id = dto.id;\n this.name = dto.name;\n if (dto.logoUrl !== undefined) this.logoUrl = dto.logoUrl;\n if (dto.websiteUrl !== undefined) this.websiteUrl = dto.websiteUrl;\n }\n\n /** UI-specific: Display name */\n get displayName(): string {\n return this.name;\n }\n\n /** UI-specific: Whether has website */\n get hasWebsite(): boolean {\n return !!this.websiteUrl;\n }\n\n /** UI-specific: Website link text */\n get websiteLinkText(): string {\n return 'Visit Website';\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/SponsorshipDetailViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":3,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":17,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface SponsorshipDetailViewData {\n id: string;\n leagueId: string;\n leagueName: string;\n seasonId: string;\n seasonName: string;\n tier: 'main' | 'secondary';\n status: string;\n amount: number;\n currency: string;\n type: string;\n entityName: string;\n price: number;\n impressions: number;\n}\n\nexport class SponsorshipDetailViewModel extends ViewModel {\n id: string;\n leagueId: string;\n leagueName: string;\n seasonId: string;\n seasonName: string;\n tier: 'main' | 'secondary';\n status: string;\n amount: number;\n currency: string;\n type: string;\n entityName: string;\n price: number;\n impressions: number;\n\n constructor(data: SponsorshipDetailViewData) {\n super();\n this.id = data.id;\n this.leagueId = data.leagueId;\n this.leagueName = data.leagueName;\n this.seasonId = data.seasonId;\n this.seasonName = data.seasonName;\n this.tier = data.tier;\n this.status = data.status;\n this.amount = data.amount;\n this.currency = data.currency;\n this.type = data.type;\n this.entityName = data.entityName;\n this.price = data.price;\n this.impressions = data.impressions;\n }\n\n /** UI-specific: Formatted amount */\n get formattedAmount(): string {\n return `${this.currency} ${this.amount.toLocaleString()}`;\n }\n\n /** UI-specific: Tier badge variant */\n get tierBadgeVariant(): string {\n return this.tier === 'main' ? 'primary' : 'secondary';\n }\n\n /** UI-specific: Status color */\n get statusColor(): string {\n switch (this.status) {\n case 'active': return 'green';\n case 'pending': return 'yellow';\n case 'expired': return 'red';\n default: return 'gray';\n }\n }\n\n /** UI-specific: Status display */\n get statusDisplay(): string {\n return this.status.charAt(0).toUpperCase() + this.status.slice(1);\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/SponsorshipPricingViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":12,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Sponsorship Pricing View Model\n *\n * View model for sponsorship pricing data with UI-specific transformations.\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface SponsorshipPricingViewData {\n mainSlotPrice: number;\n secondarySlotPrice: number;\n currency: string;\n}\n\nexport class SponsorshipPricingViewModel extends ViewModel {\n mainSlotPrice: number;\n secondarySlotPrice: number;\n currency: string;\n\n constructor(data: SponsorshipPricingViewData) {\n super();\n this.mainSlotPrice = data.mainSlotPrice;\n this.secondarySlotPrice = data.secondarySlotPrice;\n this.currency = data.currency;\n }\n\n /** UI-specific: Formatted main slot price */\n get formattedMainSlotPrice(): string {\n return `${this.currency} ${this.mainSlotPrice.toLocaleString()}`;\n }\n\n /** UI-specific: Formatted secondary slot price */\n get formattedSecondarySlotPrice(): string {\n return `${this.currency} ${this.secondarySlotPrice.toLocaleString()}`;\n }\n\n /** UI-specific: Price difference */\n get priceDifference(): number {\n return this.mainSlotPrice - this.secondarySlotPrice;\n }\n\n /** UI-specific: Formatted price difference */\n get formattedPriceDifference(): string {\n return `${this.currency} ${this.priceDifference.toLocaleString()}`;\n }\n\n /** UI-specific: Discount percentage for secondary slot */\n get secondaryDiscountPercentage(): number {\n if (this.mainSlotPrice === 0) return 0;\n return Math.round((1 - this.secondarySlotPrice / this.mainSlotPrice) * 100);\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/SponsorshipRequestViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":3,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":16,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface SponsorshipRequestViewData {\n id: string;\n sponsorId: string;\n sponsorName: string;\n sponsorLogo?: string;\n tier: 'main' | 'secondary';\n offeredAmount: number;\n currency: string;\n formattedAmount: string;\n message?: string;\n createdAt: string;\n platformFee: number;\n netAmount: number;\n}\n\nexport class SponsorshipRequestViewModel extends ViewModel {\n id: string;\n sponsorId: string;\n sponsorName: string;\n sponsorLogo?: string;\n tier: 'main' | 'secondary';\n offeredAmount: number;\n currency: string;\n formattedAmount: string;\n message?: string;\n createdAt: Date;\n platformFee: number;\n netAmount: number;\n\n constructor(data: SponsorshipRequestViewData) {\n super();\n this.id = data.id;\n this.sponsorId = data.sponsorId;\n this.sponsorName = data.sponsorName;\n this.sponsorLogo = data.sponsorLogo;\n this.tier = data.tier;\n this.offeredAmount = data.offeredAmount;\n this.currency = data.currency;\n this.formattedAmount = data.formattedAmount;\n this.message = data.message;\n this.createdAt = new Date(data.createdAt);\n this.platformFee = data.platformFee;\n this.netAmount = data.netAmount;\n }\n\n /** UI-specific: Formatted date */\n get formattedDate(): string {\n return this.createdAt.toLocaleDateString('en-US', {\n month: 'short',\n day: 'numeric',\n });\n }\n\n /** UI-specific: Net amount in dollars */\n get netAmountDollars(): string {\n return `$${(this.netAmount / 100).toFixed(2)}`;\n }\n\n /** UI-specific: Tier display */\n get tierDisplay(): string {\n return this.tier === 'main' ? 'Main Sponsor' : 'Secondary';\n }\n\n /** UI-specific: Tier badge variant */\n get tierBadgeVariant(): 'primary' | 'secondary' {\n return this.tier === 'main' ? 'primary' : 'secondary';\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/SponsorshipViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":9,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":28,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { CurrencyDisplay } from '@/lib/display-objects/CurrencyDisplay';\nimport { DateDisplay } from '@/lib/display-objects/DateDisplay';\nimport { NumberDisplay } from '@/lib/display-objects/NumberDisplay';\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\n/**\n * Interface for sponsorship data input\n */\nexport interface SponsorshipViewData {\n id: string;\n type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';\n entityId: string;\n entityName: string;\n tier?: 'main' | 'secondary';\n status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';\n applicationDate?: string | Date;\n approvalDate?: string | Date;\n rejectionReason?: string;\n startDate: string | Date;\n endDate: string | Date;\n price: number;\n impressions: number;\n impressionsChange?: number;\n engagement?: number;\n details?: string;\n entityOwner?: string;\n applicationMessage?: string;\n}\n\n/**\n * Sponsorship View Model\n *\n * View model for individual sponsorship data.\n */\nexport class SponsorshipViewModel extends ViewModel {\n id: string;\n type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';\n entityId: string;\n entityName: string;\n tier?: 'main' | 'secondary';\n status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';\n applicationDate?: Date;\n approvalDate?: Date;\n rejectionReason?: string;\n startDate: Date;\n endDate: Date;\n price: number;\n impressions: number;\n impressionsChange?: number;\n engagement?: number;\n details?: string;\n entityOwner?: string;\n applicationMessage?: string;\n\n constructor(data: SponsorshipViewData) {\n super();\n this.id = data.id;\n this.type = data.type;\n this.entityId = data.entityId;\n this.entityName = data.entityName;\n this.tier = data.tier;\n this.status = data.status;\n this.applicationDate = data.applicationDate ? new Date(data.applicationDate) : undefined;\n this.approvalDate = data.approvalDate ? new Date(data.approvalDate) : undefined;\n this.rejectionReason = data.rejectionReason;\n this.startDate = new Date(data.startDate);\n this.endDate = new Date(data.endDate);\n this.price = data.price;\n this.impressions = data.impressions;\n this.impressionsChange = data.impressionsChange;\n this.engagement = data.engagement;\n this.details = data.details;\n this.entityOwner = data.entityOwner;\n this.applicationMessage = data.applicationMessage;\n }\n\n get formattedImpressions(): string {\n return NumberDisplay.format(this.impressions);\n }\n\n get formattedPrice(): string {\n return CurrencyDisplay.format(this.price);\n }\n\n get daysRemaining(): number {\n const now = new Date();\n const diffTime = this.endDate.getTime() - now.getTime();\n return Math.ceil(diffTime / (1000 * 60 * 60 * 24));\n }\n\n get isExpiringSoon(): boolean {\n return this.daysRemaining > 0 && this.daysRemaining <= 30;\n }\n\n get statusLabel(): string {\n const labels = {\n active: 'Active',\n pending_approval: 'Awaiting Approval',\n approved: 'Approved',\n rejected: 'Declined',\n expired: 'Expired',\n };\n return labels[this.status] || this.status;\n }\n\n get typeLabel(): string {\n const labels = {\n leagues: 'League',\n teams: 'Team',\n drivers: 'Driver',\n races: 'Race',\n platform: 'Platform',\n };\n return labels[this.type] || this.type;\n }\n\n get periodDisplay(): string {\n const start = DateDisplay.formatMonthYear(this.startDate);\n const end = DateDisplay.formatMonthYear(this.endDate);\n return `${start} - ${end}`;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/StandingEntryViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":20,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../contracts/view-models/ViewModel\";\nimport { FinishDisplay } from \"../display-objects/FinishDisplay\";\n\n/**\n * ViewData for StandingEntry\n * This is the JSON-serializable input for the Template.\n */\nexport interface StandingEntryViewData {\n driverId: string;\n position: number;\n points: number;\n wins: number;\n podiums: number;\n races: number;\n leaderPoints: number;\n nextPoints: number;\n currentUserId: string;\n previousPosition?: number;\n driver?: any;\n}\n\nexport class StandingEntryViewModel extends ViewModel {\n private readonly data: StandingEntryViewData;\n\n constructor(data: StandingEntryViewData) {\n super();\n this.data = data;\n }\n\n get driverId(): string { return this.data.driverId; }\n get position(): number { return this.data.position; }\n get points(): number { return this.data.points; }\n get wins(): number { return this.data.wins; }\n get podiums(): number { return this.data.podiums; }\n get races(): number { return this.data.races; }\n get driver(): any { return this.data.driver; }\n\n /** UI-specific: Badge for position display */\n get positionBadge(): string {\n return FinishDisplay.format(this.position);\n }\n\n /** UI-specific: Points difference to leader */\n get pointsGapToLeader(): number {\n return this.points - this.data.leaderPoints;\n }\n\n /** UI-specific: Points difference to next position */\n get pointsGapToNext(): number {\n return this.points - this.data.nextPoints;\n }\n\n /** UI-specific: Whether this entry is the current user */\n get isCurrentUser(): boolean {\n return this.driverId === this.data.currentUserId;\n }\n\n /** UI-specific: Trend compared to previous */\n get trend(): 'up' | 'down' | 'same' {\n if (!this.data.previousPosition) return 'same';\n if (this.position < this.data.previousPosition) return 'up';\n if (this.position > this.data.previousPosition) return 'down';\n return 'same';\n }\n\n /** UI-specific: Arrow for trend */\n get trendArrow(): string {\n switch (this.trend) {\n case 'up': return '↑';\n case 'down': return '↓';\n default: return '-';\n }\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/TeamCardViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/TeamDetailsViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":94},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/GetTeamDetailsOutputDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":94},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":38},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":38},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":21,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":21,"endColumn":43},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":21,"column":20,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":21,"endColumn":43},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":22,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":22,"endColumn":18},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":23,"column":17,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":23,"endColumn":20},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":24,"column":16,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":24,"endColumn":19},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":25,"column":24,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":25,"endColumn":27},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":26,"column":20,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":26,"endColumn":23},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":27,"column":20,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":27,"endColumn":23},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":28,"column":22,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":28,"endColumn":25},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":30,"column":24,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":30,"endColumn":27},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":30,"column":43,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":30,"endColumn":46},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":41,"column":23,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":41,"endColumn":26},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":42,"column":13,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":42,"endColumn":16},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":43,"column":17,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":43,"endColumn":20},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":44,"column":17,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":44,"endColumn":20},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":46,"column":23,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":46,"endColumn":26}],"suppressedMessages":[],"errorCount":20,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport class TeamDetailsViewModel extends ViewModel {\n id!: string;\n name!: string;\n tag!: string;\n description?: string;\n ownerId!: string;\n leagues!: string[];\n createdAt: string | undefined;\n specialization: string | undefined;\n region: string | undefined;\n languages: string[] | undefined;\n category: string | undefined;\n membership: { role: string; joinedAt: string; isActive: boolean } | null;\n private _canManage: boolean;\n private currentUserId: string;\n\n constructor(dto: GetTeamDetailsOutputDTO, currentUserId: string) {\n this.id = dto.team.id;\n this.name = dto.team.name;\n this.tag = dto.team.tag;\n this.description = dto.team.description;\n this.ownerId = dto.team.ownerId;\n this.leagues = dto.team.leagues;\n this.createdAt = dto.team.createdAt;\n\n const teamExtras = dto.team as typeof dto.team & {\n specialization?: string;\n region?: string;\n languages?: string[];\n category?: string;\n };\n\n this.specialization = teamExtras.specialization ?? undefined;\n this.region = teamExtras.region ?? undefined;\n this.languages = teamExtras.languages ?? undefined;\n this.category = teamExtras.category ?? undefined;\n this.membership = dto.membership ? {\n role: dto.membership.role,\n joinedAt: dto.membership.joinedAt,\n isActive: dto.membership.isActive\n } : null;\n this._canManage = dto.canManage;\n this.currentUserId = currentUserId;\n }\n\n /** UI-specific: Whether current user is owner */\n get isOwner(): boolean {\n return this.membership?.role === 'owner';\n }\n\n /** UI-specific: Whether can manage team */\n get canManage(): boolean {\n return this._canManage;\n }\n\n /** UI-specific: Whether current user is member */\n get isMember(): boolean {\n return this.membership !== null;\n }\n\n /** UI-specific: Current user's role */\n get userRole(): string {\n return this.membership?.role || 'none';\n }\n}","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/TeamJoinRequestViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":84},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/TeamJoinRequestDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":84},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":33},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":33},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":17,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":17,"endColumn":38},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":17,"column":20,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":17,"endColumn":38},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":18,"column":22,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":18,"endColumn":25},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":19,"column":21,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":19,"endColumn":24},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":20,"column":23,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":20,"endColumn":26},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":21,"column":19,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":21,"endColumn":22},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":22,"column":26,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":22,"endColumn":29},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":23,"column":24,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":23,"endColumn":27},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":24,"column":22,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":24,"endColumn":25}],"suppressedMessages":[],"errorCount":13,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { TeamJoinRequestDTO } from '@/lib/types/generated/TeamJoinRequestDTO';\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport class TeamJoinRequestViewModel extends ViewModel {\n requestId: string;\n driverId: string;\n driverName: string;\n teamId: string;\n requestStatus: string;\n requestedAt: string;\n avatarUrl: string;\n\n private readonly currentUserId: string;\n private readonly isOwner: boolean;\n\n constructor(dto: TeamJoinRequestDTO, currentUserId: string, isOwner: boolean) {\n this.requestId = dto.requestId;\n this.driverId = dto.driverId;\n this.driverName = dto.driverName;\n this.teamId = dto.teamId;\n this.requestStatus = dto.status;\n this.requestedAt = dto.requestedAt;\n this.avatarUrl = dto.avatarUrl || '';\n this.currentUserId = currentUserId;\n this.isOwner = isOwner;\n }\n\n get id(): string {\n return this.requestId;\n }\n\n get status(): string {\n if (this.requestStatus === 'pending') return 'Pending';\n if (this.requestStatus === 'approved') return 'Approved';\n if (this.requestStatus === 'rejected') return 'Rejected';\n return this.requestStatus;\n }\n\n /** UI-specific: Whether current user can approve */\n get canApprove(): boolean {\n return this.isOwner;\n }\n\n /** UI-specific: Formatted requested date */\n get formattedRequestedAt(): string {\n return new Date(this.requestedAt).toLocaleString();\n }\n\n /** UI-specific: Status color */\n get statusColor(): string {\n if (this.requestStatus === 'approved') return 'green';\n if (this.requestStatus === 'rejected') return 'red';\n return 'yellow';\n }\n\n /** UI-specific: Approve button text */\n get approveButtonText(): string {\n return 'Approve';\n }\n\n /** UI-specific: Reject button text */\n get rejectButtonText(): string {\n return 'Reject';\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/TeamMemberViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"noDtoImport","endLine":1,"endColumn":74},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: @/lib/types/generated/TeamMemberDTO","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":1,"endColumn":74},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":1,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":1,"endColumn":28},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":25,"column":15,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":25,"endColumn":33},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":25,"column":20,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":25,"endColumn":33},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":26,"column":21,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":26,"endColumn":24},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":27,"column":23,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":27,"endColumn":26},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":28,"column":35,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":28,"endColumn":38},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":29,"column":21,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":29,"endColumn":24},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":30,"column":21,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":30,"endColumn":24},{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels must not use the word \"DTO\". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.","line":31,"column":22,"nodeType":"Identifier","messageId":"noDtoInViewModel","endLine":31,"endColumn":25}],"suppressedMessages":[],"errorCount":12,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { TeamMemberDTO } from '@/lib/types/generated/TeamMemberDTO';\n\ntype TeamMemberRole = 'owner' | 'manager' | 'member';\n\nfunction normalizeTeamRole(role: string): TeamMemberRole {\n if (role === 'owner' || role === 'manager' || role === 'member') return role;\n // Backwards compatibility\n if (role === 'admin') return 'manager';\n return 'member';\n}\n\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport class TeamMemberViewModel extends ViewModel {\n driverId: string;\n driverName: string;\n role: TeamMemberRole;\n joinedAt: string;\n isActive: boolean;\n avatarUrl: string;\n\n private currentUserId: string;\n private teamOwnerId: string;\n\n constructor(dto: TeamMemberDTO, currentUserId: string, teamOwnerId: string) {\n this.driverId = dto.driverId;\n this.driverName = dto.driverName;\n this.role = normalizeTeamRole(dto.role);\n this.joinedAt = dto.joinedAt;\n this.isActive = dto.isActive;\n this.avatarUrl = dto.avatarUrl || '';\n this.currentUserId = currentUserId;\n this.teamOwnerId = teamOwnerId;\n }\n\n /** UI-specific: Role badge variant */\n get roleBadgeVariant(): string {\n switch (this.role) {\n case 'owner': return 'primary';\n case 'manager': return 'secondary';\n case 'member': return 'default';\n default: return 'default';\n }\n }\n\n /** UI-specific: Whether this member is the owner */\n get isOwner(): boolean {\n return this.driverId === this.teamOwnerId;\n }\n\n /** UI-specific: Whether current user can manage this member */\n get canManage(): boolean {\n return this.currentUserId === this.teamOwnerId && this.driverId !== this.currentUserId;\n }\n\n /** UI-specific: Whether this is the current user */\n get isCurrentUser(): boolean {\n return this.driverId === this.currentUserId;\n }\n\n /** UI-specific: Formatted joined date */\n get formattedJoinedAt(): string {\n return new Date(this.joinedAt).toLocaleDateString();\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/TeamSummaryViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/UpcomingRaceCardViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/UpdateAvatarViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":11,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Update Avatar View Model\n *\n * Represents the result of an avatar update operation\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface UpdateAvatarViewData {\n success: boolean;\n error?: string;\n}\n\nexport class UpdateAvatarViewModel extends ViewModel {\n success: boolean;\n error?: string;\n\n constructor(data: UpdateAvatarViewData) {\n super();\n this.success = data.success;\n this.error = data.error;\n }\n\n /** UI-specific: Whether update was successful */\n get isSuccessful(): boolean {\n return this.success;\n }\n\n /** UI-specific: Whether there was an error */\n get hasError(): boolean {\n return !!this.error;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/UpdateTeamViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":10,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * View Model for Update Team Result\n *\n * Represents the result of updating a team in a UI-ready format.\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface UpdateTeamViewData {\n success: boolean;\n}\n\nexport class UpdateTeamViewModel extends ViewModel {\n success: boolean;\n\n constructor(data: UpdateTeamViewData) {\n super();\n this.success = data.success;\n }\n\n /** UI-specific: Success message */\n get successMessage(): string {\n return this.success ? 'Team updated successfully!' : 'Failed to update team.';\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/UploadMediaViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.","line":8,"column":8,"nodeType":"TSInterfaceDeclaration","messageId":"noViewDataDefinition","endLine":13,"endColumn":2}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Upload Media View Model\n *\n * Represents the result of a media upload operation\n */\nimport { ViewModel } from \"../contracts/view-models/ViewModel\";\n\nexport interface UploadMediaViewData {\n success: boolean;\n mediaId?: string;\n url?: string;\n error?: string;\n}\n\nexport class UploadMediaViewModel extends ViewModel {\n success: boolean;\n mediaId?: string;\n url?: string;\n error?: string;\n\n constructor(data: UploadMediaViewData) {\n super();\n this.success = data.success;\n this.mediaId = data.mediaId;\n this.url = data.url;\n this.error = data.error;\n }\n\n /** UI-specific: Whether upload was successful */\n get isSuccessful(): boolean {\n return this.success;\n }\n\n /** UI-specific: Whether there was an error */\n get hasError(): boolean {\n return !!this.error;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/UserListViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/UserProfileViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/WalletTransactionViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/WalletViewModel.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/auth/ForgotPasswordInterfaces.ts","messages":[{"ruleId":"gridpilot-rules/view-model-implements","severity":2,"message":"ViewModel files must be classes named *ViewModel","line":1,"column":1,"nodeType":"Program","messageId":"notAClass","endLine":17,"endColumn":1}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"export interface ForgotPasswordFormField {\n value: string;\n error?: string;\n touched: boolean;\n validating: boolean;\n}\n\nexport interface ForgotPasswordFormState {\n fields: {\n email: ForgotPasswordFormField;\n };\n isValid: boolean;\n isSubmitting: boolean;\n submitError?: string;\n submitCount: number;\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/auth/ForgotPasswordViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: ./ForgotPasswordInterfaces","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":2,"endColumn":75}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../../contracts/view-models/ViewModel\";\nimport type { ForgotPasswordFormState } from \"./ForgotPasswordInterfaces\";\n\n/**\n * Forgot Password ViewModel\n * \n * Client-side state management for forgot password flow.\n * Immutable, class-based, contains only UI state.\n */\nexport class ForgotPasswordViewModel extends ViewModel {\n constructor(\n public readonly returnTo: string,\n public readonly formState: ForgotPasswordFormState,\n public readonly showSuccess: boolean = false,\n public readonly successMessage: string | null = null,\n public readonly magicLink: string | null = null,\n public readonly mutationPending: boolean = false,\n public readonly mutationError: string | null = null\n ) {\n super();\n }\n\n withFormState(formState: ForgotPasswordFormState): ForgotPasswordViewModel {\n return new ForgotPasswordViewModel(\n this.returnTo,\n formState,\n this.showSuccess,\n this.successMessage,\n this.magicLink,\n this.mutationPending,\n this.mutationError\n );\n }\n\n withSuccess(successMessage: string, magicLink: string | null = null): ForgotPasswordViewModel {\n return new ForgotPasswordViewModel(\n this.returnTo,\n this.formState,\n true,\n successMessage,\n magicLink,\n false,\n null\n );\n }\n\n withMutationState(pending: boolean, error: string | null): ForgotPasswordViewModel {\n return new ForgotPasswordViewModel(\n this.returnTo,\n this.formState,\n this.showSuccess,\n this.successMessage,\n this.magicLink,\n pending,\n error\n );\n }\n\n get isSubmitting(): boolean {\n return this.formState.isSubmitting || this.mutationPending;\n }\n\n get submitError(): string | undefined {\n return this.formState.submitError || this.mutationError || undefined;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/auth/LoginInterfaces.ts","messages":[{"ruleId":"gridpilot-rules/view-model-implements","severity":2,"message":"ViewModel files must be classes named *ViewModel","line":1,"column":1,"nodeType":"Program","messageId":"notAClass","endLine":24,"endColumn":1}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"export interface LoginFormField {\n value: string | boolean;\n error?: string;\n touched: boolean;\n validating: boolean;\n}\n\nexport interface LoginFormState {\n fields: {\n email: LoginFormField;\n password: LoginFormField;\n rememberMe: LoginFormField;\n };\n isValid: boolean;\n isSubmitting: boolean;\n submitError?: string;\n submitCount: number;\n}\n\nexport interface LoginUIState {\n showPassword: boolean;\n showErrorDetails: boolean;\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/auth/LoginViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: ./LoginInterfaces","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":2,"endColumn":71}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../../contracts/view-models/ViewModel\";\nimport type { LoginFormState, LoginUIState } from \"./LoginInterfaces\";\n\n/**\n * Login ViewModel\n * \n * Client-side state management for login flow.\n * Immutable, class-based, contains only UI state.\n */\nexport class LoginViewModel extends ViewModel {\n constructor(\n public readonly returnTo: string,\n public readonly hasInsufficientPermissions: boolean,\n public readonly formState: LoginFormState,\n public readonly uiState: LoginUIState,\n public readonly mutationPending: boolean = false,\n public readonly mutationError: string | null = null\n ) {\n super();\n }\n\n // Immutable updates\n withFormState(formState: LoginFormState): LoginViewModel {\n return new LoginViewModel(\n this.returnTo,\n this.hasInsufficientPermissions,\n formState,\n this.uiState,\n this.mutationPending,\n this.mutationError\n );\n }\n\n withUIState(uiState: LoginUIState): LoginViewModel {\n return new LoginViewModel(\n this.returnTo,\n this.hasInsufficientPermissions,\n this.formState,\n uiState,\n this.mutationPending,\n this.mutationError\n );\n }\n\n withMutationState(pending: boolean, error: string | null): LoginViewModel {\n return new LoginViewModel(\n this.returnTo,\n this.hasInsufficientPermissions,\n this.formState,\n this.uiState,\n pending,\n error\n );\n }\n\n // Getters for template consumption\n get showPassword(): boolean {\n return this.uiState.showPassword;\n }\n\n get showErrorDetails(): boolean {\n return this.uiState.showErrorDetails;\n }\n\n get isSubmitting(): boolean {\n return this.formState.isSubmitting || this.mutationPending;\n }\n\n get submitError(): string | undefined {\n return this.formState.submitError || this.mutationError || undefined;\n }\n\n get formFields() {\n return this.formState.fields;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/auth/ResetPasswordInterfaces.ts","messages":[{"ruleId":"gridpilot-rules/view-model-implements","severity":2,"message":"ViewModel files must be classes named *ViewModel","line":1,"column":1,"nodeType":"Program","messageId":"notAClass","endLine":23,"endColumn":1}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"export interface ResetPasswordFormField {\n value: string;\n error?: string;\n touched: boolean;\n validating: boolean;\n}\n\nexport interface ResetPasswordFormState {\n fields: {\n newPassword: ResetPasswordFormField;\n confirmPassword: ResetPasswordFormField;\n };\n isValid: boolean;\n isSubmitting: boolean;\n submitError?: string;\n submitCount: number;\n}\n\nexport interface ResetPasswordUIState {\n showPassword: boolean;\n showConfirmPassword: boolean;\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/auth/ResetPasswordViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: ./ResetPasswordInterfaces","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":2,"endColumn":95}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../../contracts/view-models/ViewModel\";\nimport type { ResetPasswordFormState, ResetPasswordUIState } from \"./ResetPasswordInterfaces\";\n\n/**\n * Reset Password ViewModel\n * \n * Client-side state management for reset password flow.\n * Immutable, class-based, contains only UI state.\n */\nexport class ResetPasswordViewModel extends ViewModel {\n constructor(\n public readonly token: string,\n public readonly returnTo: string,\n public readonly formState: ResetPasswordFormState,\n public readonly uiState: ResetPasswordUIState,\n public readonly showSuccess: boolean = false,\n public readonly successMessage: string | null = null,\n public readonly mutationPending: boolean = false,\n public readonly mutationError: string | null = null\n ) {\n super();\n }\n\n withFormState(formState: ResetPasswordFormState): ResetPasswordViewModel {\n return new ResetPasswordViewModel(\n this.token,\n this.returnTo,\n formState,\n this.uiState,\n this.showSuccess,\n this.successMessage,\n this.mutationPending,\n this.mutationError\n );\n }\n\n withUIState(uiState: ResetPasswordUIState): ResetPasswordViewModel {\n return new ResetPasswordViewModel(\n this.token,\n this.returnTo,\n this.formState,\n uiState,\n this.showSuccess,\n this.successMessage,\n this.mutationPending,\n this.mutationError\n );\n }\n\n withSuccess(successMessage: string): ResetPasswordViewModel {\n return new ResetPasswordViewModel(\n this.token,\n this.returnTo,\n this.formState,\n this.uiState,\n true,\n successMessage,\n false,\n null\n );\n }\n\n withMutationState(pending: boolean, error: string | null): ResetPasswordViewModel {\n return new ResetPasswordViewModel(\n this.token,\n this.returnTo,\n this.formState,\n this.uiState,\n this.showSuccess,\n this.successMessage,\n pending,\n error\n );\n }\n\n get isSubmitting(): boolean {\n return this.formState.isSubmitting || this.mutationPending;\n }\n\n get submitError(): string | undefined {\n return this.formState.submitError || this.mutationError || undefined;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/auth/SignupInterfaces.ts","messages":[{"ruleId":"gridpilot-rules/view-model-implements","severity":2,"message":"ViewModel files must be classes named *ViewModel","line":1,"column":1,"nodeType":"Program","messageId":"notAClass","endLine":26,"endColumn":1}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"export interface SignupFormField {\n value: string;\n error?: string;\n touched: boolean;\n validating: boolean;\n}\n\nexport interface SignupFormState {\n fields: {\n firstName: SignupFormField;\n lastName: SignupFormField;\n email: SignupFormField;\n password: SignupFormField;\n confirmPassword: SignupFormField;\n };\n isValid: boolean;\n isSubmitting: boolean;\n submitError?: string;\n submitCount: number;\n}\n\nexport interface SignupUIState {\n showPassword: boolean;\n showConfirmPassword: boolean;\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/auth/SignupViewModel.ts","messages":[{"ruleId":"gridpilot-rules/view-model-taxonomy","severity":2,"message":"ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: ./SignupInterfaces","line":2,"column":1,"nodeType":"ImportDeclaration","messageId":"strictImport","endLine":2,"endColumn":74}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { ViewModel } from \"../../contracts/view-models/ViewModel\";\nimport type { SignupFormState, SignupUIState } from \"./SignupInterfaces\";\n\n/**\n * Signup ViewModel\n * \n * Client-side state management for signup flow.\n * Immutable, class-based, contains only UI state.\n */\nexport class SignupViewModel extends ViewModel {\n constructor(\n public readonly returnTo: string,\n public readonly formState: SignupFormState,\n public readonly uiState: SignupUIState,\n public readonly mutationPending: boolean = false,\n public readonly mutationError: string | null = null\n ) {\n super();\n }\n\n withFormState(formState: SignupFormState): SignupViewModel {\n return new SignupViewModel(\n this.returnTo,\n formState,\n this.uiState,\n this.mutationPending,\n this.mutationError\n );\n }\n\n withUIState(uiState: SignupUIState): SignupViewModel {\n return new SignupViewModel(\n this.returnTo,\n this.formState,\n uiState,\n this.mutationPending,\n this.mutationError\n );\n }\n\n withMutationState(pending: boolean, error: string | null): SignupViewModel {\n return new SignupViewModel(\n this.returnTo,\n this.formState,\n this.uiState,\n pending,\n error\n );\n }\n\n get isSubmitting(): boolean {\n return this.formState.isSubmitting || this.mutationPending;\n }\n\n get submitError(): string | undefined {\n return this.formState.submitError || this.mutationError || undefined;\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/Users/marcmintel/Projects/gridpilot-3/apps/website/lib/view-models/index.ts","messages":[{"ruleId":"gridpilot-rules/view-model-implements","severity":2,"message":"ViewModel files must be classes named *ViewModel","line":1,"column":1,"nodeType":"Program","messageId":"notAClass","endLine":97,"endColumn":1}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"export * from \"./ActivityItemViewModel\";\nexport * from \"./AdminUserViewModel\";\nexport * from \"./AnalyticsDashboardViewModel\";\nexport * from \"./AnalyticsMetricsViewModel\";\nexport * from \"./AvailableLeaguesViewModel\";\nexport * from \"./AvatarGenerationViewModel\";\nexport * from \"./AvatarViewModel\";\nexport * from \"./BillingViewModel\";\nexport * from \"./CompleteOnboardingViewModel\";\nexport * from \"./CreateLeagueViewModel\";\nexport * from \"./CreateTeamViewModel\";\nexport * from \"./DeleteMediaViewModel\";\nexport * from \"./DriverLeaderboardItemViewModel\";\nexport * from \"./DriverLeaderboardViewModel\";\nexport * from \"./DriverProfileViewModel\";\nexport * from \"./DriverRegistrationStatusViewModel\";\nexport * from \"./DriverSummaryViewModel\";\nexport * from \"./DriverTeamViewModel\";\nexport * from \"./DriverViewModel\";\nexport * from \"./EmailSignupViewModel\";\nexport * from \"./HomeDiscoveryViewModel\";\nexport * from \"./ImportRaceResultsSummaryViewModel\";\nexport * from \"./LeagueAdminRosterJoinRequestViewModel\";\nexport * from \"./LeagueAdminRosterMemberViewModel\";\nexport * from \"./LeagueAdminScheduleViewModel\";\nexport * from \"./LeagueAdminViewModel\";\nexport * from \"./LeagueCardViewModel\";\nexport * from \"./LeagueDetailPageViewModel\";\nexport * from \"./LeagueDetailViewModel\";\nexport * from \"./LeagueJoinRequestViewModel\";\nexport * from \"./LeagueMembershipsViewModel\";\nexport * from \"./LeagueMemberViewModel\";\nexport * from \"./LeaguePageDetailViewModel\";\nexport * from \"./LeagueScheduleViewModel\";\nexport * from \"./LeagueScoringChampionshipViewModel\";\nexport * from \"./LeagueScoringConfigViewModel\";\nexport * from \"./LeagueScoringPresetsViewModel\";\nexport * from \"./LeagueScoringPresetViewModel\";\nexport * from \"./LeagueScoringSectionViewModel\";\nexport * from \"./LeagueSeasonSummaryViewModel\";\nexport * from \"./LeagueSettingsViewModel\";\nexport * from \"./LeagueStandingsViewModel\";\nexport * from \"./LeagueStatsViewModel\";\nexport * from \"./LeagueStewardingViewModel\";\nexport * from \"./LeagueSummaryViewModel\";\nexport * from \"./LeagueWalletViewModel\";\nexport * from \"./MediaViewModel\";\nexport * from \"./MembershipFeeViewModel\";\nexport * from \"./OnboardingViewModel\";\nexport * from \"./PaymentViewModel\";\nexport * from \"./PrizeViewModel\";\nexport * from \"./ProfileOverviewViewModel\";\nexport * from \"./ProtestDetailViewModel\";\nexport * from \"./ProtestDriverViewModel\";\nexport * from \"./ProtestViewModel\";\nexport * from \"./RaceDetailEntryViewModel\";\nexport * from \"./RaceDetailsViewModel\";\nexport * from \"./RaceDetailUserResultViewModel\";\nexport * from \"./RaceListItemViewModel\";\nexport * from \"./RaceResultsDetailViewModel\";\nexport * from \"./RaceResultViewModel\";\nexport * from \"./RacesPageViewModel\";\nexport * from \"./RaceStatsViewModel\";\nexport * from \"./RaceStewardingViewModel\";\nexport * from \"./RaceViewModel\";\nexport * from \"./RaceWithSOFViewModel\";\nexport * from \"./RecordEngagementInputViewModel\";\nexport * from \"./RecordEngagementOutputViewModel\";\nexport * from \"./RecordPageViewInputViewModel\";\nexport * from \"./RecordPageViewOutputViewModel\";\nexport * from \"./RemoveMemberViewModel\";\nexport * from \"./RenewalAlertViewModel\";\nexport * from \"./RequestAvatarGenerationViewModel\";\nexport * from \"./ScoringConfigurationViewModel\";\nexport * from \"./SessionViewModel\";\nexport * from \"./SponsorDashboardViewModel\";\nexport * from \"./SponsorSettingsViewModel\";\nexport * from \"./SponsorshipDetailViewModel\";\nexport * from \"./SponsorshipPricingViewModel\";\nexport * from \"./SponsorshipRequestViewModel\";\nexport * from \"./SponsorshipViewModel\";\nexport * from \"./SponsorSponsorshipsViewModel\";\nexport * from \"./SponsorViewModel\";\nexport * from \"./StandingEntryViewModel\";\nexport * from \"./TeamCardViewModel\";\nexport * from \"./TeamDetailsViewModel\";\nexport * from \"./TeamJoinRequestViewModel\";\nexport * from \"./TeamMemberViewModel\";\nexport * from \"./TeamSummaryViewModel\";\nexport * from \"./UpcomingRaceCardViewModel\";\nexport * from \"./UpdateAvatarViewModel\";\nexport * from \"./UpdateTeamViewModel\";\nexport * from \"./UploadMediaViewModel\";\nexport * from \"./UserProfileViewModel\";\nexport * from \"./WalletTransactionViewModel\";\nexport * from \"./WalletViewModel\";\n","usedDeprecatedRules":[]}] \ No newline at end of file