view data fixes
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
10
apps/website/lib/display-objects/MembershipFeeTypeDisplay.ts
Normal file
10
apps/website/lib/display-objects/MembershipFeeTypeDisplay.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export class MembershipFeeTypeDisplay {
|
||||
static format(type: string): string {
|
||||
switch (type) {
|
||||
case 'season': return 'Per Season';
|
||||
case 'monthly': return 'Monthly';
|
||||
case 'per_race': return 'Per Race';
|
||||
default: return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
5
apps/website/lib/display-objects/PayerTypeDisplay.ts
Normal file
5
apps/website/lib/display-objects/PayerTypeDisplay.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class PayerTypeDisplay {
|
||||
static format(type: string): string {
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
}
|
||||
5
apps/website/lib/display-objects/PaymentTypeDisplay.ts
Normal file
5
apps/website/lib/display-objects/PaymentTypeDisplay.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class PaymentTypeDisplay {
|
||||
static format(type: string): string {
|
||||
return type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
}
|
||||
}
|
||||
10
apps/website/lib/display-objects/PrizeTypeDisplay.ts
Normal file
10
apps/website/lib/display-objects/PrizeTypeDisplay.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export class PrizeTypeDisplay {
|
||||
static format(type: string): string {
|
||||
switch (type) {
|
||||
case 'cash': return 'Cash Prize';
|
||||
case 'merchandise': return 'Merchandise';
|
||||
case 'other': return 'Other';
|
||||
default: return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class TransactionTypeDisplay {
|
||||
static format(type: string): string {
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
5
apps/website/lib/view-data/CompleteOnboardingViewData.ts
Normal file
5
apps/website/lib/view-data/CompleteOnboardingViewData.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface CompleteOnboardingViewData {
|
||||
success: boolean;
|
||||
driverId?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
4
apps/website/lib/view-data/DeleteMediaViewData.ts
Normal file
4
apps/website/lib/view-data/DeleteMediaViewData.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface DeleteMediaViewData {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
10
apps/website/lib/view-data/DriverSummaryData.ts
Normal file
10
apps/website/lib/view-data/DriverSummaryData.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface DriverSummaryData {
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
avatarUrl: string | null;
|
||||
rating: number | null;
|
||||
rank: number | null;
|
||||
roleBadgeText: string;
|
||||
roleBadgeClasses: string;
|
||||
profileUrl: string;
|
||||
}
|
||||
14
apps/website/lib/view-data/DriverViewData.ts
Normal file
14
apps/website/lib/view-data/DriverViewData.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* ViewData for Driver
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface DriverViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
iracingId?: string;
|
||||
rating?: number;
|
||||
country?: string;
|
||||
bio?: string;
|
||||
joinedAt?: string;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
||||
|
||||
|
||||
export interface AdminScheduleRaceData extends ViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string; // ISO string
|
||||
}
|
||||
/**
|
||||
* ViewData for LeagueAdminSchedule
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueAdminScheduleViewData {
|
||||
seasonId: string;
|
||||
published: boolean;
|
||||
races: any[];
|
||||
}
|
||||
|
||||
11
apps/website/lib/view-data/LeagueAdminViewData.ts
Normal file
11
apps/website/lib/view-data/LeagueAdminViewData.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { LeagueMemberViewData } from './LeagueMemberViewData';
|
||||
|
||||
/**
|
||||
* ViewData for LeagueAdmin
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueAdminViewData {
|
||||
config: unknown;
|
||||
members: LeagueMemberViewData[];
|
||||
joinRequests: any[];
|
||||
}
|
||||
9
apps/website/lib/view-data/LeagueCardViewData.ts
Normal file
9
apps/website/lib/view-data/LeagueCardViewData.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* ViewData for LeagueCard
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueCardViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
41
apps/website/lib/view-data/LeagueDetailPageViewData.ts
Normal file
41
apps/website/lib/view-data/LeagueDetailPageViewData.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { DriverViewData } from './DriverViewData';
|
||||
|
||||
export interface SponsorInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
tier: 'main' | 'secondary';
|
||||
tagline?: string;
|
||||
}
|
||||
|
||||
export interface LeagueMembershipWithRole {
|
||||
driverId: string;
|
||||
role: 'owner' | 'admin' | 'steward' | 'member';
|
||||
status: 'active' | 'inactive';
|
||||
joinedAt: string;
|
||||
}
|
||||
|
||||
export interface LeagueDetailPageViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
ownerId: string;
|
||||
createdAt: string;
|
||||
settings: {
|
||||
maxDrivers?: number;
|
||||
};
|
||||
socialLinks?: {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
owner: DriverViewData | null;
|
||||
scoringConfig: any | null;
|
||||
drivers: DriverViewData[];
|
||||
memberships: LeagueMembershipWithRole[];
|
||||
allRaces: any[];
|
||||
averageSOF: number | null;
|
||||
completedRacesCount: number;
|
||||
sponsors: SponsorInfo[];
|
||||
}
|
||||
@@ -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 })[];
|
||||
}
|
||||
|
||||
11
apps/website/lib/view-data/LeagueJoinRequestViewData.ts
Normal file
11
apps/website/lib/view-data/LeagueJoinRequestViewData.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* ViewData for LeagueJoinRequest
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueJoinRequestViewData {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
requestedAt: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
11
apps/website/lib/view-data/LeagueMemberViewData.ts
Normal file
11
apps/website/lib/view-data/LeagueMemberViewData.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* ViewData for LeagueMember
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueMemberViewData {
|
||||
driverId: string;
|
||||
currentUserId: string;
|
||||
driver?: any;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
}
|
||||
9
apps/website/lib/view-data/LeagueMembershipsViewData.ts
Normal file
9
apps/website/lib/view-data/LeagueMembershipsViewData.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { LeagueMemberViewData } from './LeagueMemberViewData';
|
||||
|
||||
/**
|
||||
* ViewData for LeagueMemberships
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueMembershipsViewData {
|
||||
memberships: LeagueMemberViewData[];
|
||||
}
|
||||
9
apps/website/lib/view-data/LeaguePageDetailViewData.ts
Normal file
9
apps/website/lib/view-data/LeaguePageDetailViewData.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface LeaguePageDetailViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
ownerName: string;
|
||||
isAdmin: boolean;
|
||||
mainSponsor: { name: string; logoUrl: string; websiteUrl: string } | null;
|
||||
}
|
||||
@@ -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;
|
||||
}>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
10
apps/website/lib/view-data/LeagueScoringConfigViewData.ts
Normal file
10
apps/website/lib/view-data/LeagueScoringConfigViewData.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* ViewData for LeagueScoringConfig
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueScoringConfigViewData {
|
||||
gameName: string;
|
||||
scoringPresetName?: string;
|
||||
dropPolicySummary?: string;
|
||||
championships?: any[];
|
||||
}
|
||||
16
apps/website/lib/view-data/LeagueScoringPresetViewData.ts
Normal file
16
apps/website/lib/view-data/LeagueScoringPresetViewData.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* ViewData for LeagueScoringPreset
|
||||
*/
|
||||
export interface LeagueScoringPresetViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
sessionSummary: string;
|
||||
bonusSummary?: string;
|
||||
defaultTimings: {
|
||||
practiceMinutes: number;
|
||||
qualifyingMinutes: number;
|
||||
sprintRaceMinutes: number;
|
||||
mainRaceMinutes: number;
|
||||
sessionCount: number;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* ViewData for league scoring presets
|
||||
*/
|
||||
export interface LeagueScoringPresetsViewData {
|
||||
presets: any[];
|
||||
totalCount?: number;
|
||||
}
|
||||
15
apps/website/lib/view-data/LeagueScoringSectionViewData.ts
Normal file
15
apps/website/lib/view-data/LeagueScoringSectionViewData.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { LeagueConfigFormModel } from '../types/LeagueConfigFormModel';
|
||||
import type { LeagueScoringPresetViewData } from './LeagueScoringPresetViewData';
|
||||
|
||||
/**
|
||||
* ViewData for LeagueScoringSection
|
||||
*/
|
||||
export interface LeagueScoringSectionViewData {
|
||||
form: LeagueConfigFormModel;
|
||||
presets: LeagueScoringPresetViewData[];
|
||||
options?: {
|
||||
readOnly?: boolean;
|
||||
patternOnly?: boolean;
|
||||
championshipsOnly?: boolean;
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,18 @@
|
||||
import { ViewData } from "../contracts/view-data/ViewData";
|
||||
import type { LeagueConfigFormModel } from '../types/LeagueConfigFormModel';
|
||||
|
||||
|
||||
export interface LeagueSettingsViewData extends ViewData {
|
||||
leagueId: string;
|
||||
/**
|
||||
* ViewData for LeagueSettings
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueSettingsViewData {
|
||||
league: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
visibility: 'public' | 'private';
|
||||
ownerId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
config: {
|
||||
maxDrivers: number;
|
||||
scoringPresetId: string;
|
||||
allowLateJoin: boolean;
|
||||
requireApproval: boolean;
|
||||
};
|
||||
}
|
||||
config: LeagueConfigFormModel;
|
||||
presets: any[];
|
||||
owner: any | null;
|
||||
members: any[];
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
6
apps/website/lib/view-data/LeagueStatsViewData.ts
Normal file
6
apps/website/lib/view-data/LeagueStatsViewData.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* ViewData for LeagueStats
|
||||
*/
|
||||
export interface LeagueStatsViewData {
|
||||
totalLeagues: number;
|
||||
}
|
||||
31
apps/website/lib/view-data/LeagueSummaryViewData.ts
Normal file
31
apps/website/lib/view-data/LeagueSummaryViewData.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* ViewData for LeagueSummary
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueSummaryViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
logoUrl: string | null;
|
||||
ownerId: string;
|
||||
createdAt: string;
|
||||
maxDrivers: number;
|
||||
usedDriverSlots: number;
|
||||
activeDriversCount?: number;
|
||||
nextRaceAt?: string;
|
||||
maxTeams?: number;
|
||||
usedTeamSlots?: number;
|
||||
structureSummary: string;
|
||||
scoringPatternSummary?: string;
|
||||
timingSummary: string;
|
||||
category?: string | null;
|
||||
scoring?: {
|
||||
gameId: string;
|
||||
gameName: string;
|
||||
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
|
||||
scoringPresetId: string;
|
||||
scoringPresetName: string;
|
||||
dropPolicySummary: string;
|
||||
scoringPatternSummary: string;
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import { ViewData } from "../contracts/view-data/ViewData";
|
||||
import type { WalletTransactionViewData } from './WalletTransactionViewData';
|
||||
|
||||
|
||||
export interface LeagueWalletTransactionViewData extends ViewData {
|
||||
id: string;
|
||||
type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize';
|
||||
amount: number;
|
||||
formattedAmount: string;
|
||||
amountColor: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
formattedDate: string;
|
||||
status: 'completed' | 'pending' | 'failed';
|
||||
statusColor: string;
|
||||
typeColor: string;
|
||||
}
|
||||
/**
|
||||
* ViewData for LeagueWallet
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueWalletViewData {
|
||||
balance: number;
|
||||
currency: string;
|
||||
totalRevenue: number;
|
||||
totalFees: number;
|
||||
totalWithdrawals: number;
|
||||
pendingPayouts: number;
|
||||
transactions: WalletTransactionViewData[];
|
||||
canWithdraw: boolean;
|
||||
withdrawalBlockReason?: string;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
14
apps/website/lib/view-data/MembershipFeeViewData.ts
Normal file
14
apps/website/lib/view-data/MembershipFeeViewData.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* ViewData for MembershipFee
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface MembershipFeeViewData {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
type: string;
|
||||
amount: number;
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
11
apps/website/lib/view-data/NotificationSettingsViewData.ts
Normal file
11
apps/website/lib/view-data/NotificationSettingsViewData.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* ViewData for NotificationSettings
|
||||
*/
|
||||
export interface NotificationSettingsViewData {
|
||||
emailNewSponsorships: boolean;
|
||||
emailWeeklyReport: boolean;
|
||||
emailRaceAlerts: boolean;
|
||||
emailPaymentAlerts: boolean;
|
||||
emailNewOpportunities: boolean;
|
||||
emailContractExpiry: boolean;
|
||||
}
|
||||
18
apps/website/lib/view-data/PaymentViewData.ts
Normal file
18
apps/website/lib/view-data/PaymentViewData.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* ViewData for Payment
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface PaymentViewData {
|
||||
id: string;
|
||||
type: string;
|
||||
amount: number;
|
||||
platformFee: number;
|
||||
netAmount: number;
|
||||
payerId: string;
|
||||
payerType: string;
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
9
apps/website/lib/view-data/PrivacySettingsViewData.ts
Normal file
9
apps/website/lib/view-data/PrivacySettingsViewData.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* ViewData for PrivacySettings
|
||||
*/
|
||||
export interface PrivacySettingsViewData {
|
||||
publicProfile: boolean;
|
||||
showStats: boolean;
|
||||
showActiveSponsorships: boolean;
|
||||
allowDirectContact: boolean;
|
||||
}
|
||||
18
apps/website/lib/view-data/PrizeViewData.ts
Normal file
18
apps/website/lib/view-data/PrizeViewData.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* ViewData for Prize
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface PrizeViewData {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
position: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
type: string;
|
||||
description?: string;
|
||||
awarded: boolean;
|
||||
awardedTo?: string;
|
||||
awardedAt?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
12
apps/website/lib/view-data/ProfileOverviewViewData.ts
Normal file
12
apps/website/lib/view-data/ProfileOverviewViewData.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* ViewData for ProfileOverview
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface ProfileOverviewViewData {
|
||||
currentDriver: any | null;
|
||||
stats: any | null;
|
||||
finishDistribution: any | null;
|
||||
teamMemberships: any[];
|
||||
socialSummary: any;
|
||||
extendedProfile: any | null;
|
||||
}
|
||||
4
apps/website/lib/view-data/ProtestDriverViewData.ts
Normal file
4
apps/website/lib/view-data/ProtestDriverViewData.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface ProtestDriverViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
21
apps/website/lib/view-data/ProtestViewData.ts
Normal file
21
apps/website/lib/view-data/ProtestViewData.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
||||
|
||||
/**
|
||||
* ViewData for Protest
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface ProtestViewData extends ViewData {
|
||||
id: string;
|
||||
raceId: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
description: string;
|
||||
submittedAt: string;
|
||||
filedAt?: string;
|
||||
status: string;
|
||||
reviewedAt?: string;
|
||||
decisionNotes?: string;
|
||||
incident?: { lap?: number; description?: string } | null;
|
||||
proofVideoUrl?: string | null;
|
||||
comment?: string | null;
|
||||
}
|
||||
12
apps/website/lib/view-data/RaceDetailEntryViewData.ts
Normal file
12
apps/website/lib/view-data/RaceDetailEntryViewData.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* ViewData for RaceDetailEntry
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface RaceDetailEntryViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
isCurrentUser: boolean;
|
||||
rating: number | null;
|
||||
}
|
||||
14
apps/website/lib/view-data/RaceDetailUserResultViewData.ts
Normal file
14
apps/website/lib/view-data/RaceDetailUserResultViewData.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* ViewData for RaceDetailUserResult
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface RaceDetailUserResultViewData {
|
||||
position: number;
|
||||
startPosition: number;
|
||||
incidents: number;
|
||||
fastestLap: number;
|
||||
positionChange: number;
|
||||
isPodium: boolean;
|
||||
isClean: boolean;
|
||||
ratingChange: number;
|
||||
}
|
||||
37
apps/website/lib/view-data/RaceDetailsViewData.ts
Normal file
37
apps/website/lib/view-data/RaceDetailsViewData.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { RaceDetailEntryViewData } from './RaceDetailEntryViewData';
|
||||
import type { RaceDetailUserResultViewData } from './RaceDetailUserResultViewData';
|
||||
|
||||
export interface RaceDetailsRaceViewData {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
sessionType: string;
|
||||
}
|
||||
|
||||
export interface RaceDetailsLeagueViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
settings?: unknown;
|
||||
}
|
||||
|
||||
export interface RaceDetailsRegistrationViewData {
|
||||
canRegister: boolean;
|
||||
isUserRegistered: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewData for RaceDetails
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface RaceDetailsViewData {
|
||||
race: RaceDetailsRaceViewData | null;
|
||||
league: RaceDetailsLeagueViewData | null;
|
||||
entryList: RaceDetailEntryViewData[];
|
||||
registration: RaceDetailsRegistrationViewData;
|
||||
userResult: RaceDetailUserResultViewData | null;
|
||||
canReopenRace: boolean;
|
||||
error?: string;
|
||||
}
|
||||
17
apps/website/lib/view-data/RaceListItemViewData.ts
Normal file
17
apps/website/lib/view-data/RaceListItemViewData.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* ViewData for RaceListItem
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface RaceListItemViewData {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
strengthOfField: number | null;
|
||||
isUpcoming: boolean;
|
||||
isLive: boolean;
|
||||
isPast: boolean;
|
||||
}
|
||||
18
apps/website/lib/view-data/RaceResultViewData.ts
Normal file
18
apps/website/lib/view-data/RaceResultViewData.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* ViewData for RaceResult
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface RaceResultViewData {
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
avatarUrl: string;
|
||||
position: number;
|
||||
startPosition: number;
|
||||
incidents: number;
|
||||
fastestLap: number;
|
||||
positionChange: number;
|
||||
isPodium: boolean;
|
||||
isClean: boolean;
|
||||
id: string;
|
||||
raceId: string;
|
||||
}
|
||||
19
apps/website/lib/view-data/RaceResultsDetailViewData.ts
Normal file
19
apps/website/lib/view-data/RaceResultsDetailViewData.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { RaceResultViewData } from './RaceResultViewData';
|
||||
|
||||
/**
|
||||
* ViewData for RaceResultsDetail
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface RaceResultsDetailViewData {
|
||||
raceId: string;
|
||||
track: string;
|
||||
currentUserId: string;
|
||||
results: RaceResultViewData[];
|
||||
league?: { id: string; name: string };
|
||||
race?: { id: string; track: string; scheduledAt: string };
|
||||
drivers: { id: string; name: string }[];
|
||||
pointsSystem: Record<number, number>;
|
||||
fastestLapTime: number;
|
||||
penalties: { driverId: string; type: string; value?: number }[];
|
||||
currentDriverId: string;
|
||||
}
|
||||
7
apps/website/lib/view-data/RaceStatsViewData.ts
Normal file
7
apps/website/lib/view-data/RaceStatsViewData.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* ViewData for RaceStats
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface RaceStatsViewData {
|
||||
totalRaces: number;
|
||||
}
|
||||
@@ -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<string, Driver>;
|
||||
pendingCount: number;
|
||||
resolvedCount: number;
|
||||
penaltiesCount: number;
|
||||
}
|
||||
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<string, { id: string; name: string }>;
|
||||
}
|
||||
|
||||
19
apps/website/lib/view-data/RaceViewData.ts
Normal file
19
apps/website/lib/view-data/RaceViewData.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Race View Data
|
||||
*
|
||||
* ViewData for the race template.
|
||||
* JSON-serializable, template-ready data structure.
|
||||
*/
|
||||
|
||||
import { ViewData } from "../contracts/view-data/ViewData";
|
||||
|
||||
export interface RaceViewData extends ViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
date: string;
|
||||
track: string;
|
||||
car: string;
|
||||
status?: string;
|
||||
registeredCount?: number;
|
||||
strengthOfField?: number;
|
||||
}
|
||||
9
apps/website/lib/view-data/RaceWithSOFViewData.ts
Normal file
9
apps/website/lib/view-data/RaceWithSOFViewData.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* ViewData for RaceWithSOF
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface RaceWithSOFViewData {
|
||||
id: string;
|
||||
track: string;
|
||||
strengthOfField: number | null;
|
||||
}
|
||||
10
apps/website/lib/view-data/RacesPageViewData.ts
Normal file
10
apps/website/lib/view-data/RacesPageViewData.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ViewData } from '../contracts/view-data/ViewData';
|
||||
import { RaceListItemViewData } from './RaceListItemViewData';
|
||||
|
||||
/**
|
||||
* ViewData for RacesPage
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface RacesPageViewData extends ViewData {
|
||||
races: RaceListItemViewData[];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Record engagement input view data
|
||||
*/
|
||||
export interface RecordEngagementInputViewData {
|
||||
eventType: string;
|
||||
userId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Record page view input view data
|
||||
*/
|
||||
export interface RecordPageViewInputViewData {
|
||||
path: string;
|
||||
userId?: string;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Record page view output view data
|
||||
*/
|
||||
export interface RecordPageViewOutputViewData {
|
||||
pageViewId: string;
|
||||
}
|
||||
6
apps/website/lib/view-data/RemoveMemberViewData.ts
Normal file
6
apps/website/lib/view-data/RemoveMemberViewData.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* ViewData for RemoveMember
|
||||
*/
|
||||
export interface RemoveMemberViewData {
|
||||
success: boolean;
|
||||
}
|
||||
10
apps/website/lib/view-data/RenewalAlertViewData.ts
Normal file
10
apps/website/lib/view-data/RenewalAlertViewData.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* ViewData for RenewalAlert
|
||||
*/
|
||||
export interface RenewalAlertViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'league' | 'team' | 'driver' | 'race' | 'platform';
|
||||
renewDate: string;
|
||||
price: number;
|
||||
}
|
||||
18
apps/website/lib/view-data/ScoringConfigurationViewData.ts
Normal file
18
apps/website/lib/view-data/ScoringConfigurationViewData.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { LeagueConfigFormModel } from '../types/LeagueConfigFormModel';
|
||||
import type { LeagueScoringPresetViewData } from './LeagueScoringPresetViewData';
|
||||
|
||||
export interface CustomPointsConfig {
|
||||
racePoints: number[];
|
||||
poleBonusPoints: number;
|
||||
fastestLapPoints: number;
|
||||
leaderLapPoints: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewData for ScoringConfiguration
|
||||
*/
|
||||
export interface ScoringConfigurationViewData {
|
||||
config: LeagueConfigFormModel['scoring'];
|
||||
presets: LeagueScoringPresetViewData[];
|
||||
customPoints?: CustomPointsConfig;
|
||||
}
|
||||
@@ -1,38 +1,7 @@
|
||||
import { ViewData } from "../contracts/view-data/ViewData";
|
||||
|
||||
|
||||
export interface SponsorDashboardViewData extends ViewData {
|
||||
/**
|
||||
* ViewData for SponsorDashboard
|
||||
*/
|
||||
export interface SponsorDashboardViewData {
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
totalImpressions: string;
|
||||
totalInvestment: string;
|
||||
metrics: {
|
||||
impressionsChange: number;
|
||||
viewersChange: number;
|
||||
exposureChange: number;
|
||||
};
|
||||
categoryData: {
|
||||
leagues: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
|
||||
teams: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
|
||||
drivers: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
|
||||
races: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
|
||||
platform: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
|
||||
};
|
||||
sponsorships: Record<string, unknown>; // From DTO
|
||||
activeSponsorships: number;
|
||||
formattedTotalInvestment: string;
|
||||
costPerThousandViews: string;
|
||||
upcomingRenewals: Array<{
|
||||
id: string;
|
||||
type: 'league' | 'team' | 'driver' | 'race' | 'platform';
|
||||
name: string;
|
||||
formattedRenewDate: string;
|
||||
formattedPrice: string;
|
||||
}>;
|
||||
recentActivity: Array<{
|
||||
id: string;
|
||||
message: string;
|
||||
time: string;
|
||||
typeColor: string;
|
||||
formattedImpressions?: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
25
apps/website/lib/view-data/SponsorProfileViewData.ts
Normal file
25
apps/website/lib/view-data/SponsorProfileViewData.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* ViewData for SponsorProfile
|
||||
*/
|
||||
export interface SponsorProfileViewData {
|
||||
companyName: string;
|
||||
contactName: string;
|
||||
contactEmail: string;
|
||||
contactPhone: string;
|
||||
website: string;
|
||||
description: string;
|
||||
logoUrl: string | null;
|
||||
industry: string;
|
||||
address: {
|
||||
street: string;
|
||||
city: string;
|
||||
country: string;
|
||||
postalCode: string;
|
||||
};
|
||||
taxId: string;
|
||||
socialLinks: {
|
||||
twitter: string;
|
||||
linkedin: string;
|
||||
instagram: string;
|
||||
};
|
||||
}
|
||||
12
apps/website/lib/view-data/SponsorSettingsViewData.ts
Normal file
12
apps/website/lib/view-data/SponsorSettingsViewData.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NotificationSettingsViewData } from './NotificationSettingsViewData';
|
||||
import { PrivacySettingsViewData } from './PrivacySettingsViewData';
|
||||
import type { SponsorProfileViewData } from './SponsorProfileViewData';
|
||||
|
||||
/**
|
||||
* ViewData for SponsorSettings
|
||||
*/
|
||||
export interface SponsorSettingsViewData {
|
||||
profile: SponsorProfileViewData;
|
||||
notifications: NotificationSettingsViewData;
|
||||
privacy: PrivacySettingsViewData;
|
||||
}
|
||||
10
apps/website/lib/view-data/SponsorSponsorshipsViewData.ts
Normal file
10
apps/website/lib/view-data/SponsorSponsorshipsViewData.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { SponsorshipDetailViewData } from './SponsorshipDetailViewData';
|
||||
|
||||
/**
|
||||
* ViewData for SponsorSponsorships
|
||||
*/
|
||||
export interface SponsorSponsorshipsViewData {
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
sponsorships: SponsorshipDetailViewData[];
|
||||
}
|
||||
9
apps/website/lib/view-data/SponsorViewData.ts
Normal file
9
apps/website/lib/view-data/SponsorViewData.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* ViewData for Sponsor
|
||||
*/
|
||||
export interface SponsorViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
}
|
||||
18
apps/website/lib/view-data/SponsorshipDetailViewData.ts
Normal file
18
apps/website/lib/view-data/SponsorshipDetailViewData.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* ViewData for SponsorshipDetail
|
||||
*/
|
||||
export interface SponsorshipDetailViewData {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
seasonId: string;
|
||||
seasonName: string;
|
||||
tier: 'main' | 'secondary';
|
||||
status: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
type: string;
|
||||
entityName: string;
|
||||
price: number;
|
||||
impressions: number;
|
||||
}
|
||||
8
apps/website/lib/view-data/SponsorshipPricingViewData.ts
Normal file
8
apps/website/lib/view-data/SponsorshipPricingViewData.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* ViewData for SponsorshipPricing
|
||||
*/
|
||||
export interface SponsorshipPricingViewData {
|
||||
mainSlotPrice: number;
|
||||
secondarySlotPrice: number;
|
||||
currency: string;
|
||||
}
|
||||
17
apps/website/lib/view-data/SponsorshipRequestViewData.ts
Normal file
17
apps/website/lib/view-data/SponsorshipRequestViewData.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* ViewData for SponsorshipRequest
|
||||
*/
|
||||
export interface SponsorshipRequestViewData {
|
||||
id: string;
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
sponsorLogo?: string;
|
||||
tier: 'main' | 'secondary';
|
||||
offeredAmount: number;
|
||||
currency: string;
|
||||
formattedAmount: string;
|
||||
message?: string;
|
||||
createdAt: string;
|
||||
platformFee: number;
|
||||
netAmount: number;
|
||||
}
|
||||
23
apps/website/lib/view-data/SponsorshipViewData.ts
Normal file
23
apps/website/lib/view-data/SponsorshipViewData.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Interface for sponsorship data input
|
||||
*/
|
||||
export interface SponsorshipViewData {
|
||||
id: string;
|
||||
type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';
|
||||
entityId: string;
|
||||
entityName: string;
|
||||
tier?: 'main' | 'secondary';
|
||||
status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';
|
||||
applicationDate?: string | Date;
|
||||
approvalDate?: string | Date;
|
||||
rejectionReason?: string;
|
||||
startDate: string | Date;
|
||||
endDate: string | Date;
|
||||
price: number;
|
||||
impressions: number;
|
||||
impressionsChange?: number;
|
||||
engagement?: number;
|
||||
details?: string;
|
||||
entityOwner?: string;
|
||||
applicationMessage?: string;
|
||||
}
|
||||
17
apps/website/lib/view-data/StandingEntryViewData.ts
Normal file
17
apps/website/lib/view-data/StandingEntryViewData.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* ViewData for StandingEntry
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface StandingEntryViewData {
|
||||
driverId: string;
|
||||
position: number;
|
||||
points: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
races: number;
|
||||
leaderPoints: number;
|
||||
nextPoints: number;
|
||||
currentUserId: string;
|
||||
previousPosition?: number;
|
||||
driver?: any;
|
||||
}
|
||||
9
apps/website/lib/view-data/TeamCardViewData.ts
Normal file
9
apps/website/lib/view-data/TeamCardViewData.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
||||
|
||||
export interface TeamCardViewData extends ViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
21
apps/website/lib/view-data/TeamDetailsViewData.ts
Normal file
21
apps/website/lib/view-data/TeamDetailsViewData.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* ViewData for TeamDetails
|
||||
*/
|
||||
export interface TeamDetailsViewData {
|
||||
team: {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description?: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
createdAt?: string;
|
||||
specialization?: string;
|
||||
region?: string;
|
||||
languages?: string[];
|
||||
category?: string;
|
||||
};
|
||||
membership: { role: string; joinedAt: string; isActive: boolean } | null;
|
||||
canManage: boolean;
|
||||
currentUserId: string;
|
||||
}
|
||||
14
apps/website/lib/view-data/TeamJoinRequestViewData.ts
Normal file
14
apps/website/lib/view-data/TeamJoinRequestViewData.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* ViewData for TeamJoinRequest
|
||||
*/
|
||||
export interface TeamJoinRequestViewData {
|
||||
requestId: string;
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
teamId: string;
|
||||
status: string;
|
||||
requestedAt: string;
|
||||
avatarUrl?: string;
|
||||
currentUserId: string;
|
||||
isOwner: boolean;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
15
apps/website/lib/view-data/TeamMemberViewData.ts
Normal file
15
apps/website/lib/view-data/TeamMemberViewData.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type TeamMemberRole = 'owner' | 'manager' | 'member';
|
||||
|
||||
/**
|
||||
* ViewData for TeamMember
|
||||
*/
|
||||
export interface TeamMemberViewData {
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
isActive: boolean;
|
||||
avatarUrl?: string;
|
||||
currentUserId: string;
|
||||
teamOwnerId: string;
|
||||
}
|
||||
18
apps/website/lib/view-data/TeamSummaryViewData.ts
Normal file
18
apps/website/lib/view-data/TeamSummaryViewData.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface TeamSummaryViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
memberCount: number;
|
||||
description?: string;
|
||||
totalWins: number;
|
||||
totalRaces: number;
|
||||
performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
isRecruiting: boolean;
|
||||
specialization: 'endurance' | 'sprint' | 'mixed' | undefined;
|
||||
region: string | undefined;
|
||||
languages: string[];
|
||||
leagues: string[];
|
||||
logoUrl: string | undefined;
|
||||
rating: number | undefined;
|
||||
category: string | undefined;
|
||||
}
|
||||
6
apps/website/lib/view-data/UpcomingRaceCardViewData.ts
Normal file
6
apps/website/lib/view-data/UpcomingRaceCardViewData.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface UpcomingRaceCardViewData {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
}
|
||||
7
apps/website/lib/view-data/UpdateAvatarViewData.ts
Normal file
7
apps/website/lib/view-data/UpdateAvatarViewData.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* ViewData for UpdateAvatar
|
||||
*/
|
||||
export interface UpdateAvatarViewData {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
6
apps/website/lib/view-data/UpdateTeamViewData.ts
Normal file
6
apps/website/lib/view-data/UpdateTeamViewData.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* ViewData for UpdateTeam
|
||||
*/
|
||||
export interface UpdateTeamViewData {
|
||||
success: boolean;
|
||||
}
|
||||
9
apps/website/lib/view-data/UploadMediaViewData.ts
Normal file
9
apps/website/lib/view-data/UploadMediaViewData.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* ViewData for UploadMedia
|
||||
*/
|
||||
export interface UploadMediaViewData {
|
||||
success: boolean;
|
||||
mediaId?: string;
|
||||
url?: string;
|
||||
error?: string;
|
||||
}
|
||||
9
apps/website/lib/view-data/UserProfileViewData.ts
Normal file
9
apps/website/lib/view-data/UserProfileViewData.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
||||
|
||||
export interface UserProfileViewData extends ViewData {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
iracingId?: string;
|
||||
rating?: number;
|
||||
}
|
||||
17
apps/website/lib/view-data/WalletTransactionViewData.ts
Normal file
17
apps/website/lib/view-data/WalletTransactionViewData.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
||||
|
||||
/**
|
||||
* ViewData for WalletTransaction
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface WalletTransactionViewData extends ViewData {
|
||||
id: string;
|
||||
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize' | 'deposit';
|
||||
description: string;
|
||||
amount: number;
|
||||
fee: number;
|
||||
netAmount: number;
|
||||
date: string; // ISO string
|
||||
status: 'completed' | 'pending' | 'failed';
|
||||
reference?: string;
|
||||
}
|
||||
18
apps/website/lib/view-data/WalletViewData.ts
Normal file
18
apps/website/lib/view-data/WalletViewData.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
||||
import type { WalletTransactionViewData } from './WalletTransactionViewData';
|
||||
|
||||
/**
|
||||
* ViewData for Wallet
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface WalletViewData extends ViewData {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
balance: number;
|
||||
totalRevenue: number;
|
||||
totalPlatformFees: number;
|
||||
totalWithdrawn: number;
|
||||
createdAt: string;
|
||||
currency: string;
|
||||
transactions?: WalletTransactionViewData[];
|
||||
}
|
||||
@@ -9,21 +9,19 @@ import { ActivityItemViewData } from "../view-data/ActivityItemViewData";
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class ActivityItemViewModel extends ViewModel {
|
||||
readonly id: string;
|
||||
readonly type: string;
|
||||
readonly message: string;
|
||||
readonly time: string;
|
||||
readonly impressions?: number;
|
||||
private readonly data: ActivityItemViewData;
|
||||
|
||||
constructor(viewData: ActivityItemViewData) {
|
||||
constructor(data: ActivityItemViewData) {
|
||||
super();
|
||||
this.id = viewData.id;
|
||||
this.type = viewData.type;
|
||||
this.message = viewData.message;
|
||||
this.time = viewData.time;
|
||||
this.impressions = viewData.impressions;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get type(): string { return this.data.type; }
|
||||
get message(): string { return this.data.message; }
|
||||
get time(): string { return this.data.time; }
|
||||
get impressions(): number | undefined { return this.data.impressions; }
|
||||
|
||||
get typeColor(): string {
|
||||
const colors: Record<string, string> = {
|
||||
race: 'bg-warning-amber',
|
||||
@@ -36,6 +34,7 @@ export class ActivityItemViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
get formattedImpressions(): string | null {
|
||||
// Client-only formatting
|
||||
return this.impressions ? this.impressions.toLocaleString() : null;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
23
apps/website/lib/view-models/BillingStatsViewModel.ts
Normal file
23
apps/website/lib/view-models/BillingStatsViewModel.ts
Normal file
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
74
apps/website/lib/view-models/DashboardStatsViewModel.ts
Normal file
74
apps/website/lib/view-models/DashboardStatsViewModel.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user