view data fixes
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -1,41 +1,32 @@
|
||||
import type { LeaderboardDriverItem } from '@/lib/view-data/LeaderboardDriverItem';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeaderboardDriverItem } from '../view-data/LeaderboardDriverItem';
|
||||
import { SkillLevelDisplay } from "../display-objects/SkillLevelDisplay";
|
||||
import { SkillLevelIconDisplay } from "../display-objects/SkillLevelIconDisplay";
|
||||
import { WinRateDisplay } from "../display-objects/WinRateDisplay";
|
||||
import { RatingTrendDisplay } from "../display-objects/RatingTrendDisplay";
|
||||
|
||||
export class DriverLeaderboardItemViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
rating: number;
|
||||
skillLevel: string;
|
||||
nationality: string;
|
||||
racesCompleted: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
rank: number;
|
||||
avatarUrl: string;
|
||||
position: number;
|
||||
private previousRating: number | undefined;
|
||||
private readonly data: LeaderboardDriverItem;
|
||||
private readonly previousRating: number | undefined;
|
||||
|
||||
constructor(viewData: LeaderboardDriverItem, previousRating?: number) {
|
||||
constructor(data: LeaderboardDriverItem, previousRating?: number) {
|
||||
super();
|
||||
this.id = viewData.id;
|
||||
this.name = viewData.name;
|
||||
this.rating = viewData.rating;
|
||||
this.skillLevel = viewData.skillLevel;
|
||||
this.nationality = viewData.nationality;
|
||||
this.racesCompleted = viewData.racesCompleted;
|
||||
this.wins = viewData.wins;
|
||||
this.podiums = viewData.podiums;
|
||||
this.rank = viewData.rank;
|
||||
this.avatarUrl = viewData.avatarUrl;
|
||||
this.position = viewData.position;
|
||||
this.data = data;
|
||||
this.previousRating = previousRating;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get rating(): number { return this.data.rating; }
|
||||
get skillLevel(): string { return this.data.skillLevel; }
|
||||
get nationality(): string { return this.data.nationality; }
|
||||
get racesCompleted(): number { return this.data.racesCompleted; }
|
||||
get wins(): number { return this.data.wins; }
|
||||
get podiums(): number { return this.data.podiums; }
|
||||
get rank(): number { return this.data.rank; }
|
||||
get avatarUrl(): string { return this.data.avatarUrl; }
|
||||
get position(): number { return this.data.position; }
|
||||
|
||||
/** UI-specific: Skill level color */
|
||||
get skillLevelColor(): string {
|
||||
return SkillLevelDisplay.getColor(this.skillLevel);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DriverLeaderboardViewModel } from './DriverLeaderboardViewModel';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { LeaderboardDriverItem } from '../view-data/LeaderboardDriverItem';
|
||||
import { LeaderboardsViewData } from '../view-data/LeaderboardsViewData';
|
||||
import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel';
|
||||
import { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
|
||||
import { LeaderboardDriverItem } from '@/lib/view-data/LeaderboardDriverItem';
|
||||
import { DriverLeaderboardViewModel } from './DriverLeaderboardViewModel';
|
||||
|
||||
const createDriverViewData = (overrides: Partial<LeaderboardDriverItem> = {}): LeaderboardDriverItem => ({
|
||||
id: 'driver-1',
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
|
||||
import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel';
|
||||
import type { LeaderboardsViewData } from '../view-data/LeaderboardsViewData';
|
||||
|
||||
export class DriverLeaderboardViewModel extends ViewModel {
|
||||
drivers: DriverLeaderboardItemViewModel[];
|
||||
private readonly data: LeaderboardsViewData;
|
||||
readonly drivers: DriverLeaderboardItemViewModel[];
|
||||
|
||||
constructor(
|
||||
viewData: LeaderboardsViewData,
|
||||
data: LeaderboardsViewData,
|
||||
previousRatings?: Record<string, number>,
|
||||
) {
|
||||
super();
|
||||
this.drivers = viewData.drivers.map((driver) => {
|
||||
this.data = data;
|
||||
this.drivers = data.drivers.map((driver) => {
|
||||
const previousRating = previousRatings?.[driver.id];
|
||||
return new DriverLeaderboardItemViewModel(driver, previousRating);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { DriverSummaryData } from "../view-data/LeagueDetailViewData";
|
||||
import type { DriverSummaryData } from "../view-data/DriverSummaryData";
|
||||
import { NumberDisplay } from "../display-objects/NumberDisplay";
|
||||
import { RatingDisplay } from "../display-objects/RatingDisplay";
|
||||
|
||||
|
||||
@@ -5,35 +5,26 @@
|
||||
* Note: client-only ViewModel created from ViewData (never DTO).
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { DriverData } from "../view-data/LeagueStandingsViewData";
|
||||
|
||||
type DriverViewModelViewData = DriverData & {
|
||||
bio?: string;
|
||||
joinedAt?: string;
|
||||
};
|
||||
import { RatingDisplay } from "../display-objects/RatingDisplay";
|
||||
import type { DriverViewData } from "../view-data/DriverViewData";
|
||||
|
||||
export class DriverViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
iracingId?: string;
|
||||
rating?: number;
|
||||
country?: string;
|
||||
bio?: string;
|
||||
joinedAt?: string;
|
||||
private readonly data: DriverViewData;
|
||||
|
||||
constructor(viewData: DriverViewModelViewData) {
|
||||
constructor(data: DriverViewData) {
|
||||
super();
|
||||
this.id = viewData.id;
|
||||
this.name = viewData.name;
|
||||
this.avatarUrl = viewData.avatarUrl ?? null;
|
||||
if (viewData.iracingId !== undefined) this.iracingId = viewData.iracingId;
|
||||
if (viewData.rating !== undefined) this.rating = viewData.rating;
|
||||
if (viewData.country !== undefined) this.country = viewData.country;
|
||||
if (viewData.bio !== undefined) this.bio = viewData.bio;
|
||||
if (viewData.joinedAt !== undefined) this.joinedAt = viewData.joinedAt;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get avatarUrl(): string { return this.data.avatarUrl || ''; }
|
||||
get iracingId(): string | undefined { return this.data.iracingId; }
|
||||
get rating(): number | undefined { return this.data.rating; }
|
||||
get country(): string | undefined { return this.data.country; }
|
||||
get bio(): string | undefined { return this.data.bio; }
|
||||
get joinedAt(): string | undefined { return this.data.joinedAt; }
|
||||
|
||||
/** UI-specific: Whether driver has an iRacing ID */
|
||||
get hasIracingId(): boolean {
|
||||
return !!this.iracingId;
|
||||
@@ -41,6 +32,6 @@ export class DriverViewModel extends ViewModel {
|
||||
|
||||
/** UI-specific: Formatted rating */
|
||||
get formattedRating(): string {
|
||||
return this.rating ? this.rating.toFixed(0) : "Unrated";
|
||||
return this.rating !== undefined ? RatingDisplay.format(this.rating) : "Unrated";
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,23 @@ import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { EmailSignupViewData } from "../view-data/EmailSignupViewData";
|
||||
|
||||
export class EmailSignupViewModel extends ViewModel {
|
||||
constructor(private readonly viewData: EmailSignupViewData) {
|
||||
private readonly data: EmailSignupViewData;
|
||||
|
||||
constructor(data: EmailSignupViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get email(): string {
|
||||
return this.viewData.email;
|
||||
return this.data.email;
|
||||
}
|
||||
|
||||
get message(): string {
|
||||
return this.viewData.message;
|
||||
return this.data.message;
|
||||
}
|
||||
|
||||
get status(): EmailSignupViewData["status"] {
|
||||
return this.viewData.status;
|
||||
return this.data.status;
|
||||
}
|
||||
|
||||
get isSuccess(): boolean {
|
||||
|
||||
@@ -7,17 +7,17 @@ import type { HomeDiscoveryViewData } from '@/lib/view-data/HomeDiscoveryViewDat
|
||||
import { ViewModel } from '../contracts/view-models/ViewModel';
|
||||
|
||||
export class HomeDiscoveryViewModel extends ViewModel {
|
||||
readonly topLeagues: HomeDiscoveryViewData['topLeagues'];
|
||||
readonly teams: HomeDiscoveryViewData['teams'];
|
||||
readonly upcomingRaces: HomeDiscoveryViewData['upcomingRaces'];
|
||||
private readonly data: HomeDiscoveryViewData;
|
||||
|
||||
constructor(viewData: HomeDiscoveryViewData) {
|
||||
constructor(data: HomeDiscoveryViewData) {
|
||||
super();
|
||||
this.topLeagues = viewData.topLeagues;
|
||||
this.teams = viewData.teams;
|
||||
this.upcomingRaces = viewData.upcomingRaces;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get topLeagues() { return this.data.topLeagues; }
|
||||
get teams() { return this.data.teams; }
|
||||
get upcomingRaces() { return this.data.upcomingRaces; }
|
||||
|
||||
get hasTopLeagues(): boolean {
|
||||
return this.topLeagues.length > 0;
|
||||
}
|
||||
|
||||
@@ -2,28 +2,31 @@ import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { ImportRaceResultsSummaryViewData } from "../view-data/ImportRaceResultsSummaryViewData";
|
||||
|
||||
export class ImportRaceResultsSummaryViewModel extends ViewModel {
|
||||
constructor(private readonly viewData: ImportRaceResultsSummaryViewData) {
|
||||
private readonly data: ImportRaceResultsSummaryViewData;
|
||||
|
||||
constructor(data: ImportRaceResultsSummaryViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get success(): boolean {
|
||||
return this.viewData.success;
|
||||
return this.data.success;
|
||||
}
|
||||
|
||||
get raceId(): string {
|
||||
return this.viewData.raceId;
|
||||
return this.data.raceId;
|
||||
}
|
||||
|
||||
get driversProcessed(): number {
|
||||
return this.viewData.driversProcessed;
|
||||
return this.data.driversProcessed;
|
||||
}
|
||||
|
||||
get resultsRecorded(): number {
|
||||
return this.viewData.resultsRecorded;
|
||||
return this.data.resultsRecorded;
|
||||
}
|
||||
|
||||
get errors(): string[] {
|
||||
return this.viewData.errors;
|
||||
return this.data.errors;
|
||||
}
|
||||
|
||||
get hasErrors(): boolean {
|
||||
|
||||
27
apps/website/lib/view-models/InvoiceViewModel.ts
Normal file
27
apps/website/lib/view-models/InvoiceViewModel.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { InvoiceViewData } from "@/lib/view-data/BillingViewData";
|
||||
|
||||
export class InvoiceViewModel extends ViewModel {
|
||||
private readonly data: InvoiceViewData;
|
||||
|
||||
constructor(data: InvoiceViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get invoiceNumber(): string { return this.data.invoiceNumber; }
|
||||
get date(): string { return this.data.date; }
|
||||
get dueDate(): string { return this.data.dueDate; }
|
||||
get amount(): number { return this.data.amount; }
|
||||
get vatAmount(): number { return this.data.vatAmount; }
|
||||
get totalAmount(): number { return this.data.totalAmount; }
|
||||
get status(): string { return this.data.status; }
|
||||
get description(): string { return this.data.description; }
|
||||
get sponsorshipType(): string { return this.data.sponsorshipType; }
|
||||
get pdfUrl(): string { return this.data.pdfUrl; }
|
||||
get totalAmountFormatted(): string { return this.data.formattedTotalAmount; }
|
||||
get vatAmountFormatted(): string { return this.data.formattedVatAmount; }
|
||||
get dateFormatted(): string { return this.data.formattedDate; }
|
||||
get isOverdue(): boolean { return this.data.isOverdue; }
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueAdminRosterJoinRequestViewData } from "../view-data/LeagueAdminRosterJoinRequestViewData";
|
||||
|
||||
export interface LeagueAdminRosterJoinRequestViewModel extends ViewModel {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
requestedAtIso: string;
|
||||
message?: string;
|
||||
export class LeagueAdminRosterJoinRequestViewModel extends ViewModel {
|
||||
private readonly data: LeagueAdminRosterJoinRequestViewData;
|
||||
|
||||
constructor(data: LeagueAdminRosterJoinRequestViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get leagueId(): string { return this.data.leagueId; }
|
||||
get driverId(): string { return this.data.driverId; }
|
||||
get driverName(): string { return this.data.driverName; }
|
||||
get requestedAtIso(): string { return this.data.requestedAtIso; }
|
||||
get message(): string | undefined { return this.data.message; }
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
import type { MembershipRole } from '@/lib/types/MembershipRole';
|
||||
|
||||
import type { MembershipRole } from '../types/MembershipRole';
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueAdminRosterMemberViewData } from "../view-data/LeagueAdminRosterMemberViewData";
|
||||
|
||||
export interface LeagueAdminRosterMemberViewModel extends ViewModel {
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
role: MembershipRole;
|
||||
joinedAtIso: string;
|
||||
export class LeagueAdminRosterMemberViewModel extends ViewModel {
|
||||
private readonly data: LeagueAdminRosterMemberViewData;
|
||||
|
||||
constructor(data: LeagueAdminRosterMemberViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get driverId(): string { return this.data.driverId; }
|
||||
get driverName(): string { return this.data.driverName; }
|
||||
get role(): MembershipRole { return this.data.role; }
|
||||
get joinedAtIso(): string { return this.data.joinedAtIso; }
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { LeagueScheduleRaceViewModel } from './LeagueScheduleViewModel';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueAdminScheduleViewData } from "../view-data/LeagueAdminScheduleViewData";
|
||||
|
||||
export class LeagueAdminScheduleViewModel extends ViewModel {
|
||||
readonly seasonId: string;
|
||||
readonly published: boolean;
|
||||
readonly races: LeagueScheduleRaceViewModel[];
|
||||
private readonly data: LeagueAdminScheduleViewData;
|
||||
|
||||
constructor(input: { seasonId: string; published: boolean; races: LeagueScheduleRaceViewModel[] }) {
|
||||
this.seasonId = input.seasonId;
|
||||
this.published = input.published;
|
||||
this.races = input.races;
|
||||
constructor(data: LeagueAdminScheduleViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get seasonId(): string { return this.data.seasonId; }
|
||||
get published(): boolean { return this.data.published; }
|
||||
get races(): any[] { return this.data.races; }
|
||||
}
|
||||
@@ -1,27 +1,20 @@
|
||||
import type { LeagueMemberViewModel } from './LeagueMemberViewModel';
|
||||
import type { LeagueJoinRequestViewModel } from './LeagueJoinRequestViewModel';
|
||||
|
||||
/**
|
||||
* League admin view model
|
||||
* Transform from DTO to ViewModel with UI fields
|
||||
*/
|
||||
import { LeagueMemberViewModel } from './LeagueMemberViewModel';
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueAdminViewData } from "../view-data/LeagueAdminViewData";
|
||||
|
||||
export class LeagueAdminViewModel extends ViewModel {
|
||||
config: unknown;
|
||||
members: LeagueMemberViewModel[];
|
||||
joinRequests: LeagueJoinRequestViewModel[];
|
||||
private readonly data: LeagueAdminViewData;
|
||||
readonly members: LeagueMemberViewModel[];
|
||||
|
||||
constructor(dto: {
|
||||
config: unknown;
|
||||
members: LeagueMemberViewModel[];
|
||||
joinRequests: LeagueJoinRequestViewModel[];
|
||||
}) {
|
||||
this.config = dto.config;
|
||||
this.members = dto.members;
|
||||
this.joinRequests = dto.joinRequests;
|
||||
constructor(data: LeagueAdminViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.members = data.members.map(m => new LeagueMemberViewModel(m));
|
||||
}
|
||||
|
||||
get config(): unknown { return this.data.config; }
|
||||
get joinRequests(): any[] { return this.data.joinRequests; }
|
||||
|
||||
/** UI-specific: Total pending requests count */
|
||||
get pendingRequestsCount(): number {
|
||||
return this.joinRequests.length;
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import type { LeagueSummaryDTO } from '@/lib/types/generated/LeagueSummaryDTO';
|
||||
|
||||
interface LeagueCardDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* League card view model
|
||||
* UI representation of a league on the landing page.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueCardViewData } from "../view-data/LeagueCardViewData";
|
||||
|
||||
export class LeagueCardViewModel extends ViewModel {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
private readonly data: LeagueCardViewData;
|
||||
|
||||
constructor(dto: LeagueCardDTO | LeagueSummaryDTO & { description?: string }) {
|
||||
this.id = dto.id;
|
||||
this.name = dto.name;
|
||||
this.description = dto.description ?? 'Competitive iRacing league';
|
||||
constructor(data: LeagueCardViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get description(): string { return this.data.description ?? 'Competitive iRacing league'; }
|
||||
}
|
||||
|
||||
19
apps/website/lib/view-models/LeagueDetailDriverViewModel.ts
Normal file
19
apps/website/lib/view-models/LeagueDetailDriverViewModel.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { DriverViewModel } from "./DriverViewModel";
|
||||
import type { DriverViewData } from "../view-data/DriverViewData";
|
||||
|
||||
export class LeagueDetailDriverViewModel extends DriverViewModel {
|
||||
private readonly detailData: DriverViewData & { impressions: number };
|
||||
|
||||
constructor(data: DriverViewData & { impressions: number }) {
|
||||
super(data);
|
||||
this.detailData = data;
|
||||
}
|
||||
|
||||
get impressions(): number {
|
||||
return this.detailData.impressions;
|
||||
}
|
||||
|
||||
get formattedImpressions(): string {
|
||||
return this.impressions.toLocaleString();
|
||||
}
|
||||
}
|
||||
@@ -1,192 +1,79 @@
|
||||
import { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
|
||||
import { LeagueStatsDTO } from '@/lib/types/generated/LeagueStatsDTO';
|
||||
import { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
|
||||
import { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
|
||||
import { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { RaceViewModel } from './RaceViewModel';
|
||||
import { DriverViewModel } from './DriverViewModel';
|
||||
import type { LeagueDetailPageViewData, LeagueMembershipWithRole, SponsorInfo } from '../view-data/LeagueDetailPageViewData';
|
||||
|
||||
// Sponsor info type
|
||||
export interface SponsorInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
tier: 'main' | 'secondary';
|
||||
tagline?: string;
|
||||
}
|
||||
|
||||
// Driver summary for management section
|
||||
export interface DriverSummary {
|
||||
driver: DriverViewModel;
|
||||
rating: number | null;
|
||||
rank: number | null;
|
||||
}
|
||||
|
||||
// League membership with role
|
||||
export interface LeagueMembershipWithRole {
|
||||
driverId: string;
|
||||
role: 'owner' | 'admin' | 'steward' | 'member';
|
||||
status: 'active' | 'inactive';
|
||||
joinedAt: string;
|
||||
}
|
||||
|
||||
// Helper interfaces for type narrowing
|
||||
interface LeagueSettings {
|
||||
maxDrivers?: number;
|
||||
}
|
||||
|
||||
interface SocialLinks {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
}
|
||||
|
||||
interface LeagueStatsExtended {
|
||||
averageSOF?: number;
|
||||
averageRating?: number;
|
||||
completedRaces?: number;
|
||||
totalRaces?: number;
|
||||
}
|
||||
|
||||
interface MembershipsContainer {
|
||||
members?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>;
|
||||
memberships?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class LeagueDetailPageViewModel extends ViewModel {
|
||||
// League basic info
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
ownerId: string;
|
||||
createdAt: string;
|
||||
settings: {
|
||||
maxDrivers?: number;
|
||||
};
|
||||
socialLinks: {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
} | undefined;
|
||||
private readonly data: LeagueDetailPageViewData;
|
||||
readonly allRaces: RaceViewModel[];
|
||||
readonly runningRaces: RaceViewModel[];
|
||||
readonly ownerSummary: DriverSummary | null;
|
||||
readonly adminSummaries: DriverSummary[];
|
||||
readonly stewardSummaries: DriverSummary[];
|
||||
|
||||
// Owner info
|
||||
owner: GetDriverOutputDTO | null;
|
||||
|
||||
// Scoring configuration
|
||||
scoringConfig: LeagueScoringConfigDTO | null;
|
||||
|
||||
// Drivers and memberships
|
||||
drivers: GetDriverOutputDTO[];
|
||||
memberships: LeagueMembershipWithRole[];
|
||||
|
||||
// Races
|
||||
allRaces: RaceViewModel[];
|
||||
runningRaces: RaceViewModel[];
|
||||
|
||||
// Stats
|
||||
averageSOF: number | null;
|
||||
completedRacesCount: number;
|
||||
|
||||
// Sponsors
|
||||
sponsors: SponsorInfo[];
|
||||
|
||||
// Sponsor insights data
|
||||
sponsorInsights: {
|
||||
avgViewsPerRace: number;
|
||||
totalImpressions: number;
|
||||
engagementRate: string;
|
||||
estimatedReach: number;
|
||||
mainSponsorAvailable: boolean;
|
||||
secondarySlotsAvailable: number;
|
||||
mainSponsorPrice: number;
|
||||
secondaryPrice: number;
|
||||
tier: 'premium' | 'standard' | 'starter';
|
||||
trustScore: number;
|
||||
discordMembers: number;
|
||||
monthlyActivity: number;
|
||||
};
|
||||
|
||||
// Driver summaries for management
|
||||
ownerSummary: DriverSummary | null;
|
||||
adminSummaries: DriverSummary[];
|
||||
stewardSummaries: DriverSummary[];
|
||||
|
||||
constructor(
|
||||
league: LeagueWithCapacityAndScoringDTO,
|
||||
owner: GetDriverOutputDTO | null,
|
||||
scoringConfig: LeagueScoringConfigDTO | null,
|
||||
drivers: GetDriverOutputDTO[],
|
||||
memberships: LeagueMembershipsDTO,
|
||||
allRaces: RaceViewModel[],
|
||||
leagueStats: LeagueStatsDTO,
|
||||
sponsors: SponsorInfo[]
|
||||
) {
|
||||
this.id = league.id;
|
||||
this.name = league.name;
|
||||
this.description = league.description ?? '';
|
||||
this.ownerId = league.ownerId;
|
||||
this.createdAt = league.createdAt;
|
||||
constructor(data: LeagueDetailPageViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
|
||||
// Handle settings with proper type narrowing
|
||||
const settings = league.settings as LeagueSettings | undefined;
|
||||
const maxDrivers = settings?.maxDrivers;
|
||||
this.settings = {
|
||||
maxDrivers: maxDrivers,
|
||||
};
|
||||
|
||||
// Handle social links with proper type narrowing
|
||||
const socialLinks = league.socialLinks as SocialLinks | undefined;
|
||||
const discordUrl = socialLinks?.discordUrl;
|
||||
const youtubeUrl = socialLinks?.youtubeUrl;
|
||||
const websiteUrl = socialLinks?.websiteUrl;
|
||||
|
||||
this.socialLinks = {
|
||||
discordUrl,
|
||||
youtubeUrl,
|
||||
websiteUrl,
|
||||
this.allRaces = data.allRaces.map(r => r instanceof RaceViewModel ? r : new RaceViewModel(r));
|
||||
this.runningRaces = this.allRaces.filter(r => r.status === 'running');
|
||||
|
||||
// Build driver summaries
|
||||
this.ownerSummary = this.buildDriverSummary(data.ownerId);
|
||||
this.adminSummaries = data.memberships
|
||||
.filter(m => m.role === 'admin')
|
||||
.slice(0, 3)
|
||||
.map(m => this.buildDriverSummary(m.driverId))
|
||||
.filter((s): s is DriverSummary => s !== null);
|
||||
this.stewardSummaries = data.memberships
|
||||
.filter(m => m.role === 'steward')
|
||||
.slice(0, 3)
|
||||
.map(m => this.buildDriverSummary(m.driverId))
|
||||
.filter((s): s is DriverSummary => s !== null);
|
||||
}
|
||||
|
||||
private buildDriverSummary(driverId: string): DriverSummary | null {
|
||||
const driverData = this.data.drivers.find(d => d.id === driverId) ||
|
||||
(this.data.owner?.id === driverId ? this.data.owner : null);
|
||||
|
||||
if (!driverData) return null;
|
||||
|
||||
const driver = new DriverViewModel(driverData);
|
||||
|
||||
return {
|
||||
driver,
|
||||
rating: null,
|
||||
rank: null,
|
||||
};
|
||||
}
|
||||
|
||||
this.owner = owner;
|
||||
this.scoringConfig = scoringConfig;
|
||||
this.drivers = drivers;
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get description(): string { return this.data.description ?? ''; }
|
||||
get ownerId(): string { return this.data.ownerId; }
|
||||
get createdAt(): string { return this.data.createdAt; }
|
||||
get settings() { return this.data.settings; }
|
||||
get socialLinks() { return this.data.socialLinks; }
|
||||
get owner() { return this.data.owner; }
|
||||
get scoringConfig() { return this.data.scoringConfig; }
|
||||
get drivers() { return this.data.drivers; }
|
||||
get memberships() { return this.data.memberships; }
|
||||
get averageSOF(): number | null { return this.data.averageSOF; }
|
||||
get completedRacesCount(): number { return this.data.completedRacesCount; }
|
||||
get sponsors(): SponsorInfo[] { return this.data.sponsors; }
|
||||
|
||||
// Handle memberships with proper type narrowing
|
||||
const membershipsContainer = memberships as MembershipsContainer;
|
||||
const membershipDtos = membershipsContainer.members ??
|
||||
membershipsContainer.memberships ??
|
||||
[];
|
||||
|
||||
this.memberships = membershipDtos.map((m) => ({
|
||||
driverId: m.driverId,
|
||||
role: m.role as 'owner' | 'admin' | 'steward' | 'member',
|
||||
status: m.status ?? 'active',
|
||||
joinedAt: m.joinedAt,
|
||||
}));
|
||||
|
||||
this.allRaces = allRaces;
|
||||
this.runningRaces = allRaces.filter(r => r.status === 'running');
|
||||
|
||||
// Calculate SOF from available data with proper type narrowing
|
||||
const statsExtended = leagueStats as LeagueStatsExtended;
|
||||
const averageSOF = statsExtended.averageSOF ??
|
||||
statsExtended.averageRating ?? undefined;
|
||||
const completedRaces = statsExtended.completedRaces ??
|
||||
statsExtended.totalRaces ?? undefined;
|
||||
|
||||
this.averageSOF = typeof averageSOF === 'number' ? averageSOF : null;
|
||||
this.completedRacesCount = typeof completedRaces === 'number' ? completedRaces : 0;
|
||||
|
||||
this.sponsors = sponsors;
|
||||
|
||||
// Calculate sponsor insights
|
||||
get sponsorInsights() {
|
||||
const memberCount = this.memberships.length;
|
||||
const mainSponsorTaken = this.sponsors.some(s => s.tier === 'main');
|
||||
const secondaryTaken = this.sponsors.filter(s => s.tier === 'secondary').length;
|
||||
|
||||
this.sponsorInsights = {
|
||||
return {
|
||||
avgViewsPerRace: 5400 + memberCount * 50,
|
||||
totalImpressions: 45000 + memberCount * 500,
|
||||
engagementRate: (3.5 + (memberCount / 50)).toFixed(1),
|
||||
@@ -200,59 +87,17 @@ export class LeagueDetailPageViewModel extends ViewModel {
|
||||
discordMembers: memberCount * 3,
|
||||
monthlyActivity: Math.min(100, 40 + this.completedRacesCount * 2),
|
||||
};
|
||||
|
||||
// Build driver summaries
|
||||
this.ownerSummary = this.buildDriverSummary(this.ownerId);
|
||||
this.adminSummaries = this.memberships
|
||||
.filter(m => m.role === 'admin')
|
||||
.slice(0, 3)
|
||||
.map(m => this.buildDriverSummary(m.driverId))
|
||||
.filter((s): s is DriverSummary => s !== null);
|
||||
this.stewardSummaries = this.memberships
|
||||
.filter(m => m.role === 'steward')
|
||||
.slice(0, 3)
|
||||
.map(m => this.buildDriverSummary(m.driverId))
|
||||
.filter((s): s is DriverSummary => s !== null);
|
||||
}
|
||||
|
||||
private buildDriverSummary(driverId: string): DriverSummary | null {
|
||||
const driverDto = this.drivers.find(d => d.id === driverId);
|
||||
if (!driverDto) return null;
|
||||
|
||||
// Handle avatarUrl with proper type checking
|
||||
const driverAny = driverDto as { avatarUrl?: unknown };
|
||||
const avatarUrl = typeof driverAny.avatarUrl === 'string' ? driverAny.avatarUrl : null;
|
||||
|
||||
const driver = new DriverViewModel({
|
||||
id: driverDto.id,
|
||||
name: driverDto.name,
|
||||
avatarUrl: avatarUrl,
|
||||
iracingId: driverDto.iracingId,
|
||||
});
|
||||
|
||||
// Detailed rating and rank data are not wired from the analytics services yet;
|
||||
// expose the driver identity only so the UI can still render role assignments.
|
||||
return {
|
||||
driver,
|
||||
rating: null,
|
||||
rank: null,
|
||||
};
|
||||
}
|
||||
|
||||
// UI helper methods
|
||||
get isSponsorMode(): boolean {
|
||||
// League detail pages are rendered in organizer mode in this build; sponsor-specific
|
||||
// mode switches will be introduced once sponsor dashboards share this view model.
|
||||
return false;
|
||||
}
|
||||
|
||||
get currentUserMembership(): LeagueMembershipWithRole | null {
|
||||
// Current user identity is not available in this view model context yet; callers must
|
||||
// pass an explicit membership if they need per-user permissions.
|
||||
return null;
|
||||
}
|
||||
|
||||
get canEndRaces(): boolean {
|
||||
return this.currentUserMembership?.role === 'admin' || this.currentUserMembership?.role === 'owner';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
apps/website/lib/view-models/LeagueDetailRaceViewModel.ts
Normal file
19
apps/website/lib/view-models/LeagueDetailRaceViewModel.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { RaceViewModel } from "./RaceViewModel";
|
||||
import type { RaceViewData } from "../view-data/RaceViewData";
|
||||
|
||||
export class LeagueDetailRaceViewModel extends RaceViewModel {
|
||||
private readonly detailData: RaceViewData & { views: number };
|
||||
|
||||
constructor(data: RaceViewData & { views: number }) {
|
||||
super(data);
|
||||
this.detailData = data;
|
||||
}
|
||||
|
||||
get views(): number {
|
||||
return this.detailData.views;
|
||||
}
|
||||
|
||||
get formattedViews(): string {
|
||||
return this.views.toLocaleString();
|
||||
}
|
||||
}
|
||||
@@ -1,135 +1,20 @@
|
||||
import { DriverViewModel as SharedDriverViewModel } from "./DriverViewModel";
|
||||
import { RaceViewModel as SharedRaceViewModel } from "./RaceViewModel";
|
||||
|
||||
/**
|
||||
* League Detail View Model
|
||||
*
|
||||
* View model for detailed league information for sponsors.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { LeagueViewModel } from "./LeagueViewModel";
|
||||
import { LeagueDetailDriverViewModel } from "./LeagueDetailDriverViewModel";
|
||||
import { LeagueDetailRaceViewModel } from "./LeagueDetailRaceViewModel";
|
||||
import type { LeagueDetailViewData } from "../view-data/LeagueDetailViewData";
|
||||
|
||||
export class LeagueDetailViewModel extends ViewModel {
|
||||
league: LeagueViewModel;
|
||||
drivers: LeagueDetailDriverViewModel[];
|
||||
races: LeagueDetailRaceViewModel[];
|
||||
private readonly data: LeagueDetailViewData;
|
||||
readonly league: LeagueViewModel;
|
||||
readonly drivers: LeagueDetailDriverViewModel[];
|
||||
readonly races: LeagueDetailRaceViewModel[];
|
||||
|
||||
constructor(data: { league: unknown; drivers: unknown[]; races: unknown[] }) {
|
||||
constructor(data: LeagueDetailViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.league = new LeagueViewModel(data.league);
|
||||
this.drivers = data.drivers.map(driver => new LeagueDetailDriverViewModel(driver as any));
|
||||
this.races = data.races.map(race => new LeagueDetailRaceViewModel(race as any));
|
||||
}
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class LeagueDetailDriverViewModel extends SharedDriverViewModel extends ViewModel {
|
||||
impressions: number;
|
||||
|
||||
constructor(dto: any) {
|
||||
super(dto);
|
||||
this.impressions = dto.impressions || 0;
|
||||
}
|
||||
|
||||
get formattedImpressions(): string {
|
||||
return this.impressions.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class LeagueDetailRaceViewModel extends SharedRaceViewModel extends ViewModel {
|
||||
views: number;
|
||||
|
||||
constructor(dto: any) {
|
||||
super(dto);
|
||||
this.views = dto.views || 0;
|
||||
}
|
||||
|
||||
get formattedViews(): string {
|
||||
return this.views.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class LeagueViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
game: string;
|
||||
tier: 'premium' | 'standard' | 'starter';
|
||||
season: string;
|
||||
description: string;
|
||||
drivers: number;
|
||||
races: number;
|
||||
completedRaces: number;
|
||||
totalImpressions: number;
|
||||
avgViewsPerRace: number;
|
||||
engagement: number;
|
||||
rating: number;
|
||||
seasonStatus: 'active' | 'upcoming' | 'completed';
|
||||
seasonDates: { start: string; end: string };
|
||||
nextRace?: { name: string; date: string };
|
||||
sponsorSlots: {
|
||||
main: { available: boolean; price: number; benefits: string[] };
|
||||
secondary: { available: number; total: number; price: number; benefits: string[] };
|
||||
};
|
||||
|
||||
constructor(data: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const d = data as any;
|
||||
this.id = d.id;
|
||||
this.name = d.name;
|
||||
this.game = d.game;
|
||||
this.tier = d.tier;
|
||||
this.season = d.season;
|
||||
this.description = d.description;
|
||||
this.drivers = d.drivers;
|
||||
this.races = d.races;
|
||||
this.completedRaces = d.completedRaces;
|
||||
this.totalImpressions = d.totalImpressions;
|
||||
this.avgViewsPerRace = d.avgViewsPerRace;
|
||||
this.engagement = d.engagement;
|
||||
this.rating = d.rating;
|
||||
this.seasonStatus = d.seasonStatus;
|
||||
this.seasonDates = d.seasonDates;
|
||||
this.nextRace = d.nextRace;
|
||||
this.sponsorSlots = d.sponsorSlots;
|
||||
}
|
||||
|
||||
get formattedTotalImpressions(): string {
|
||||
return this.totalImpressions.toLocaleString();
|
||||
}
|
||||
|
||||
get formattedAvgViewsPerRace(): string {
|
||||
return this.avgViewsPerRace.toLocaleString();
|
||||
}
|
||||
|
||||
get projectedTotalViews(): number {
|
||||
return Math.round(this.avgViewsPerRace * this.races);
|
||||
}
|
||||
|
||||
get formattedProjectedTotal(): string {
|
||||
return this.projectedTotalViews.toLocaleString();
|
||||
}
|
||||
|
||||
get mainSponsorCpm(): number {
|
||||
return Math.round((this.sponsorSlots.main.price / this.projectedTotalViews) * 1000);
|
||||
}
|
||||
|
||||
get formattedMainSponsorCpm(): string {
|
||||
return `$${this.mainSponsorCpm.toFixed(2)}`;
|
||||
}
|
||||
|
||||
get racesLeft(): number {
|
||||
return this.races - this.completedRaces;
|
||||
}
|
||||
|
||||
get tierConfig() {
|
||||
const configs = {
|
||||
premium: { color: 'text-yellow-400', bgColor: 'bg-yellow-500/10', border: 'border-yellow-500/30' },
|
||||
standard: { color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', border: 'border-primary-blue/30' },
|
||||
starter: { color: 'text-gray-400', bgColor: 'bg-gray-500/10', border: 'border-gray-500/30' },
|
||||
};
|
||||
return configs[this.tier];
|
||||
this.drivers = data.drivers.map(driver => new LeagueDetailDriverViewModel(driver));
|
||||
this.races = data.races.map(race => new LeagueDetailRaceViewModel(race));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import type { LeagueJoinRequestDTO } from '@/lib/types/generated/LeagueJoinRequestDTO';
|
||||
|
||||
/**
|
||||
* League join request view model
|
||||
* Transform from DTO to ViewModel with UI fields
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueJoinRequestViewData } from "../view-data/LeagueJoinRequestViewData";
|
||||
|
||||
export class LeagueJoinRequestViewModel extends ViewModel {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
requestedAt: string;
|
||||
private readonly data: LeagueJoinRequestViewData;
|
||||
|
||||
private isAdmin: boolean;
|
||||
|
||||
constructor(dto: LeagueJoinRequestDTO, currentUserId: string, isAdmin: boolean) {
|
||||
this.id = dto.id;
|
||||
this.leagueId = dto.leagueId;
|
||||
this.driverId = dto.driverId;
|
||||
this.requestedAt = dto.requestedAt;
|
||||
this.isAdmin = isAdmin;
|
||||
constructor(data: LeagueJoinRequestViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get leagueId(): string { return this.data.leagueId; }
|
||||
get driverId(): string { return this.data.driverId; }
|
||||
get requestedAt(): string { return this.data.requestedAt; }
|
||||
|
||||
/** UI-specific: Formatted request date */
|
||||
get formattedRequestedAt(): string {
|
||||
return new Date(this.requestedAt).toLocaleString();
|
||||
@@ -29,11 +21,11 @@ export class LeagueJoinRequestViewModel extends ViewModel {
|
||||
|
||||
/** UI-specific: Whether the request can be approved by current user */
|
||||
get canApprove(): boolean {
|
||||
return this.isAdmin;
|
||||
return this.data.isAdmin;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether the request can be rejected by current user */
|
||||
get canReject(): boolean {
|
||||
return this.isAdmin;
|
||||
return this.data.isAdmin;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,29 @@
|
||||
import { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||
import { DriverViewModel } from './DriverViewModel';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { LeagueRoleDisplay, LeagueRole } from "../display-objects/LeagueRoleDisplay";
|
||||
import type { LeagueMemberViewData } from "../view-data/LeagueMemberViewData";
|
||||
|
||||
export class LeagueMemberViewModel extends ViewModel {
|
||||
driverId: string;
|
||||
private readonly data: LeagueMemberViewData;
|
||||
|
||||
currentUserId: string;
|
||||
|
||||
constructor(dto: LeagueMemberDTO, currentUserId: string) {
|
||||
this.driverId = dto.driverId;
|
||||
this.currentUserId = currentUserId;
|
||||
constructor(data: LeagueMemberViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
// Note: The generated DTO is incomplete
|
||||
// These fields will need to be added when the OpenAPI spec is updated
|
||||
driver?: DriverViewModel;
|
||||
role: string = 'member';
|
||||
joinedAt: string = new Date().toISOString();
|
||||
get driverId(): string { return this.data.driverId; }
|
||||
get driver(): any { return this.data.driver; }
|
||||
get role(): string { return this.data.role; }
|
||||
get joinedAt(): string { return this.data.joinedAt; }
|
||||
|
||||
/** UI-specific: Formatted join date */
|
||||
get formattedJoinedAt(): string {
|
||||
// Client-only formatting
|
||||
return new Date(this.joinedAt).toLocaleDateString();
|
||||
}
|
||||
|
||||
/** UI-specific: Badge variant for role */
|
||||
get roleBadgeVariant(): string {
|
||||
switch (this.role) {
|
||||
case 'owner': return 'primary';
|
||||
case 'admin': return 'secondary';
|
||||
default: return 'default';
|
||||
}
|
||||
/** UI-specific: Badge classes for role */
|
||||
get roleBadgeClasses(): string {
|
||||
return LeagueRoleDisplay.getLeagueRoleDisplay(this.role as LeagueRole)?.badgeClasses || '';
|
||||
}
|
||||
|
||||
/** UI-specific: Whether this member is the owner */
|
||||
@@ -40,6 +33,6 @@ export class LeagueMemberViewModel extends ViewModel {
|
||||
|
||||
/** UI-specific: Whether this is the current user */
|
||||
get isCurrentUser(): boolean {
|
||||
return this.driverId === this.currentUserId;
|
||||
return this.driverId === this.data.currentUserId;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
import { LeagueMemberViewModel } from './LeagueMemberViewModel';
|
||||
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||
|
||||
/**
|
||||
* View Model for League Memberships
|
||||
*
|
||||
* Represents the league's memberships in a UI-ready format.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueMembershipsViewData } from "../view-data/LeagueMembershipsViewData";
|
||||
|
||||
export class LeagueMembershipsViewModel extends ViewModel {
|
||||
memberships: LeagueMemberViewModel[];
|
||||
private readonly data: LeagueMembershipsViewData;
|
||||
readonly memberships: LeagueMemberViewModel[];
|
||||
|
||||
constructor(dto: { members?: LeagueMemberDTO[]; memberships?: LeagueMemberDTO[] }, currentUserId: string) {
|
||||
const memberships = dto.members ?? dto.memberships ?? [];
|
||||
this.memberships = memberships.map((membership) => new LeagueMemberViewModel(membership, currentUserId));
|
||||
constructor(data: LeagueMembershipsViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.memberships = data.memberships.map((m) => new LeagueMemberViewModel(m));
|
||||
}
|
||||
|
||||
/** UI-specific: Number of members */
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* View model for league page details.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeaguePageDetailViewData } from "../view-data/LeaguePageDetailViewData";
|
||||
|
||||
export class LeaguePageDetailViewModel extends ViewModel {
|
||||
id: string;
|
||||
@@ -14,7 +15,8 @@ export class LeaguePageDetailViewModel extends ViewModel {
|
||||
isAdmin: boolean;
|
||||
mainSponsor: { name: string; logoUrl: string; websiteUrl: string } | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: LeaguePageDetailViewData) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.description = data.description;
|
||||
@@ -23,4 +25,4 @@ export class LeaguePageDetailViewModel extends ViewModel {
|
||||
this.isAdmin = data.isAdmin;
|
||||
this.mainSponsor = data.mainSponsor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
apps/website/lib/view-models/LeagueScheduleRaceViewModel.ts
Normal file
16
apps/website/lib/view-models/LeagueScheduleRaceViewModel.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface LeagueScheduleRaceViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
scheduledAt: Date;
|
||||
formattedDate: string;
|
||||
formattedTime: string;
|
||||
isPast: boolean;
|
||||
isUpcoming: boolean;
|
||||
status: string;
|
||||
track?: string;
|
||||
car?: string;
|
||||
sessionType?: string;
|
||||
isRegistered?: boolean;
|
||||
}
|
||||
@@ -1,32 +1,15 @@
|
||||
/**
|
||||
* View Model for League Schedule
|
||||
*
|
||||
* Service layer maps DTOs into these shapes; UI consumes ViewModels only.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface LeagueScheduleRaceViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
scheduledAt: Date;
|
||||
formattedDate: string;
|
||||
formattedTime: string;
|
||||
isPast: boolean;
|
||||
isUpcoming: boolean;
|
||||
status: string;
|
||||
track?: string;
|
||||
car?: string;
|
||||
sessionType?: string;
|
||||
isRegistered?: boolean;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueScheduleRaceViewModel } from "./LeagueScheduleRaceViewModel";
|
||||
import type { LeagueScheduleViewData } from "../view-data/LeagueScheduleViewData";
|
||||
|
||||
export class LeagueScheduleViewModel extends ViewModel {
|
||||
private readonly data: LeagueScheduleViewData;
|
||||
readonly races: LeagueScheduleRaceViewModel[];
|
||||
|
||||
constructor(races: LeagueScheduleRaceViewModel[]) {
|
||||
this.races = races;
|
||||
constructor(data: LeagueScheduleViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.races = data.races;
|
||||
}
|
||||
|
||||
get raceCount(): number {
|
||||
@@ -36,4 +19,4 @@ export class LeagueScheduleViewModel extends ViewModel {
|
||||
get hasRaces(): boolean {
|
||||
return this.raceCount > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
export type LeagueScoringChampionshipViewModelInput = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
sessionTypes: string[];
|
||||
pointsPreview?: Array<{ sessionType: string; position: number; points: number }> | null;
|
||||
bonusSummary?: string[] | null;
|
||||
dropPolicyDescription?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* LeagueScoringChampionshipViewModel
|
||||
*
|
||||
* View model for league scoring championship
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueScoringChampionshipViewData } from "../view-data/LeagueScoringChampionshipViewData";
|
||||
|
||||
export class LeagueScoringChampionshipViewModel extends ViewModel {
|
||||
readonly id: string;
|
||||
@@ -24,13 +10,14 @@ export class LeagueScoringChampionshipViewModel extends ViewModel {
|
||||
readonly bonusSummary: string[];
|
||||
readonly dropPolicyDescription?: string;
|
||||
|
||||
constructor(input: LeagueScoringChampionshipViewModelInput) {
|
||||
this.id = input.id;
|
||||
this.name = input.name;
|
||||
this.type = input.type;
|
||||
this.sessionTypes = input.sessionTypes;
|
||||
this.pointsPreview = input.pointsPreview ?? [];
|
||||
this.bonusSummary = input.bonusSummary ?? [];
|
||||
this.dropPolicyDescription = input.dropPolicyDescription;
|
||||
constructor(data: LeagueScoringChampionshipViewData) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.type = data.type;
|
||||
this.sessionTypes = data.sessionTypes;
|
||||
this.pointsPreview = data.pointsPreview ?? [];
|
||||
this.bonusSummary = data.bonusSummary ?? [];
|
||||
this.dropPolicyDescription = data.dropPolicyDescription;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
|
||||
import type { LeagueScoringChampionshipDTO } from '@/lib/types/generated/LeagueScoringChampionshipDTO';
|
||||
|
||||
/**
|
||||
* LeagueScoringConfigViewModel
|
||||
*
|
||||
* View model for league scoring configuration
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueScoringConfigViewData } from "../view-data/LeagueScoringConfigViewData";
|
||||
|
||||
export class LeagueScoringConfigViewModel extends ViewModel {
|
||||
readonly gameName: string;
|
||||
readonly scoringPresetName?: string;
|
||||
readonly dropPolicySummary?: string;
|
||||
readonly championships?: LeagueScoringChampionshipDTO[];
|
||||
private readonly data: LeagueScoringConfigViewData;
|
||||
|
||||
constructor(dto: LeagueScoringConfigDTO) {
|
||||
this.gameName = dto.gameName;
|
||||
this.scoringPresetName = dto.scoringPresetName;
|
||||
this.dropPolicySummary = dto.dropPolicySummary;
|
||||
this.championships = dto.championships;
|
||||
constructor(data: LeagueScoringConfigViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get gameName(): string { return this.data.gameName; }
|
||||
get scoringPresetName(): string | undefined { return this.data.scoringPresetName; }
|
||||
get dropPolicySummary(): string | undefined { return this.data.dropPolicySummary; }
|
||||
get championships(): any[] | undefined { return this.data.championships; }
|
||||
}
|
||||
@@ -1,40 +1,19 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export type LeagueScoringPresetTimingDefaultsViewModel = ViewModel & {
|
||||
practiceMinutes: number;
|
||||
qualifyingMinutes: number;
|
||||
sprintRaceMinutes: number;
|
||||
mainRaceMinutes: number;
|
||||
sessionCount: number;
|
||||
};
|
||||
|
||||
export type LeagueScoringPresetViewModelInput = {
|
||||
id: string;
|
||||
name: string;
|
||||
sessionSummary: string;
|
||||
bonusSummary?: string;
|
||||
defaultTimings: LeagueScoringPresetTimingDefaultsViewModel;
|
||||
};
|
||||
|
||||
/**
|
||||
* LeagueScoringPresetViewModel
|
||||
*
|
||||
* View model for league scoring preset configuration
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueScoringPresetViewData } from "../view-data/LeagueScoringPresetViewData";
|
||||
|
||||
export class LeagueScoringPresetViewModel extends ViewModel {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly sessionSummary: string;
|
||||
readonly bonusSummary?: string;
|
||||
readonly defaultTimings: LeagueScoringPresetTimingDefaultsViewModel;
|
||||
readonly defaultTimings: LeagueScoringPresetViewData['defaultTimings'];
|
||||
|
||||
constructor(input: LeagueScoringPresetViewModelInput) {
|
||||
this.id = input.id;
|
||||
this.name = input.name;
|
||||
this.sessionSummary = input.sessionSummary;
|
||||
this.bonusSummary = input.bonusSummary;
|
||||
this.defaultTimings = input.defaultTimings;
|
||||
constructor(data: LeagueScoringPresetViewData) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.sessionSummary = data.sessionSummary;
|
||||
this.bonusSummary = data.bonusSummary;
|
||||
this.defaultTimings = data.defaultTimings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';
|
||||
|
||||
/**
|
||||
* View Model for league scoring presets
|
||||
* Transform from DTO to ViewModel with UI fields
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueScoringPresetsViewData } from "../view-data/LeagueScoringPresetsViewData";
|
||||
|
||||
export class LeagueScoringPresetsViewModel extends ViewModel {
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
presets: any[];
|
||||
totalCount: number;
|
||||
|
||||
constructor(dto: {
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
totalCount?: number;
|
||||
}) {
|
||||
this.presets = dto.presets;
|
||||
this.totalCount = dto.totalCount ?? dto.presets.length;
|
||||
constructor(data: LeagueScoringPresetsViewData) {
|
||||
super();
|
||||
this.presets = data.presets;
|
||||
this.totalCount = data.totalCount ?? data.presets.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
|
||||
import type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationViewModel';
|
||||
|
||||
/**
|
||||
* LeagueScoringSectionViewModel
|
||||
*
|
||||
* View model for the league scoring section UI state and operations
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { LeagueScoringPresetViewModel } from './LeagueScoringPresetViewModel';
|
||||
import type { CustomPointsConfig } from "../view-data/ScoringConfigurationViewData";
|
||||
import type { LeagueScoringSectionViewData } from "../view-data/LeagueScoringSectionViewData";
|
||||
|
||||
export class LeagueScoringSectionViewModel extends ViewModel {
|
||||
readonly form: LeagueConfigFormModel;
|
||||
readonly presets: LeagueScoringPresetViewModel[];
|
||||
readonly readOnly: boolean;
|
||||
readonly patternOnly: boolean;
|
||||
@@ -18,28 +11,24 @@ export class LeagueScoringSectionViewModel extends ViewModel {
|
||||
readonly disabled: boolean;
|
||||
readonly currentPreset: LeagueScoringPresetViewModel | null;
|
||||
readonly isCustom: boolean;
|
||||
private readonly data: LeagueScoringSectionViewData;
|
||||
|
||||
constructor(
|
||||
form: LeagueConfigFormModel,
|
||||
presets: LeagueScoringPresetViewModel[],
|
||||
options: {
|
||||
readOnly?: boolean;
|
||||
patternOnly?: boolean;
|
||||
championshipsOnly?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
this.form = form;
|
||||
this.presets = presets;
|
||||
this.readOnly = options.readOnly || false;
|
||||
this.patternOnly = options.patternOnly || false;
|
||||
this.championshipsOnly = options.championshipsOnly || false;
|
||||
constructor(data: LeagueScoringSectionViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.presets = data.presets.map(p => new LeagueScoringPresetViewModel(p));
|
||||
this.readOnly = data.options?.readOnly || false;
|
||||
this.patternOnly = data.options?.patternOnly || false;
|
||||
this.championshipsOnly = data.options?.championshipsOnly || false;
|
||||
this.disabled = this.readOnly;
|
||||
this.currentPreset = form.scoring.patternId
|
||||
? presets.find(p => p.id === form.scoring.patternId) || null
|
||||
this.currentPreset = data.form.scoring.patternId
|
||||
? this.presets.find(p => p.id === data.form.scoring.patternId) || null
|
||||
: null;
|
||||
this.isCustom = form.scoring.customScoringEnabled || false;
|
||||
this.isCustom = data.form.scoring.customScoringEnabled || false;
|
||||
}
|
||||
|
||||
get form() { return this.data.form; }
|
||||
|
||||
/**
|
||||
* Get default custom points configuration
|
||||
*/
|
||||
@@ -120,8 +109,6 @@ export class LeagueScoringSectionViewModel extends ViewModel {
|
||||
* Get the active custom points configuration
|
||||
*/
|
||||
getActiveCustomPoints(): CustomPointsConfig {
|
||||
// This would be stored separately in the form model
|
||||
// For now, return defaults
|
||||
return LeagueScoringSectionViewModel.getDefaultCustomPoints();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,16 @@ export type LeagueSeasonSummaryViewModelInput = {
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class LeagueSeasonSummaryViewModel extends ViewModel {
|
||||
readonly seasonId: string;
|
||||
readonly name: string;
|
||||
readonly status: string;
|
||||
readonly isPrimary: boolean;
|
||||
readonly isParallelActive: boolean;
|
||||
private readonly data: LeagueSeasonSummaryViewModelInput;
|
||||
|
||||
constructor(input: LeagueSeasonSummaryViewModelInput) {
|
||||
this.seasonId = input.seasonId;
|
||||
this.name = input.name;
|
||||
this.status = input.status;
|
||||
this.isPrimary = input.isPrimary;
|
||||
this.isParallelActive = input.isParallelActive;
|
||||
constructor(data: LeagueSeasonSummaryViewModelInput) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get seasonId(): string { return this.data.seasonId; }
|
||||
get name(): string { return this.data.name; }
|
||||
get status(): string { return this.data.status; }
|
||||
get isPrimary(): boolean { return this.data.isPrimary; }
|
||||
get isParallelActive(): boolean { return this.data.isParallelActive; }
|
||||
}
|
||||
@@ -1,40 +1,20 @@
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';
|
||||
import { DriverSummaryViewModel } from './DriverSummaryViewModel';
|
||||
|
||||
/**
|
||||
* View Model for league settings page
|
||||
* Combines league config, presets, owner, and members
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueSettingsViewData } from "../view-data/LeagueSettingsViewData";
|
||||
|
||||
export class LeagueSettingsViewModel extends ViewModel {
|
||||
league: {
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
};
|
||||
config: LeagueConfigFormModel;
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
owner: DriverSummaryViewModel | null;
|
||||
members: DriverSummaryViewModel[];
|
||||
private readonly data: LeagueSettingsViewData;
|
||||
readonly owner: DriverSummaryViewModel | null;
|
||||
readonly members: DriverSummaryViewModel[];
|
||||
|
||||
constructor(dto: {
|
||||
league: {
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
createdAt: string;
|
||||
};
|
||||
config: LeagueConfigFormModel;
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
owner: DriverSummaryViewModel | null;
|
||||
members: DriverSummaryViewModel[];
|
||||
}) {
|
||||
this.league = dto.league;
|
||||
this.config = dto.config;
|
||||
this.presets = dto.presets;
|
||||
this.owner = dto.owner;
|
||||
this.members = dto.members;
|
||||
constructor(data: LeagueSettingsViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.owner = data.owner ? new DriverSummaryViewModel(data.owner) : null;
|
||||
this.members = data.members.map(m => new DriverSummaryViewModel(m));
|
||||
}
|
||||
|
||||
get league() { return this.data.league; }
|
||||
get config() { return this.data.config; }
|
||||
get presets() { return this.data.presets; }
|
||||
}
|
||||
@@ -1,23 +1,17 @@
|
||||
import { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO';
|
||||
import { StandingEntryViewModel } from './StandingEntryViewModel';
|
||||
import { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
|
||||
import { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueStandingsViewData } from "../view-data/LeagueStandingsViewData";
|
||||
|
||||
export class LeagueStandingsViewModel extends ViewModel {
|
||||
standings: StandingEntryViewModel[];
|
||||
drivers: GetDriverOutputDTO[];
|
||||
memberships: LeagueMembership[];
|
||||
private readonly data: LeagueStandingsViewData;
|
||||
readonly standings: StandingEntryViewModel[];
|
||||
|
||||
constructor(dto: { standings: LeagueStandingDTO[]; drivers: GetDriverOutputDTO[]; memberships: LeagueMembership[] }, currentUserId: string, previousStandings?: LeagueStandingDTO[]) {
|
||||
const leaderPoints = dto.standings[0]?.points || 0;
|
||||
this.standings = dto.standings.map((entry, index) => {
|
||||
const nextPoints = dto.standings[index + 1]?.points || entry.points;
|
||||
const previousPosition = previousStandings?.find(p => p.driverId === entry.driverId)?.position;
|
||||
return new StandingEntryViewModel(entry, leaderPoints, nextPoints, currentUserId, previousPosition);
|
||||
});
|
||||
this.drivers = dto.drivers;
|
||||
this.memberships = dto.memberships;
|
||||
constructor(data: LeagueStandingsViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.standings = data.standings.map(s => new StandingEntryViewModel(s));
|
||||
}
|
||||
|
||||
get drivers(): any[] { return this.data.drivers; }
|
||||
get memberships(): any[] { return this.data.memberships; }
|
||||
}
|
||||
@@ -4,16 +4,21 @@
|
||||
* Represents the total number of leagues in a UI-ready format.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueStatsViewData } from "../view-data/LeagueStatsViewData";
|
||||
|
||||
export class LeagueStatsViewModel extends ViewModel {
|
||||
totalLeagues: number;
|
||||
private readonly data: LeagueStatsViewData;
|
||||
|
||||
constructor(dto: { totalLeagues: number }) {
|
||||
this.totalLeagues = dto.totalLeagues;
|
||||
constructor(data: LeagueStatsViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get totalLeagues(): number { return this.data.totalLeagues; }
|
||||
|
||||
/** UI-specific: Formatted total leagues display */
|
||||
get formattedTotalLeagues(): string {
|
||||
// Client-only formatting
|
||||
return this.totalLeagues.toLocaleString();
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,26 @@
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
/**
|
||||
* ViewData for LeagueStewarding
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface LeagueStewardingViewData {
|
||||
racesWithData: RaceWithProtests[];
|
||||
driverMap: Record<string, { id: string; name: string; avatarUrl?: string; iracingId?: string; rating?: number }>;
|
||||
}
|
||||
|
||||
export class LeagueStewardingViewModel extends ViewModel {
|
||||
constructor(
|
||||
public readonly racesWithData: RaceWithProtests[],
|
||||
public readonly driverMap: Record<string, { id: string; name: string; avatarUrl?: string; iracingId?: string; rating?: number }>
|
||||
) {
|
||||
private readonly data: LeagueStewardingViewData;
|
||||
|
||||
constructor(data: LeagueStewardingViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get racesWithData(): RaceWithProtests[] { return this.data.racesWithData; }
|
||||
get driverMap() { return this.data.driverMap; }
|
||||
|
||||
/** UI-specific: Total pending protests count */
|
||||
get totalPending(): number {
|
||||
return this.racesWithData.reduce((sum, r) => sum + r.pendingProtests.length, 0);
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { LeagueSummaryViewData } from "../view-data/LeagueSummaryViewData";
|
||||
|
||||
export interface LeagueSummaryViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
logoUrl: string | null;
|
||||
ownerId: string;
|
||||
createdAt: string;
|
||||
maxDrivers: number;
|
||||
usedDriverSlots: number;
|
||||
activeDriversCount?: number;
|
||||
nextRaceAt?: string;
|
||||
maxTeams?: number;
|
||||
usedTeamSlots?: number;
|
||||
structureSummary: string;
|
||||
scoringPatternSummary?: string;
|
||||
timingSummary: string;
|
||||
category?: string | null;
|
||||
scoring?: {
|
||||
gameId: string;
|
||||
gameName: string;
|
||||
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
|
||||
scoringPresetId: string;
|
||||
scoringPresetName: string;
|
||||
dropPolicySummary: string;
|
||||
scoringPatternSummary: string;
|
||||
};
|
||||
export class LeagueSummaryViewModel extends ViewModel {
|
||||
private readonly data: LeagueSummaryViewData;
|
||||
|
||||
constructor(data: LeagueSummaryViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get description(): string | null { return this.data.description; }
|
||||
get logoUrl(): string | null { return this.data.logoUrl; }
|
||||
get ownerId(): string { return this.data.ownerId; }
|
||||
get createdAt(): string { return this.data.createdAt; }
|
||||
get maxDrivers(): number { return this.data.maxDrivers; }
|
||||
get usedDriverSlots(): number { return this.data.usedDriverSlots; }
|
||||
get activeDriversCount(): number | undefined { return this.data.activeDriversCount; }
|
||||
get nextRaceAt(): string | undefined { return this.data.nextRaceAt; }
|
||||
get maxTeams(): number | undefined { return this.data.maxTeams; }
|
||||
get usedTeamSlots(): number | undefined { return this.data.usedTeamSlots; }
|
||||
get structureSummary(): string { return this.data.structureSummary; }
|
||||
get scoringPatternSummary(): string | undefined { return this.data.scoringPatternSummary; }
|
||||
get timingSummary(): string { return this.data.timingSummary; }
|
||||
get category(): string | null | undefined { return this.data.category; }
|
||||
get scoring() { return this.data.scoring; }
|
||||
}
|
||||
63
apps/website/lib/view-models/LeagueViewModel.ts
Normal file
63
apps/website/lib/view-models/LeagueViewModel.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { CurrencyDisplay } from "../display-objects/CurrencyDisplay";
|
||||
import { LeagueTierDisplay } from "../display-objects/LeagueTierDisplay";
|
||||
import type { LeagueViewData } from "../view-data/LeagueDetailViewData";
|
||||
|
||||
export class LeagueViewModel extends ViewModel {
|
||||
private readonly data: LeagueViewData;
|
||||
|
||||
constructor(data: LeagueViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get game(): string { return this.data.game; }
|
||||
get tier(): 'premium' | 'standard' | 'starter' { return this.data.tier; }
|
||||
get season(): string { return this.data.season; }
|
||||
get description(): string { return this.data.description; }
|
||||
get drivers(): number { return this.data.drivers; }
|
||||
get races(): number { return this.data.races; }
|
||||
get completedRaces(): number { return this.data.completedRaces; }
|
||||
get totalImpressions(): number { return this.data.totalImpressions; }
|
||||
get avgViewsPerRace(): number { return this.data.avgViewsPerRace; }
|
||||
get engagement(): number { return this.data.engagement; }
|
||||
get rating(): number { return this.data.rating; }
|
||||
get seasonStatus(): 'active' | 'upcoming' | 'completed' { return this.data.seasonStatus; }
|
||||
get seasonDates() { return this.data.seasonDates; }
|
||||
get nextRace() { return this.data.nextRace; }
|
||||
get sponsorSlots() { return this.data.sponsorSlots; }
|
||||
|
||||
get formattedTotalImpressions(): string {
|
||||
return this.totalImpressions.toLocaleString();
|
||||
}
|
||||
|
||||
get formattedAvgViewsPerRace(): string {
|
||||
return this.avgViewsPerRace.toLocaleString();
|
||||
}
|
||||
|
||||
get projectedTotalViews(): number {
|
||||
return Math.round(this.avgViewsPerRace * this.races);
|
||||
}
|
||||
|
||||
get formattedProjectedTotal(): string {
|
||||
return this.projectedTotalViews.toLocaleString();
|
||||
}
|
||||
|
||||
get mainSponsorCpm(): number {
|
||||
return Math.round((this.sponsorSlots.main.price / this.projectedTotalViews) * 1000);
|
||||
}
|
||||
|
||||
get formattedMainSponsorCpm(): string {
|
||||
return CurrencyDisplay.format(this.mainSponsorCpm);
|
||||
}
|
||||
|
||||
get racesLeft(): number {
|
||||
return this.races - this.completedRaces;
|
||||
}
|
||||
|
||||
get tierConfig() {
|
||||
return LeagueTierDisplay.getDisplay(this.tier);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,49 @@
|
||||
import { WalletTransactionViewModel } from './WalletTransactionViewModel';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { CurrencyDisplay } from "../display-objects/CurrencyDisplay";
|
||||
import type { LeagueWalletViewData } from "../view-data/LeagueWalletViewData";
|
||||
|
||||
export class LeagueWalletViewModel extends ViewModel {
|
||||
balance: number;
|
||||
currency: string;
|
||||
totalRevenue: number;
|
||||
totalFees: number;
|
||||
totalWithdrawals: number;
|
||||
pendingPayouts: number;
|
||||
transactions: WalletTransactionViewModel[];
|
||||
canWithdraw: boolean;
|
||||
withdrawalBlockReason?: string;
|
||||
private readonly data: LeagueWalletViewData;
|
||||
readonly transactions: WalletTransactionViewModel[];
|
||||
|
||||
constructor(dto: {
|
||||
balance: number;
|
||||
currency: string;
|
||||
totalRevenue: number;
|
||||
totalFees: number;
|
||||
totalWithdrawals: number;
|
||||
pendingPayouts: number;
|
||||
transactions: WalletTransactionViewModel[];
|
||||
canWithdraw: boolean;
|
||||
withdrawalBlockReason?: string;
|
||||
}) {
|
||||
constructor(data: LeagueWalletViewData) {
|
||||
super();
|
||||
this.balance = dto.balance;
|
||||
this.currency = dto.currency;
|
||||
this.totalRevenue = dto.totalRevenue;
|
||||
this.totalFees = dto.totalFees;
|
||||
this.totalWithdrawals = dto.totalWithdrawals;
|
||||
this.pendingPayouts = dto.pendingPayouts;
|
||||
this.transactions = dto.transactions;
|
||||
this.canWithdraw = dto.canWithdraw;
|
||||
this.withdrawalBlockReason = dto.withdrawalBlockReason;
|
||||
this.data = data;
|
||||
this.transactions = data.transactions.map(t => new WalletTransactionViewModel(t));
|
||||
}
|
||||
|
||||
get balance(): number { return this.data.balance; }
|
||||
get currency(): string { return this.data.currency; }
|
||||
get totalRevenue(): number { return this.data.totalRevenue; }
|
||||
get totalFees(): number { return this.data.totalFees; }
|
||||
get totalWithdrawals(): number { return this.data.totalWithdrawals; }
|
||||
get pendingPayouts(): number { return this.data.pendingPayouts; }
|
||||
get canWithdraw(): boolean { return this.data.canWithdraw; }
|
||||
get withdrawalBlockReason(): string | undefined { return this.data.withdrawalBlockReason; }
|
||||
|
||||
/** UI-specific: Formatted balance */
|
||||
get formattedBalance(): string {
|
||||
return `$${this.balance.toFixed(2)}`;
|
||||
return CurrencyDisplay.format(this.balance, this.currency);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted total revenue */
|
||||
get formattedTotalRevenue(): string {
|
||||
return `$${this.totalRevenue.toFixed(2)}`;
|
||||
return CurrencyDisplay.format(this.totalRevenue, this.currency);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted total fees */
|
||||
get formattedTotalFees(): string {
|
||||
return `$${this.totalFees.toFixed(2)}`;
|
||||
return CurrencyDisplay.format(this.totalFees, this.currency);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted pending payouts */
|
||||
get formattedPendingPayouts(): string {
|
||||
return `$${this.pendingPayouts.toFixed(2)}`;
|
||||
return CurrencyDisplay.format(this.pendingPayouts, this.currency);
|
||||
}
|
||||
|
||||
/** UI-specific: Filtered transactions by type */
|
||||
getFilteredTransactions(type: 'all' | 'sponsorship' | 'membership' | 'withdrawal' | 'prize'): WalletTransactionViewModel[] {
|
||||
return type === 'all' ? this.transactions : this.transactions.filter(t => t.type === type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { MediaViewData } from '@/lib/view-data/MediaViewData';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
type MediaAssetViewData = MediaViewData['assets'][number];
|
||||
import type { MediaAssetViewData } from "../view-data/MediaViewData";
|
||||
|
||||
/**
|
||||
* Media View Model
|
||||
@@ -11,23 +8,20 @@ type MediaAssetViewData = MediaViewData['assets'][number];
|
||||
* Represents a single media asset card in the UI.
|
||||
*/
|
||||
export class MediaViewModel extends ViewModel {
|
||||
id: string;
|
||||
src: string;
|
||||
title: string;
|
||||
category: string;
|
||||
date?: string;
|
||||
dimensions?: string;
|
||||
private readonly data: MediaAssetViewData;
|
||||
|
||||
constructor(viewData: MediaAssetViewData) {
|
||||
constructor(data: MediaAssetViewData) {
|
||||
super();
|
||||
this.id = viewData.id;
|
||||
this.src = viewData.src;
|
||||
this.title = viewData.title;
|
||||
this.category = viewData.category;
|
||||
if (viewData.date !== undefined) this.date = viewData.date;
|
||||
if (viewData.dimensions !== undefined) this.dimensions = viewData.dimensions;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get src(): string { return this.data.src; }
|
||||
get title(): string { return this.data.title; }
|
||||
get category(): string { return this.data.category; }
|
||||
get date(): string | undefined { return this.data.date; }
|
||||
get dimensions(): string | undefined { return this.data.dimensions; }
|
||||
|
||||
/** UI-specific: Combined subtitle used by MediaCard */
|
||||
get subtitle(): string {
|
||||
return `${this.category}${this.dimensions ? ` • ${this.dimensions}` : ''}`;
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import type { MembershipFeeDTO } from '@/lib/types/generated';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { CurrencyDisplay } from "../display-objects/CurrencyDisplay";
|
||||
import { DateDisplay } from "../display-objects/DateDisplay";
|
||||
import { MembershipFeeTypeDisplay } from "../display-objects/MembershipFeeTypeDisplay";
|
||||
import type { MembershipFeeViewData } from "../view-data/MembershipFeeViewData";
|
||||
|
||||
export class MembershipFeeViewModel extends ViewModel {
|
||||
id!: string;
|
||||
leagueId!: string;
|
||||
seasonId?: string;
|
||||
type!: string;
|
||||
amount!: number;
|
||||
enabled!: boolean;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
private readonly data: MembershipFeeViewData;
|
||||
|
||||
constructor(dto: MembershipFeeDTO) {
|
||||
Object.assign(this, dto);
|
||||
constructor(data: MembershipFeeViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get leagueId(): string { return this.data.leagueId; }
|
||||
get seasonId(): string | undefined { return this.data.seasonId; }
|
||||
get type(): string { return this.data.type; }
|
||||
get amount(): number { return this.data.amount; }
|
||||
get enabled(): boolean { return this.data.enabled; }
|
||||
get createdAt(): string { return this.data.createdAt; }
|
||||
get updatedAt(): string { return this.data.updatedAt; }
|
||||
|
||||
/** UI-specific: Formatted amount */
|
||||
get formattedAmount(): string {
|
||||
return `€${this.amount.toFixed(2)}`; // Assuming EUR
|
||||
return CurrencyDisplay.format(this.amount, 'EUR');
|
||||
}
|
||||
|
||||
/** UI-specific: Type display */
|
||||
get typeDisplay(): string {
|
||||
switch (this.type) {
|
||||
case 'season': return 'Per Season';
|
||||
case 'monthly': return 'Monthly';
|
||||
case 'per_race': return 'Per Race';
|
||||
default: return this.type;
|
||||
}
|
||||
return MembershipFeeTypeDisplay.format(this.type);
|
||||
}
|
||||
|
||||
/** UI-specific: Status display */
|
||||
@@ -43,11 +43,11 @@ export class MembershipFeeViewModel extends ViewModel {
|
||||
|
||||
/** UI-specific: Formatted created date */
|
||||
get formattedCreatedAt(): string {
|
||||
return this.createdAt.toLocaleString();
|
||||
return DateDisplay.formatShort(this.createdAt);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted updated date */
|
||||
get formattedUpdatedAt(): string {
|
||||
return this.updatedAt.toLocaleString();
|
||||
return DateDisplay.formatShort(this.updatedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { NotificationSettingsViewData } from "../view-data/NotificationSettingsViewData";
|
||||
|
||||
export class NotificationSettingsViewModel extends ViewModel {
|
||||
emailNewSponsorships: boolean;
|
||||
emailWeeklyReport: boolean;
|
||||
emailRaceAlerts: boolean;
|
||||
emailPaymentAlerts: boolean;
|
||||
emailNewOpportunities: boolean;
|
||||
emailContractExpiry: boolean;
|
||||
|
||||
constructor(data: NotificationSettingsViewData) {
|
||||
super();
|
||||
this.emailNewSponsorships = data.emailNewSponsorships;
|
||||
this.emailWeeklyReport = data.emailWeeklyReport;
|
||||
this.emailRaceAlerts = data.emailRaceAlerts;
|
||||
this.emailPaymentAlerts = data.emailPaymentAlerts;
|
||||
this.emailNewOpportunities = data.emailNewOpportunities;
|
||||
this.emailContractExpiry = data.emailContractExpiry;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface OnboardingViewModel extends ViewModel {
|
||||
/**
|
||||
* ViewData for Onboarding
|
||||
* This is the JSON-serializable input for the Template.
|
||||
*/
|
||||
export interface OnboardingViewData {
|
||||
isAlreadyOnboarded: boolean;
|
||||
}
|
||||
|
||||
export class OnboardingViewModel extends ViewModel {
|
||||
private readonly data: OnboardingViewData;
|
||||
|
||||
constructor(data: OnboardingViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get isAlreadyOnboarded(): boolean {
|
||||
return this.data.isAlreadyOnboarded;
|
||||
}
|
||||
}
|
||||
22
apps/website/lib/view-models/PaymentMethodViewModel.ts
Normal file
22
apps/website/lib/view-models/PaymentMethodViewModel.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { PaymentMethodViewData } from "@/lib/view-data/BillingViewData";
|
||||
|
||||
export class PaymentMethodViewModel extends ViewModel {
|
||||
private readonly data: PaymentMethodViewData;
|
||||
|
||||
constructor(data: PaymentMethodViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get type(): string { return this.data.type; }
|
||||
get last4(): string { return this.data.last4; }
|
||||
get brand(): string | undefined { return this.data.brand; }
|
||||
get isDefault(): boolean { return this.data.isDefault; }
|
||||
get expiryMonth(): number | undefined { return this.data.expiryMonth; }
|
||||
get expiryYear(): number | undefined { return this.data.expiryYear; }
|
||||
get bankName(): string | undefined { return this.data.bankName; }
|
||||
get displayLabel(): string { return this.data.displayLabel; }
|
||||
get expiryDisplay(): string | null { return this.data.expiryDisplay; }
|
||||
}
|
||||
@@ -1,33 +1,40 @@
|
||||
import type { PaymentDTO } from '@/lib/types/generated/PaymentDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { CurrencyDisplay } from "../display-objects/CurrencyDisplay";
|
||||
import { DateDisplay } from "../display-objects/DateDisplay";
|
||||
import { PaymentTypeDisplay } from "../display-objects/PaymentTypeDisplay";
|
||||
import { PayerTypeDisplay } from "../display-objects/PayerTypeDisplay";
|
||||
import { StatusDisplay } from "../display-objects/StatusDisplay";
|
||||
import type { PaymentViewData } from "../view-data/PaymentViewData";
|
||||
|
||||
export class PaymentViewModel extends ViewModel {
|
||||
id!: string;
|
||||
type!: string;
|
||||
amount!: number;
|
||||
platformFee!: number;
|
||||
netAmount!: number;
|
||||
payerId!: string;
|
||||
payerType!: string;
|
||||
leagueId!: string;
|
||||
seasonId?: string;
|
||||
status!: string;
|
||||
createdAt!: Date;
|
||||
completedAt?: Date;
|
||||
private readonly data: PaymentViewData;
|
||||
|
||||
constructor(dto: PaymentDTO) {
|
||||
Object.assign(this, dto);
|
||||
constructor(data: PaymentViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get type(): string { return this.data.type; }
|
||||
get amount(): number { return this.data.amount; }
|
||||
get platformFee(): number { return this.data.platformFee; }
|
||||
get netAmount(): number { return this.data.netAmount; }
|
||||
get payerId(): string { return this.data.payerId; }
|
||||
get payerType(): string { return this.data.payerType; }
|
||||
get leagueId(): string { return this.data.leagueId; }
|
||||
get seasonId(): string | undefined { return this.data.seasonId; }
|
||||
get status(): string { return this.data.status; }
|
||||
get createdAt(): string { return this.data.createdAt; }
|
||||
get completedAt(): string | undefined { return this.data.completedAt; }
|
||||
|
||||
/** UI-specific: Formatted amount */
|
||||
get formattedAmount(): string {
|
||||
return `€${this.amount.toFixed(2)}`; // Assuming EUR currency
|
||||
return CurrencyDisplay.format(this.amount, 'EUR');
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted net amount */
|
||||
get formattedNetAmount(): string {
|
||||
return `€${this.netAmount.toFixed(2)}`;
|
||||
return CurrencyDisplay.format(this.netAmount, 'EUR');
|
||||
}
|
||||
|
||||
/** UI-specific: Status color */
|
||||
@@ -43,26 +50,26 @@ export class PaymentViewModel extends ViewModel {
|
||||
|
||||
/** UI-specific: Formatted created date */
|
||||
get formattedCreatedAt(): string {
|
||||
return this.createdAt.toLocaleString();
|
||||
return DateDisplay.formatShort(this.createdAt);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted completed date */
|
||||
get formattedCompletedAt(): string {
|
||||
return this.completedAt ? this.completedAt.toLocaleString() : 'Not completed';
|
||||
return this.completedAt ? DateDisplay.formatShort(this.completedAt) : 'Not completed';
|
||||
}
|
||||
|
||||
/** UI-specific: Status display */
|
||||
get statusDisplay(): string {
|
||||
return this.status.charAt(0).toUpperCase() + this.status.slice(1);
|
||||
return StatusDisplay.transactionStatus(this.status);
|
||||
}
|
||||
|
||||
/** UI-specific: Type display */
|
||||
get typeDisplay(): string {
|
||||
return this.type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
return PaymentTypeDisplay.format(this.type);
|
||||
}
|
||||
|
||||
/** UI-specific: Payer type display */
|
||||
get payerTypeDisplay(): string {
|
||||
return this.payerType.charAt(0).toUpperCase() + this.payerType.slice(1);
|
||||
return PayerTypeDisplay.format(this.payerType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
apps/website/lib/view-models/PrivacySettingsViewModel.ts
Normal file
17
apps/website/lib/view-models/PrivacySettingsViewModel.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { PrivacySettingsViewData } from "../view-data/PrivacySettingsViewData";
|
||||
|
||||
export class PrivacySettingsViewModel extends ViewModel {
|
||||
publicProfile: boolean;
|
||||
showStats: boolean;
|
||||
showActiveSponsorships: boolean;
|
||||
allowDirectContact: boolean;
|
||||
|
||||
constructor(data: PrivacySettingsViewData) {
|
||||
super();
|
||||
this.publicProfile = data.publicProfile;
|
||||
this.showStats = data.showStats;
|
||||
this.showActiveSponsorships = data.showActiveSponsorships;
|
||||
this.allowDirectContact = data.allowDirectContact;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,44 @@
|
||||
import type { PrizeDTO } from '@/lib/types/generated/PrizeDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { CurrencyDisplay } from "../display-objects/CurrencyDisplay";
|
||||
import { FinishDisplay } from "../display-objects/FinishDisplay";
|
||||
import { DateDisplay } from "../display-objects/DateDisplay";
|
||||
import { PrizeTypeDisplay } from "../display-objects/PrizeTypeDisplay";
|
||||
import type { PrizeViewData } from "../view-data/PrizeViewData";
|
||||
|
||||
export class PrizeViewModel extends ViewModel {
|
||||
id!: string;
|
||||
leagueId!: string;
|
||||
seasonId!: string;
|
||||
position!: number;
|
||||
name!: string;
|
||||
amount!: number;
|
||||
type!: string;
|
||||
description?: string;
|
||||
awarded!: boolean;
|
||||
awardedTo?: string;
|
||||
awardedAt?: Date;
|
||||
createdAt!: Date;
|
||||
private readonly data: PrizeViewData;
|
||||
|
||||
constructor(dto: PrizeDTO) {
|
||||
this.id = dto.id;
|
||||
this.leagueId = dto.leagueId;
|
||||
this.seasonId = dto.seasonId;
|
||||
this.position = dto.position;
|
||||
this.name = dto.name;
|
||||
this.amount = dto.amount;
|
||||
this.type = dto.type;
|
||||
this.description = dto.description;
|
||||
this.awarded = dto.awarded;
|
||||
this.awardedTo = dto.awardedTo;
|
||||
this.awardedAt = dto.awardedAt ? new Date(dto.awardedAt) : undefined;
|
||||
this.createdAt = new Date(dto.createdAt);
|
||||
constructor(data: PrizeViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get leagueId(): string { return this.data.leagueId; }
|
||||
get seasonId(): string { return this.data.seasonId; }
|
||||
get position(): number { return this.data.position; }
|
||||
get name(): string { return this.data.name; }
|
||||
get amount(): number { return this.data.amount; }
|
||||
get type(): string { return this.data.type; }
|
||||
get description(): string | undefined { return this.data.description; }
|
||||
get awarded(): boolean { return this.data.awarded; }
|
||||
get awardedTo(): string | undefined { return this.data.awardedTo; }
|
||||
get awardedAt(): string | undefined { return this.data.awardedAt; }
|
||||
get createdAt(): string { return this.data.createdAt; }
|
||||
|
||||
/** UI-specific: Formatted amount */
|
||||
get formattedAmount(): string {
|
||||
return `€${this.amount.toFixed(2)}`; // Assuming EUR
|
||||
return CurrencyDisplay.format(this.amount, 'EUR');
|
||||
}
|
||||
|
||||
/** UI-specific: Position display */
|
||||
get positionDisplay(): string {
|
||||
switch (this.position) {
|
||||
case 1: return '1st Place';
|
||||
case 2: return '2nd Place';
|
||||
case 3: return '3rd Place';
|
||||
default: return `${this.position}th Place`;
|
||||
}
|
||||
return FinishDisplay.format(this.position);
|
||||
}
|
||||
|
||||
/** UI-specific: Type display */
|
||||
get typeDisplay(): string {
|
||||
switch (this.type) {
|
||||
case 'cash': return 'Cash Prize';
|
||||
case 'merchandise': return 'Merchandise';
|
||||
case 'other': return 'Other';
|
||||
default: return this.type;
|
||||
}
|
||||
return PrizeTypeDisplay.format(this.type);
|
||||
}
|
||||
|
||||
/** UI-specific: Status display */
|
||||
@@ -73,11 +58,11 @@ export class PrizeViewModel extends ViewModel {
|
||||
|
||||
/** UI-specific: Formatted awarded date */
|
||||
get formattedAwardedAt(): string {
|
||||
return this.awardedAt ? this.awardedAt.toLocaleString() : 'Not awarded';
|
||||
return this.awardedAt ? DateDisplay.formatShort(this.awardedAt) : 'Not awarded';
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted created date */
|
||||
get formattedCreatedAt(): string {
|
||||
return this.createdAt.toLocaleString();
|
||||
return DateDisplay.formatShort(this.createdAt);
|
||||
}
|
||||
}
|
||||
|
||||
93
apps/website/lib/view-models/ProfileOverviewSubViewModels.ts
Normal file
93
apps/website/lib/view-models/ProfileOverviewSubViewModels.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface ProfileOverviewDriverSummaryViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
iracingId: string | null;
|
||||
joinedAt: string;
|
||||
rating: number | null;
|
||||
globalRank: number | null;
|
||||
consistency: number | null;
|
||||
bio: string | null;
|
||||
totalDrivers: number | null;
|
||||
}
|
||||
|
||||
export interface ProfileOverviewStatsViewModel extends ViewModel {
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
dnfs: number;
|
||||
avgFinish: number | null;
|
||||
bestFinish: number | null;
|
||||
worstFinish: number | null;
|
||||
finishRate: number | null;
|
||||
winRate: number | null;
|
||||
podiumRate: number | null;
|
||||
percentile: number | null;
|
||||
rating: number | null;
|
||||
consistency: number | null;
|
||||
overallRank: number | null;
|
||||
}
|
||||
|
||||
export interface ProfileOverviewFinishDistributionViewModel extends ViewModel {
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
topTen: number;
|
||||
dnfs: number;
|
||||
other: number;
|
||||
}
|
||||
|
||||
export interface ProfileOverviewTeamMembershipViewModel extends ViewModel {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
teamTag: string | null;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
export interface ProfileOverviewSocialFriendSummaryViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export interface ProfileOverviewSocialSummaryViewModel extends ViewModel {
|
||||
friendsCount: number;
|
||||
friends: ProfileOverviewSocialFriendSummaryViewModel[];
|
||||
}
|
||||
|
||||
export type ProfileOverviewSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
|
||||
|
||||
export type ProfileOverviewAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary';
|
||||
|
||||
export interface ProfileOverviewAchievementViewModel extends ViewModel {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
|
||||
rarity: ProfileOverviewAchievementRarity;
|
||||
earnedAt: string;
|
||||
}
|
||||
|
||||
export interface ProfileOverviewSocialHandleViewModel extends ViewModel {
|
||||
platform: ProfileOverviewSocialPlatform;
|
||||
handle: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ProfileOverviewExtendedProfileViewModel extends ViewModel {
|
||||
socialHandles: ProfileOverviewSocialHandleViewModel[];
|
||||
achievements: ProfileOverviewAchievementViewModel[];
|
||||
racingStyle: string;
|
||||
favoriteTrack: string;
|
||||
favoriteCar: string;
|
||||
timezone: string;
|
||||
availableHours: string;
|
||||
lookingForTeam: boolean;
|
||||
openToRequests: boolean;
|
||||
}
|
||||
@@ -1,120 +1,26 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { ProfileOverviewViewData } from "../view-data/ProfileOverviewViewData";
|
||||
import type {
|
||||
ProfileOverviewDriverSummaryViewModel,
|
||||
ProfileOverviewStatsViewModel,
|
||||
ProfileOverviewFinishDistributionViewModel,
|
||||
ProfileOverviewTeamMembershipViewModel,
|
||||
ProfileOverviewSocialSummaryViewModel,
|
||||
ProfileOverviewExtendedProfileViewModel
|
||||
} from "./ProfileOverviewSubViewModels";
|
||||
|
||||
export interface ProfileOverviewDriverSummaryViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
iracingId: string | null;
|
||||
joinedAt: string;
|
||||
rating: number | null;
|
||||
globalRank: number | null;
|
||||
consistency: number | null;
|
||||
bio: string | null;
|
||||
totalDrivers: number | null;
|
||||
export class ProfileOverviewViewModel extends ViewModel {
|
||||
private readonly data: ProfileOverviewViewData;
|
||||
|
||||
constructor(data: ProfileOverviewViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get currentDriver(): ProfileOverviewDriverSummaryViewModel | null { return this.data.currentDriver; }
|
||||
get stats(): ProfileOverviewStatsViewModel | null { return this.data.stats; }
|
||||
get finishDistribution(): ProfileOverviewFinishDistributionViewModel | null { return this.data.finishDistribution; }
|
||||
get teamMemberships(): ProfileOverviewTeamMembershipViewModel[] { return this.data.teamMemberships; }
|
||||
get socialSummary(): ProfileOverviewSocialSummaryViewModel { return this.data.socialSummary; }
|
||||
get extendedProfile(): ProfileOverviewExtendedProfileViewModel | null { return this.data.extendedProfile; }
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface ProfileOverviewStatsViewModel extends ViewModel {
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
dnfs: number;
|
||||
avgFinish: number | null;
|
||||
bestFinish: number | null;
|
||||
worstFinish: number | null;
|
||||
finishRate: number | null;
|
||||
winRate: number | null;
|
||||
podiumRate: number | null;
|
||||
percentile: number | null;
|
||||
rating: number | null;
|
||||
consistency: number | null;
|
||||
overallRank: number | null;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface ProfileOverviewFinishDistributionViewModel extends ViewModel {
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
topTen: number;
|
||||
dnfs: number;
|
||||
other: number;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface ProfileOverviewTeamMembershipViewModel extends ViewModel {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
teamTag: string | null;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface ProfileOverviewSocialFriendSummaryViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface ProfileOverviewSocialSummaryViewModel extends ViewModel {
|
||||
friendsCount: number;
|
||||
friends: ProfileOverviewSocialFriendSummaryViewModel[];
|
||||
}
|
||||
|
||||
export type ProfileOverviewSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
|
||||
|
||||
export type ProfileOverviewAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface ProfileOverviewAchievementViewModel extends ViewModel {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
|
||||
rarity: ProfileOverviewAchievementRarity;
|
||||
earnedAt: string;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface ProfileOverviewSocialHandleViewModel extends ViewModel {
|
||||
platform: ProfileOverviewSocialPlatform;
|
||||
handle: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface ProfileOverviewExtendedProfileViewModel extends ViewModel {
|
||||
socialHandles: ProfileOverviewSocialHandleViewModel[];
|
||||
achievements: ProfileOverviewAchievementViewModel[];
|
||||
racingStyle: string;
|
||||
favoriteTrack: string;
|
||||
favoriteCar: string;
|
||||
timezone: string;
|
||||
availableHours: string;
|
||||
lookingForTeam: boolean;
|
||||
openToRequests: boolean;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export interface ProfileOverviewViewModel extends ViewModel {
|
||||
currentDriver: ProfileOverviewDriverSummaryViewModel | null;
|
||||
stats: ProfileOverviewStatsViewModel | null;
|
||||
finishDistribution: ProfileOverviewFinishDistributionViewModel | null;
|
||||
teamMemberships: ProfileOverviewTeamMembershipViewModel[];
|
||||
socialSummary: ProfileOverviewSocialSummaryViewModel;
|
||||
extendedProfile: ProfileOverviewExtendedProfileViewModel | null;
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { DriverSummaryDTO } from '@/lib/types/generated/DriverSummaryDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { ProtestDriverViewData } from "../view-data/ProtestDriverViewData";
|
||||
|
||||
export class ProtestDriverViewModel extends ViewModel {
|
||||
constructor(private readonly dto: DriverSummaryDTO) {}
|
||||
constructor(private readonly data: ProtestDriverViewData) {
|
||||
super();
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.dto.id;
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.dto.name;
|
||||
return this.data.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +1,30 @@
|
||||
import { ProtestDTO } from '@/lib/types/generated/ProtestDTO';
|
||||
import { RaceProtestDTO } from '@/lib/types/generated/RaceProtestDTO';
|
||||
import { DateDisplay } from '../display-objects/DateDisplay';
|
||||
import { StatusDisplay } from '../display-objects/StatusDisplay';
|
||||
|
||||
/**
|
||||
* Protest view model
|
||||
* Represents a race protest
|
||||
*/
|
||||
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||
import { StatusDisplay } from '@/lib/display-objects/StatusDisplay';
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { ProtestViewData } from "@/lib/view-data/ProtestViewData";
|
||||
|
||||
export class ProtestViewModel extends ViewModel {
|
||||
id: string;
|
||||
raceId: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
description: string;
|
||||
submittedAt: string;
|
||||
filedAt?: string;
|
||||
status: string;
|
||||
reviewedAt?: string;
|
||||
decisionNotes?: string;
|
||||
incident?: { lap?: number; description?: string } | null;
|
||||
proofVideoUrl?: string | null;
|
||||
comment?: string | null;
|
||||
private readonly data: ProtestViewData;
|
||||
|
||||
constructor(dto: ProtestDTO | RaceProtestDTO) {
|
||||
this.id = dto.id;
|
||||
|
||||
// Type narrowing for raceId
|
||||
if ('raceId' in dto) {
|
||||
this.raceId = dto.raceId;
|
||||
} else {
|
||||
this.raceId = '';
|
||||
}
|
||||
|
||||
this.protestingDriverId = dto.protestingDriverId;
|
||||
this.accusedDriverId = dto.accusedDriverId;
|
||||
|
||||
// Type narrowing for description
|
||||
if ('description' in dto && typeof dto.description === 'string') {
|
||||
this.description = dto.description;
|
||||
} else {
|
||||
this.description = '';
|
||||
}
|
||||
|
||||
// Type narrowing for submittedAt and filedAt
|
||||
if ('submittedAt' in dto && typeof dto.submittedAt === 'string') {
|
||||
this.submittedAt = dto.submittedAt;
|
||||
} else if ('filedAt' in dto && typeof dto.filedAt === 'string') {
|
||||
this.submittedAt = dto.filedAt;
|
||||
} else {
|
||||
this.submittedAt = '';
|
||||
}
|
||||
|
||||
if ('filedAt' in dto && typeof dto.filedAt === 'string') {
|
||||
this.filedAt = dto.filedAt;
|
||||
} else if ('submittedAt' in dto && typeof dto.submittedAt === 'string') {
|
||||
this.filedAt = dto.submittedAt;
|
||||
}
|
||||
|
||||
// Handle different DTO structures
|
||||
if ('status' in dto && typeof dto.status === 'string') {
|
||||
this.status = dto.status;
|
||||
} else {
|
||||
this.status = 'pending';
|
||||
}
|
||||
|
||||
// Handle incident data
|
||||
if ('incident' in dto && dto.incident) {
|
||||
const incident = dto.incident as { lap?: number; description?: string };
|
||||
this.incident = {
|
||||
lap: typeof incident.lap === 'number' ? incident.lap : undefined,
|
||||
description: typeof incident.description === 'string' ? incident.description : undefined
|
||||
};
|
||||
} else if (('lap' in dto && typeof (dto as { lap?: number }).lap === 'number') ||
|
||||
('description' in dto && typeof (dto as { description?: string }).description === 'string')) {
|
||||
this.incident = {
|
||||
lap: 'lap' in dto ? (dto as { lap?: number }).lap : undefined,
|
||||
description: 'description' in dto ? (dto as { description?: string }).description : undefined
|
||||
};
|
||||
} else {
|
||||
this.incident = null;
|
||||
}
|
||||
|
||||
if ('proofVideoUrl' in dto) {
|
||||
this.proofVideoUrl = (dto as { proofVideoUrl?: string }).proofVideoUrl || null;
|
||||
}
|
||||
if ('comment' in dto) {
|
||||
this.comment = (dto as { comment?: string }).comment || null;
|
||||
}
|
||||
|
||||
// Status and decision metadata are not part of the protest DTO in this build; they default to a pending, unreviewed protest
|
||||
if (!('status' in dto)) {
|
||||
this.status = 'pending';
|
||||
}
|
||||
this.reviewedAt = undefined;
|
||||
this.decisionNotes = undefined;
|
||||
constructor(data: ProtestViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get raceId(): string { return this.data.raceId; }
|
||||
get protestingDriverId(): string { return this.data.protestingDriverId; }
|
||||
get accusedDriverId(): string { return this.data.accusedDriverId; }
|
||||
get description(): string { return this.data.description; }
|
||||
get submittedAt(): string { return this.data.submittedAt; }
|
||||
get filedAt(): string | undefined { return this.data.filedAt; }
|
||||
get status(): string { return this.data.status; }
|
||||
get reviewedAt(): string | undefined { return this.data.reviewedAt; }
|
||||
get decisionNotes(): string | undefined { return this.data.decisionNotes; }
|
||||
get incident(): { lap?: number; description?: string } | null | undefined { return this.data.incident; }
|
||||
get proofVideoUrl(): string | null | undefined { return this.data.proofVideoUrl; }
|
||||
get comment(): string | null | undefined { return this.data.comment; }
|
||||
|
||||
/** UI-specific: Formatted submitted date */
|
||||
get formattedSubmittedAt(): string {
|
||||
return DateDisplay.formatShort(this.submittedAt);
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { RaceDetailEntryDTO } from '@/lib/types/generated/RaceDetailEntryDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RaceDetailEntryViewData } from "../view-data/RaceDetailEntryViewData";
|
||||
|
||||
export class RaceDetailEntryViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
isCurrentUser: boolean;
|
||||
rating: number | null;
|
||||
private readonly data: RaceDetailEntryViewData;
|
||||
|
||||
constructor(dto: RaceDetailEntryDTO, currentDriverId: string, rating?: number) {
|
||||
this.id = dto.id;
|
||||
this.name = dto.name;
|
||||
this.country = dto.country;
|
||||
this.avatarUrl = dto.avatarUrl || '';
|
||||
this.isCurrentUser = dto.id === currentDriverId;
|
||||
this.rating = rating ?? null;
|
||||
constructor(data: RaceDetailEntryViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get country(): string { return this.data.country; }
|
||||
get avatarUrl(): string { return this.data.avatarUrl; }
|
||||
get isCurrentUser(): boolean { return this.data.isCurrentUser; }
|
||||
get rating(): number | null { return this.data.rating; }
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
import { RaceDetailUserResultDTO } from '@/lib/types/generated/RaceDetailUserResultDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { DurationDisplay } from "../display-objects/DurationDisplay";
|
||||
import type { RaceDetailUserResultViewData } from "../view-data/RaceDetailUserResultViewData";
|
||||
|
||||
export class RaceDetailUserResultViewModel extends ViewModel {
|
||||
position!: number;
|
||||
startPosition!: number;
|
||||
incidents!: number;
|
||||
fastestLap!: number;
|
||||
positionChange!: number;
|
||||
isPodium!: boolean;
|
||||
isClean!: boolean;
|
||||
ratingChange!: number;
|
||||
private readonly data: RaceDetailUserResultViewData;
|
||||
|
||||
constructor(dto: RaceDetailUserResultDTO) {
|
||||
this.position = dto.position;
|
||||
this.startPosition = dto.startPosition;
|
||||
this.incidents = dto.incidents;
|
||||
this.fastestLap = dto.fastestLap;
|
||||
this.positionChange = dto.positionChange;
|
||||
this.isPodium = dto.isPodium;
|
||||
this.isClean = dto.isClean;
|
||||
this.ratingChange = dto.ratingChange ?? 0;
|
||||
constructor(data: RaceDetailUserResultViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get position(): number { return this.data.position; }
|
||||
get startPosition(): number { return this.data.startPosition; }
|
||||
get incidents(): number { return this.data.incidents; }
|
||||
get fastestLap(): number { return this.data.fastestLap; }
|
||||
get positionChange(): number { return this.data.positionChange; }
|
||||
get isPodium(): boolean { return this.data.isPodium; }
|
||||
get isClean(): boolean { return this.data.isClean; }
|
||||
get ratingChange(): number { return this.data.ratingChange; }
|
||||
|
||||
/** UI-specific: Display for position change */
|
||||
get positionChangeDisplay(): string {
|
||||
if (this.positionChange > 0) return `+${this.positionChange}`;
|
||||
@@ -58,9 +54,6 @@ export class RaceDetailUserResultViewModel extends ViewModel {
|
||||
/** UI-specific: Formatted lap time */
|
||||
get lapTimeFormatted(): string {
|
||||
if (this.fastestLap <= 0) return '--:--.---';
|
||||
const minutes = Math.floor(this.fastestLap / 60);
|
||||
const seconds = Math.floor(this.fastestLap % 60);
|
||||
const milliseconds = Math.floor((this.fastestLap % 1) * 1000);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
|
||||
return DurationDisplay.formatSeconds(this.fastestLap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
apps/website/lib/view-models/RaceDetailsLeagueViewModel.ts
Normal file
11
apps/website/lib/view-models/RaceDetailsLeagueViewModel.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RaceDetailsLeagueViewData } from "../view-data/RaceDetailsViewData";
|
||||
|
||||
export class RaceDetailsLeagueViewModel extends ViewModel {
|
||||
private readonly data: RaceDetailsLeagueViewData;
|
||||
constructor(data: RaceDetailsLeagueViewData) { super(); this.data = data; }
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get description(): string | null | undefined { return this.data.description; }
|
||||
get settings(): unknown { return this.data.settings; }
|
||||
}
|
||||
13
apps/website/lib/view-models/RaceDetailsRaceViewModel.ts
Normal file
13
apps/website/lib/view-models/RaceDetailsRaceViewModel.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RaceDetailsRaceViewData } from "../view-data/RaceDetailsViewData";
|
||||
|
||||
export class RaceDetailsRaceViewModel extends ViewModel {
|
||||
private readonly data: RaceDetailsRaceViewData;
|
||||
constructor(data: RaceDetailsRaceViewData) { super(); this.data = data; }
|
||||
get id(): string { return this.data.id; }
|
||||
get track(): string { return this.data.track; }
|
||||
get car(): string { return this.data.car; }
|
||||
get scheduledAt(): string { return this.data.scheduledAt; }
|
||||
get status(): string { return this.data.status; }
|
||||
get sessionType(): string { return this.data.sessionType; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RaceDetailsRegistrationViewData } from "../view-data/RaceDetailsViewData";
|
||||
|
||||
export class RaceDetailsRegistrationViewModel extends ViewModel {
|
||||
private readonly data: RaceDetailsRegistrationViewData;
|
||||
constructor(data: RaceDetailsRegistrationViewData) { super(); this.data = data; }
|
||||
get canRegister(): boolean { return this.data.canRegister; }
|
||||
get isUserRegistered(): boolean { return this.data.isUserRegistered; }
|
||||
}
|
||||
@@ -1,41 +1,29 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { RaceDetailEntryViewModel } from './RaceDetailEntryViewModel';
|
||||
import { RaceDetailUserResultViewModel } from './RaceDetailUserResultViewModel';
|
||||
import { RaceDetailsRaceViewModel } from './RaceDetailsRaceViewModel';
|
||||
import { RaceDetailsLeagueViewModel } from './RaceDetailsLeagueViewModel';
|
||||
import { RaceDetailsRegistrationViewModel } from './RaceDetailsRegistrationViewModel';
|
||||
import type { RaceDetailsViewData } from "../view-data/RaceDetailsViewData";
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
export class RaceDetailsViewModel extends ViewModel {
|
||||
private readonly data: RaceDetailsViewData;
|
||||
readonly race: RaceDetailsRaceViewModel | null;
|
||||
readonly league: RaceDetailsLeagueViewModel | null;
|
||||
readonly entryList: RaceDetailEntryViewModel[];
|
||||
readonly registration: RaceDetailsRegistrationViewModel;
|
||||
readonly userResult: RaceDetailUserResultViewModel | null;
|
||||
|
||||
export type RaceDetailsRaceViewModel = ViewModel & {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
sessionType: string;
|
||||
};
|
||||
constructor(data: RaceDetailsViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.race = data.race ? new RaceDetailsRaceViewModel(data.race) : null;
|
||||
this.league = data.league ? new RaceDetailsLeagueViewModel(data.league) : null;
|
||||
this.entryList = data.entryList.map(e => new RaceDetailEntryViewModel(e));
|
||||
this.registration = new RaceDetailsRegistrationViewModel(data.registration);
|
||||
this.userResult = data.userResult ? new RaceDetailUserResultViewModel(data.userResult) : null;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export type RaceDetailsLeagueViewModel = ViewModel & {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
settings?: unknown;
|
||||
};
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export type RaceDetailsRegistrationViewModel = ViewModel & {
|
||||
canRegister: boolean;
|
||||
isUserRegistered: boolean;
|
||||
};
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export type RaceDetailsViewModel = ViewModel & {
|
||||
race: RaceDetailsRaceViewModel | null;
|
||||
league: RaceDetailsLeagueViewModel | null;
|
||||
entryList: RaceDetailEntryViewModel[];
|
||||
registration: RaceDetailsRegistrationViewModel;
|
||||
userResult: RaceDetailUserResultViewModel | null;
|
||||
canReopenRace: boolean;
|
||||
error?: string;
|
||||
};
|
||||
get canReopenRace(): boolean { return this.data.canReopenRace; }
|
||||
get error(): string | undefined { return this.data.error; }
|
||||
}
|
||||
|
||||
@@ -1,65 +1,40 @@
|
||||
// DTO matching the backend RacesPageDataRaceDTO
|
||||
export interface RaceListItemDTO {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
strengthOfField?: number | null;
|
||||
isUpcoming: boolean;
|
||||
isLive: boolean;
|
||||
isPast: boolean;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { RaceStatusDisplay } from "../display-objects/RaceStatusDisplay";
|
||||
import { DateDisplay } from "../display-objects/DateDisplay";
|
||||
import type { RaceListItemViewData } from "../view-data/RaceListItemViewData";
|
||||
|
||||
export class RaceListItemViewModel extends ViewModel {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
strengthOfField: number | null;
|
||||
isUpcoming: boolean;
|
||||
isLive: boolean;
|
||||
isPast: boolean;
|
||||
private readonly data: RaceListItemViewData;
|
||||
|
||||
constructor(dto: RaceListItemDTO) {
|
||||
this.id = dto.id;
|
||||
this.track = dto.track;
|
||||
this.car = dto.car;
|
||||
this.scheduledAt = dto.scheduledAt;
|
||||
this.status = dto.status;
|
||||
this.leagueId = dto.leagueId;
|
||||
this.leagueName = dto.leagueName;
|
||||
this.strengthOfField = dto.strengthOfField ?? null;
|
||||
this.isUpcoming = dto.isUpcoming;
|
||||
this.isLive = dto.isLive;
|
||||
this.isPast = dto.isPast;
|
||||
constructor(data: RaceListItemViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get track(): string { return this.data.track; }
|
||||
get car(): string { return this.data.car; }
|
||||
get scheduledAt(): string { return this.data.scheduledAt; }
|
||||
get status(): string { return this.data.status; }
|
||||
get leagueId(): string { return this.data.leagueId; }
|
||||
get leagueName(): string { return this.data.leagueName; }
|
||||
get strengthOfField(): number | null { return this.data.strengthOfField; }
|
||||
get isUpcoming(): boolean { return this.data.isUpcoming; }
|
||||
get isLive(): boolean { return this.data.isLive; }
|
||||
get isPast(): boolean { return this.data.isPast; }
|
||||
|
||||
get title(): string {
|
||||
return `${this.track} - ${this.car}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted scheduled time */
|
||||
get formattedScheduledTime(): string {
|
||||
return new Date(this.scheduledAt).toLocaleString();
|
||||
return DateDisplay.formatDateTime(this.scheduledAt);
|
||||
}
|
||||
|
||||
/** UI-specific: Badge variant for status */
|
||||
get statusBadgeVariant(): string {
|
||||
switch (this.status) {
|
||||
case 'scheduled': return 'info';
|
||||
case 'running': return 'success';
|
||||
case 'completed': return 'secondary';
|
||||
case 'cancelled': return 'danger';
|
||||
default: return 'default';
|
||||
}
|
||||
return RaceStatusDisplay.getVariant(this.status);
|
||||
}
|
||||
|
||||
/** UI-specific: Time until start in minutes */
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO';
|
||||
import { FinishDisplay } from '../display-objects/FinishDisplay';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { FinishDisplay } from '../display-objects/FinishDisplay';
|
||||
import { DurationDisplay } from '../display-objects/DurationDisplay';
|
||||
import type { RaceResultViewData } from "../view-data/RaceResultViewData";
|
||||
|
||||
export class RaceResultViewModel extends ViewModel {
|
||||
driverId!: string;
|
||||
driverName!: string;
|
||||
avatarUrl!: string;
|
||||
position!: number;
|
||||
startPosition!: number;
|
||||
incidents!: number;
|
||||
fastestLap!: number;
|
||||
positionChange!: number;
|
||||
isPodium!: boolean;
|
||||
isClean!: boolean;
|
||||
private readonly data: RaceResultViewData;
|
||||
|
||||
constructor(dto: RaceResultDTO) {
|
||||
Object.assign(this, dto);
|
||||
constructor(data: RaceResultViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get driverId(): string { return this.data.driverId; }
|
||||
get driverName(): string { return this.data.driverName; }
|
||||
get avatarUrl(): string { return this.data.avatarUrl; }
|
||||
get position(): number { return this.data.position; }
|
||||
get startPosition(): number { return this.data.startPosition; }
|
||||
get incidents(): number { return this.data.incidents; }
|
||||
get fastestLap(): number { return this.data.fastestLap; }
|
||||
get positionChange(): number { return this.data.positionChange; }
|
||||
get isPodium(): boolean { return this.data.isPodium; }
|
||||
get isClean(): boolean { return this.data.isClean; }
|
||||
get id(): string { return this.data.id; }
|
||||
get raceId(): string { return this.data.raceId; }
|
||||
|
||||
/** UI-specific: Display for position change */
|
||||
get positionChangeDisplay(): string {
|
||||
if (this.positionChange > 0) return `+${this.positionChange}`;
|
||||
@@ -58,10 +63,7 @@ export class RaceResultViewModel extends ViewModel {
|
||||
/** UI-specific: Formatted lap time */
|
||||
get lapTimeFormatted(): string {
|
||||
if (this.fastestLap <= 0) return '--:--.---';
|
||||
const minutes = Math.floor(this.fastestLap / 60);
|
||||
const seconds = Math.floor(this.fastestLap % 60);
|
||||
const milliseconds = Math.floor((this.fastestLap % 1) * 1000);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
|
||||
return DurationDisplay.formatSeconds(this.fastestLap);
|
||||
}
|
||||
|
||||
/** Required by ResultsTable */
|
||||
@@ -87,9 +89,4 @@ export class RaceResultViewModel extends ViewModel {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Note: The generated DTO doesn't have id or raceId
|
||||
// These will need to be added when the OpenAPI spec is updated
|
||||
id: string = '';
|
||||
raceId: string = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO';
|
||||
import { RaceResultsDetailDTO } from '@/lib/types/generated/RaceResultsDetailDTO';
|
||||
import { RaceResultViewModel } from './RaceResultViewModel';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { RaceResultViewModel } from './RaceResultViewModel';
|
||||
import type { RaceResultsDetailViewData } from "../view-data/RaceResultsDetailViewData";
|
||||
|
||||
export class RaceResultsDetailViewModel extends ViewModel {
|
||||
raceId: string;
|
||||
track: string;
|
||||
currentUserId: string;
|
||||
private readonly data: RaceResultsDetailViewData;
|
||||
readonly results: RaceResultViewModel[];
|
||||
|
||||
constructor(dto: RaceResultsDetailDTO & { results?: RaceResultDTO[] }, currentUserId: string) {
|
||||
constructor(data: RaceResultsDetailViewData) {
|
||||
super();
|
||||
this.raceId = dto.raceId;
|
||||
this.track = dto.track;
|
||||
this.currentUserId = currentUserId;
|
||||
|
||||
// Map results if provided
|
||||
if (dto.results) {
|
||||
this.results = dto.results.map(r => new RaceResultViewModel(r));
|
||||
}
|
||||
this.data = data;
|
||||
this.results = data.results.map(r => new RaceResultViewModel(r));
|
||||
}
|
||||
|
||||
// Note: The generated DTO is incomplete
|
||||
// These fields will need to be added when the OpenAPI spec is updated
|
||||
results: RaceResultViewModel[] = [];
|
||||
league?: { id: string; name: string };
|
||||
race?: { id: string; track: string; scheduledAt: string };
|
||||
drivers: { id: string; name: string }[] = [];
|
||||
pointsSystem: Record<number, number> = {};
|
||||
fastestLapTime: number = 0;
|
||||
penalties: { driverId: string; type: string; value?: number }[] = [];
|
||||
currentDriverId: string = '';
|
||||
get raceId(): string { return this.data.raceId; }
|
||||
get track(): string { return this.data.track; }
|
||||
get currentUserId(): string { return this.data.currentUserId; }
|
||||
get league() { return this.data.league; }
|
||||
get race() { return this.data.race; }
|
||||
get drivers() { return this.data.drivers; }
|
||||
get pointsSystem() { return this.data.pointsSystem; }
|
||||
get fastestLapTime(): number { return this.data.fastestLapTime; }
|
||||
get penalties() { return this.data.penalties; }
|
||||
get currentDriverId(): string { return this.data.currentDriverId; }
|
||||
|
||||
/** UI-specific: Results sorted by position */
|
||||
get resultsByPosition(): RaceResultViewModel[] {
|
||||
@@ -63,4 +54,4 @@ export class RaceResultsDetailViewModel extends ViewModel {
|
||||
averageIncidents: total > 0 ? totalIncidents / total : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import type { RaceStatsDTO } from '@/lib/types/generated/RaceStatsDTO';
|
||||
|
||||
/**
|
||||
* Race stats view model
|
||||
* Represents race statistics for display
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RaceStatsViewData } from "../view-data/RaceStatsViewData";
|
||||
|
||||
export class RaceStatsViewModel extends ViewModel {
|
||||
totalRaces: number;
|
||||
private readonly data: RaceStatsViewData;
|
||||
|
||||
constructor(dto: RaceStatsDTO) {
|
||||
this.totalRaces = dto.totalRaces;
|
||||
constructor(data: RaceStatsViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get totalRaces(): number { return this.data.totalRaces; }
|
||||
|
||||
/** UI-specific: Formatted total races */
|
||||
get formattedTotalRaces(): string {
|
||||
// Client-only formatting
|
||||
return this.totalRaces.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +1,20 @@
|
||||
// DTO interfaces matching the API responses
|
||||
interface RaceDetailDTO {
|
||||
race: {
|
||||
id: string;
|
||||
track: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
} | null;
|
||||
league: {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface RaceProtestsDTO {
|
||||
protests: Array<{
|
||||
id: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
incident: {
|
||||
lap: number;
|
||||
description: string;
|
||||
};
|
||||
filedAt: string;
|
||||
status: string;
|
||||
decisionNotes?: string;
|
||||
proofVideoUrl?: string;
|
||||
}>;
|
||||
driverMap: Record<string, { id: string; name: string }>;
|
||||
}
|
||||
|
||||
interface RacePenaltiesDTO {
|
||||
penalties: Array<{
|
||||
id: string;
|
||||
driverId: string;
|
||||
type: string;
|
||||
value: number;
|
||||
reason: string;
|
||||
notes?: string;
|
||||
}>;
|
||||
driverMap: Record<string, { id: string; name: string }>;
|
||||
}
|
||||
|
||||
interface RaceStewardingDTO {
|
||||
raceDetail: RaceDetailDTO;
|
||||
protests: RaceProtestsDTO;
|
||||
penalties: RacePenaltiesDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Race Stewarding View Model
|
||||
* Represents all data needed for race stewarding (protests, penalties, race info)
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RaceStewardingViewData } from "../view-data/RaceStewardingViewData";
|
||||
|
||||
export class RaceStewardingViewModel extends ViewModel {
|
||||
race: RaceDetailDTO['race'];
|
||||
league: RaceDetailDTO['league'];
|
||||
protests: RaceProtestsDTO['protests'];
|
||||
penalties: RacePenaltiesDTO['penalties'];
|
||||
driverMap: Record<string, { id: string; name: string }>;
|
||||
private readonly data: RaceStewardingViewData;
|
||||
|
||||
constructor(dto: RaceStewardingDTO) {
|
||||
this.race = dto.raceDetail.race;
|
||||
this.league = dto.raceDetail.league;
|
||||
this.protests = dto.protests.protests;
|
||||
this.penalties = dto.penalties.penalties;
|
||||
|
||||
// Merge driver maps from protests and penalties
|
||||
this.driverMap = { ...dto.protests.driverMap, ...dto.penalties.driverMap };
|
||||
constructor(data: RaceStewardingViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get race() { return this.data.race; }
|
||||
get league() { return this.data.league; }
|
||||
get protests() { return this.data.protests; }
|
||||
get penalties() { return this.data.penalties; }
|
||||
get driverMap() { return this.data.driverMap; }
|
||||
|
||||
/** UI-specific: Pending protests */
|
||||
get pendingProtests() {
|
||||
return this.protests.filter(p => p.status === 'pending' || p.status === 'under_review');
|
||||
|
||||
@@ -1,63 +1,27 @@
|
||||
import { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
||||
import { RacesPageDataRaceDTO } from '@/lib/types/generated/RacesPageDataRaceDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { RaceViewData } from "../view-data/RaceViewData";
|
||||
|
||||
export class RaceViewModel extends ViewModel {
|
||||
constructor(
|
||||
private readonly dto: RaceDTO | RacesPageDataRaceDTO,
|
||||
private readonly _status?: string,
|
||||
private readonly _registeredCount?: number,
|
||||
private readonly _strengthOfField?: number
|
||||
) {}
|
||||
private readonly data: RaceViewData;
|
||||
|
||||
get id(): string {
|
||||
return this.dto.id;
|
||||
constructor(data: RaceViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
if ('name' in this.dto) {
|
||||
return this.dto.name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
get date(): string {
|
||||
if ('date' in this.dto) {
|
||||
return this.dto.date;
|
||||
}
|
||||
if ('scheduledAt' in this.dto) {
|
||||
return this.dto.scheduledAt;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
get scheduledAt(): string {
|
||||
return this.date;
|
||||
}
|
||||
|
||||
get track(): string {
|
||||
return 'track' in this.dto ? this.dto.track || '' : '';
|
||||
}
|
||||
|
||||
get car(): string {
|
||||
return 'car' in this.dto ? this.dto.car || '' : '';
|
||||
}
|
||||
|
||||
get status(): string | undefined {
|
||||
return this._status || ('status' in this.dto ? this.dto.status : undefined);
|
||||
}
|
||||
|
||||
get registeredCount(): number | undefined {
|
||||
return this._registeredCount;
|
||||
}
|
||||
|
||||
get strengthOfField(): number | undefined {
|
||||
return this._strengthOfField || ('strengthOfField' in this.dto ? this.dto.strengthOfField : undefined);
|
||||
}
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get date(): string { return this.data.date; }
|
||||
get scheduledAt(): string { return this.data.date; }
|
||||
get track(): string { return this.data.track; }
|
||||
get car(): string { return this.data.car; }
|
||||
get status(): string | undefined { return this.data.status; }
|
||||
get registeredCount(): number | undefined { return this.data.registeredCount; }
|
||||
get strengthOfField(): number | undefined { return this.data.strengthOfField; }
|
||||
|
||||
/** UI-specific: Formatted date */
|
||||
get formattedDate(): string {
|
||||
// Client-only formatting
|
||||
return new Date(this.date).toLocaleDateString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { RaceWithSOFDTO } from '@/lib/types/generated/RaceWithSOFDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RaceWithSOFViewData } from "../view-data/RaceWithSOFViewData";
|
||||
|
||||
export class RaceWithSOFViewModel extends ViewModel {
|
||||
id: string;
|
||||
track: string;
|
||||
strengthOfField: number | null;
|
||||
private readonly data: RaceWithSOFViewData;
|
||||
|
||||
constructor(dto: RaceWithSOFDTO) {
|
||||
constructor(data: RaceWithSOFViewData) {
|
||||
super();
|
||||
this.id = dto.id;
|
||||
this.track = dto.track;
|
||||
this.strengthOfField = 'strengthOfField' in dto ? dto.strengthOfField ?? null : null;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get track(): string { return this.data.track; }
|
||||
get strengthOfField(): number | null { return this.data.strengthOfField; }
|
||||
}
|
||||
@@ -1,56 +1,19 @@
|
||||
import { RaceListItemViewModel } from './RaceListItemViewModel';
|
||||
import type { RacesPageDataRaceDTO } from '@/lib/types/generated/RacesPageDataRaceDTO';
|
||||
|
||||
// DTO matching the backend RacesPageDataDTO
|
||||
interface RacesPageDTO {
|
||||
races: RacesPageDataRaceDTO[];
|
||||
}
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { RacesPageViewData } from '../view-data/RacesPageViewData';
|
||||
|
||||
/**
|
||||
* Races page view model
|
||||
* Represents the races page data with all races in a single list
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class RacesPageViewModel extends ViewModel {
|
||||
races: RaceListItemViewModel[];
|
||||
private readonly data: RacesPageViewData;
|
||||
readonly races: RaceListItemViewModel[];
|
||||
|
||||
constructor(dto: RacesPageDTO) {
|
||||
this.races = dto.races.map((r) => {
|
||||
const status = 'status' in r ? r.status : 'unknown';
|
||||
|
||||
const isUpcoming =
|
||||
'isUpcoming' in r ? r.isUpcoming :
|
||||
(status === 'upcoming' || status === 'scheduled');
|
||||
|
||||
const isLive =
|
||||
'isLive' in r ? r.isLive :
|
||||
(status === 'live' || status === 'running');
|
||||
|
||||
const isPast =
|
||||
'isPast' in r ? r.isPast :
|
||||
(status === 'completed' || status === 'finished' || status === 'cancelled');
|
||||
|
||||
// Build the RaceListItemDTO from the input with proper type checking
|
||||
const scheduledAt = 'scheduledAt' in r ? r.scheduledAt :
|
||||
('date' in r ? (r as { date?: string }).date : '');
|
||||
|
||||
const normalized = {
|
||||
id: r.id,
|
||||
track: 'track' in r ? r.track : '',
|
||||
car: 'car' in r ? r.car : '',
|
||||
scheduledAt: scheduledAt || '',
|
||||
status: status,
|
||||
leagueId: 'leagueId' in r ? r.leagueId : '',
|
||||
leagueName: 'leagueName' in r ? r.leagueName : '',
|
||||
strengthOfField: 'strengthOfField' in r ? (r as { strengthOfField?: number }).strengthOfField ?? null : null,
|
||||
isUpcoming: Boolean(isUpcoming),
|
||||
isLive: Boolean(isLive),
|
||||
isPast: Boolean(isPast),
|
||||
};
|
||||
|
||||
return new RaceListItemViewModel(normalized);
|
||||
});
|
||||
constructor(data: RacesPageViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.races = data.races.map((r) => new RaceListItemViewModel(r));
|
||||
}
|
||||
|
||||
/** UI-specific: Total races */
|
||||
@@ -87,4 +50,4 @@ export class RacesPageViewModel extends ViewModel {
|
||||
get completedRaces(): RaceListItemViewModel[] {
|
||||
return this.races.filter(r => r.status === 'completed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/**
|
||||
* Record engagement input view model
|
||||
* Represents input data for recording an engagement event
|
||||
*
|
||||
* Note: No matching generated DTO available yet
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RecordEngagementInputViewData } from "../view-data/RecordEngagementInputViewData";
|
||||
|
||||
export class RecordEngagementInputViewModel extends ViewModel {
|
||||
eventType: string;
|
||||
userId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
|
||||
constructor(data: { eventType: string; userId?: string; metadata?: Record<string, unknown> }) {
|
||||
constructor(data: RecordEngagementInputViewData) {
|
||||
super();
|
||||
this.eventType = data.eventType;
|
||||
this.userId = data.userId;
|
||||
this.metadata = data.metadata;
|
||||
@@ -31,4 +31,4 @@ export class RecordEngagementInputViewModel extends ViewModel {
|
||||
get metadataKeysCount(): number {
|
||||
return this.metadata ? Object.keys(this.metadata).length : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import type { RecordEngagementOutputDTO } from '@/lib/types/generated/RecordEngagementOutputDTO';
|
||||
|
||||
/**
|
||||
* Record engagement output view model
|
||||
* Represents the result of recording an engagement event for UI consumption
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class RecordEngagementOutputViewModel extends ViewModel {
|
||||
eventId: string;
|
||||
engagementWeight: number;
|
||||
|
||||
constructor(dto: RecordEngagementOutputDTO) {
|
||||
this.eventId = dto.eventId;
|
||||
this.engagementWeight = dto.engagementWeight;
|
||||
constructor(eventId: string, engagementWeight: number) {
|
||||
super();
|
||||
this.eventId = eventId;
|
||||
this.engagementWeight = engagementWeight;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted event ID for display */
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Record page view input view model
|
||||
* Represents input data for recording a page view
|
||||
*
|
||||
* Note: No matching generated DTO available yet
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RecordPageViewInputViewData } from "../view-data/RecordPageViewInputViewData";
|
||||
|
||||
export class RecordPageViewInputViewModel extends ViewModel {
|
||||
path: string;
|
||||
userId?: string;
|
||||
|
||||
constructor(data: { path: string; userId?: string }) {
|
||||
constructor(data: RecordPageViewInputViewData) {
|
||||
super();
|
||||
this.path = data.path;
|
||||
this.userId = data.userId;
|
||||
}
|
||||
@@ -24,4 +24,4 @@ export class RecordPageViewInputViewModel extends ViewModel {
|
||||
get hasUserContext(): boolean {
|
||||
return this.userId !== undefined && this.userId !== '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import type { RecordPageViewOutputDTO } from '@/lib/types/generated/RecordPageViewOutputDTO';
|
||||
|
||||
/**
|
||||
* Record page view output view model
|
||||
* Represents the result of recording a page view for UI consumption
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RecordPageViewOutputViewData } from "../view-data/RecordPageViewOutputViewData";
|
||||
|
||||
export class RecordPageViewOutputViewModel extends ViewModel {
|
||||
pageViewId: string;
|
||||
|
||||
constructor(dto: RecordPageViewOutputDTO) {
|
||||
this.pageViewId = dto.pageViewId;
|
||||
constructor(data: RecordPageViewOutputViewData) {
|
||||
super();
|
||||
this.pageViewId = data.pageViewId;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted page view ID for display */
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { RemoveLeagueMemberOutputDTO } from '@/lib/types/generated/RemoveLeagueMemberOutputDTO';
|
||||
|
||||
/**
|
||||
* View Model for Remove Member Result
|
||||
*
|
||||
* Represents the result of removing a member from a league in a UI-ready format.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { RemoveMemberViewData } from "../view-data/RemoveMemberViewData";
|
||||
|
||||
export class RemoveMemberViewModel extends ViewModel {
|
||||
success: boolean;
|
||||
|
||||
constructor(dto: RemoveLeagueMemberOutputDTO) {
|
||||
this.success = dto.success;
|
||||
constructor(data: RemoveMemberViewData) {
|
||||
super();
|
||||
this.success = data.success;
|
||||
}
|
||||
|
||||
/** UI-specific: Success message */
|
||||
get successMessage(): string {
|
||||
return this.success ? 'Member removed successfully!' : 'Failed to remove member.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/**
|
||||
* Renewal Alert View Model
|
||||
*
|
||||
* View model for upcoming renewal alerts.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { CurrencyDisplay } from "../display-objects/CurrencyDisplay";
|
||||
import { DateDisplay } from "../display-objects/DateDisplay";
|
||||
import type { RenewalAlertViewData } from "../view-data/RenewalAlertViewData";
|
||||
|
||||
export class RenewalAlertViewModel extends ViewModel {
|
||||
id: string;
|
||||
@@ -12,7 +10,8 @@ export class RenewalAlertViewModel extends ViewModel {
|
||||
renewDate: Date;
|
||||
price: number;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RenewalAlertViewData) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.type = data.type;
|
||||
@@ -21,11 +20,11 @@ export class RenewalAlertViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
get formattedPrice(): string {
|
||||
return `$${this.price}`;
|
||||
return CurrencyDisplay.format(this.price);
|
||||
}
|
||||
|
||||
get formattedRenewDate(): string {
|
||||
return this.renewDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
return DateDisplay.formatShort(this.renewDate);
|
||||
}
|
||||
|
||||
get typeIcon() {
|
||||
@@ -48,4 +47,4 @@ export class RenewalAlertViewModel extends ViewModel {
|
||||
get isUrgent(): boolean {
|
||||
return this.daysUntilRenewal <= 30;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
|
||||
|
||||
export interface CustomPointsConfig {
|
||||
racePoints: number[];
|
||||
poleBonusPoints: number;
|
||||
fastestLapPoints: number;
|
||||
leaderLapPoints: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ScoringConfigurationViewModel
|
||||
*
|
||||
* View model for scoring configuration including presets and custom points
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { LeagueScoringPresetViewModel } from './LeagueScoringPresetViewModel';
|
||||
import type { ScoringConfigurationViewData, CustomPointsConfig } from "../view-data/ScoringConfigurationViewData";
|
||||
|
||||
export class ScoringConfigurationViewModel extends ViewModel {
|
||||
readonly patternId?: string;
|
||||
@@ -21,16 +8,13 @@ export class ScoringConfigurationViewModel extends ViewModel {
|
||||
readonly customPoints?: CustomPointsConfig;
|
||||
readonly currentPreset?: LeagueScoringPresetViewModel;
|
||||
|
||||
constructor(
|
||||
config: LeagueConfigFormModel['scoring'],
|
||||
presets: LeagueScoringPresetViewModel[],
|
||||
customPoints?: CustomPointsConfig
|
||||
) {
|
||||
this.patternId = config.patternId;
|
||||
this.customScoringEnabled = config.customScoringEnabled || false;
|
||||
this.customPoints = customPoints;
|
||||
this.currentPreset = config.patternId
|
||||
? presets.find(p => p.id === config.patternId)
|
||||
constructor(data: ScoringConfigurationViewData) {
|
||||
super();
|
||||
this.patternId = data.config.patternId;
|
||||
this.customScoringEnabled = data.config.customScoringEnabled || false;
|
||||
this.customPoints = data.customPoints;
|
||||
this.currentPreset = data.config.patternId
|
||||
? new LeagueScoringPresetViewModel(data.presets.find(p => p.id === data.config.patternId)!)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -49,4 +33,4 @@ export class ScoringConfigurationViewModel extends ViewModel {
|
||||
leaderLapPoints: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
|
||||
|
||||
/**
|
||||
* Sponsor Dashboard View Model
|
||||
*
|
||||
* Represents dashboard data for a sponsor with UI-specific transformations.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { SponsorDashboardViewData } from "../view-data/SponsorDashboardViewData";
|
||||
|
||||
export class SponsorDashboardViewModel extends ViewModel {
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
|
||||
constructor(dto: SponsorDashboardDTO) {
|
||||
this.sponsorId = dto.sponsorId;
|
||||
this.sponsorName = dto.sponsorName;
|
||||
constructor(data: SponsorDashboardViewData) {
|
||||
super();
|
||||
this.sponsorId = data.sponsorId;
|
||||
this.sponsorName = data.sponsorName;
|
||||
}
|
||||
|
||||
/** UI-specific: Welcome message */
|
||||
|
||||
44
apps/website/lib/view-models/SponsorProfileViewModel.ts
Normal file
44
apps/website/lib/view-models/SponsorProfileViewModel.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { SponsorProfileViewData } from "../view-data/SponsorProfileViewData";
|
||||
|
||||
export class SponsorProfileViewModel extends ViewModel {
|
||||
companyName: string;
|
||||
contactName: string;
|
||||
contactEmail: string;
|
||||
contactPhone: string;
|
||||
website: string;
|
||||
description: string;
|
||||
logoUrl: string | null;
|
||||
industry: string;
|
||||
address: {
|
||||
street: string;
|
||||
city: string;
|
||||
country: string;
|
||||
postalCode: string;
|
||||
};
|
||||
taxId: string;
|
||||
socialLinks: {
|
||||
twitter: string;
|
||||
linkedin: string;
|
||||
instagram: string;
|
||||
};
|
||||
|
||||
constructor(data: SponsorProfileViewData) {
|
||||
super();
|
||||
this.companyName = data.companyName;
|
||||
this.contactName = data.contactName;
|
||||
this.contactEmail = data.contactEmail;
|
||||
this.contactPhone = data.contactPhone;
|
||||
this.website = data.website;
|
||||
this.description = data.description;
|
||||
this.logoUrl = data.logoUrl;
|
||||
this.industry = data.industry;
|
||||
this.address = data.address;
|
||||
this.taxId = data.taxId;
|
||||
this.socialLinks = data.socialLinks;
|
||||
}
|
||||
|
||||
get fullAddress(): string {
|
||||
return `${this.address.street}, ${this.address.city}, ${this.address.postalCode}, ${this.address.country}`;
|
||||
}
|
||||
}
|
||||
@@ -4,100 +4,20 @@
|
||||
* View model for sponsor settings data.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { SponsorProfileViewModel } from "./SponsorProfileViewModel";
|
||||
import { NotificationSettingsViewModel } from "./NotificationSettingsViewModel";
|
||||
import { PrivacySettingsViewModel } from "./PrivacySettingsViewModel";
|
||||
import type { SponsorSettingsViewData } from "../view-data/SponsorSettingsViewData";
|
||||
|
||||
export class SponsorSettingsViewModel extends ViewModel {
|
||||
profile: SponsorProfileViewModel;
|
||||
notifications: NotificationSettingsViewModel;
|
||||
privacy: PrivacySettingsViewModel;
|
||||
|
||||
constructor(data: { profile: unknown; notifications: unknown; privacy: unknown }) {
|
||||
constructor(data: SponsorSettingsViewData) {
|
||||
super();
|
||||
this.profile = new SponsorProfileViewModel(data.profile);
|
||||
this.notifications = new NotificationSettingsViewModel(data.notifications);
|
||||
this.privacy = new PrivacySettingsViewModel(data.privacy);
|
||||
}
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class SponsorProfileViewModel extends ViewModel {
|
||||
companyName: string;
|
||||
contactName: string;
|
||||
contactEmail: string;
|
||||
contactPhone: string;
|
||||
website: string;
|
||||
description: string;
|
||||
logoUrl: string | null;
|
||||
industry: string;
|
||||
address: {
|
||||
street: string;
|
||||
city: string;
|
||||
country: string;
|
||||
postalCode: string;
|
||||
};
|
||||
taxId: string;
|
||||
socialLinks: {
|
||||
twitter: string;
|
||||
linkedin: string;
|
||||
instagram: string;
|
||||
};
|
||||
|
||||
constructor(data: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const d = data as any;
|
||||
this.companyName = d.companyName;
|
||||
this.contactName = d.contactName;
|
||||
this.contactEmail = d.contactEmail;
|
||||
this.contactPhone = d.contactPhone;
|
||||
this.website = d.website;
|
||||
this.description = d.description;
|
||||
this.logoUrl = d.logoUrl;
|
||||
this.industry = d.industry;
|
||||
this.address = d.address;
|
||||
this.taxId = d.taxId;
|
||||
this.socialLinks = d.socialLinks;
|
||||
}
|
||||
|
||||
get fullAddress(): string {
|
||||
return `${this.address.street}, ${this.address.city}, ${this.address.postalCode}, ${this.address.country}`;
|
||||
}
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class NotificationSettingsViewModel extends ViewModel {
|
||||
emailNewSponsorships: boolean;
|
||||
emailWeeklyReport: boolean;
|
||||
emailRaceAlerts: boolean;
|
||||
emailPaymentAlerts: boolean;
|
||||
emailNewOpportunities: boolean;
|
||||
emailContractExpiry: boolean;
|
||||
|
||||
constructor(data: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const d = data as any;
|
||||
this.emailNewSponsorships = d.emailNewSponsorships;
|
||||
this.emailWeeklyReport = d.emailWeeklyReport;
|
||||
this.emailRaceAlerts = d.emailRaceAlerts;
|
||||
this.emailPaymentAlerts = d.emailPaymentAlerts;
|
||||
this.emailNewOpportunities = d.emailNewOpportunities;
|
||||
this.emailContractExpiry = d.emailContractExpiry;
|
||||
}
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class PrivacySettingsViewModel extends ViewModel {
|
||||
publicProfile: boolean;
|
||||
showStats: boolean;
|
||||
showActiveSponsorships: boolean;
|
||||
allowDirectContact: boolean;
|
||||
|
||||
constructor(data: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const d = data as any;
|
||||
this.publicProfile = d.publicProfile;
|
||||
this.showStats = d.showStats;
|
||||
this.showActiveSponsorships = d.showActiveSponsorships;
|
||||
this.allowDirectContact = d.allowDirectContact;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,19 @@
|
||||
import type { SponsorSponsorshipsDTO } from '@/lib/types/generated/SponsorSponsorshipsDTO';
|
||||
import { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel';
|
||||
|
||||
/**
|
||||
* Sponsor Sponsorships View Model
|
||||
*
|
||||
* View model for sponsor sponsorships data with UI-specific transformations.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel';
|
||||
import type { SponsorSponsorshipsViewData } from "../view-data/SponsorSponsorshipsViewData";
|
||||
|
||||
export class SponsorSponsorshipsViewModel extends ViewModel {
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
sponsorships: SponsorshipDetailViewModel[];
|
||||
|
||||
constructor(dto: SponsorSponsorshipsDTO) {
|
||||
this.sponsorId = dto.sponsorId;
|
||||
this.sponsorName = dto.sponsorName;
|
||||
constructor(data: SponsorSponsorshipsViewData) {
|
||||
super();
|
||||
this.sponsorId = data.sponsorId;
|
||||
this.sponsorName = data.sponsorName;
|
||||
this.sponsorships = (data.sponsorships || []).map(s => new SponsorshipDetailViewModel(s));
|
||||
}
|
||||
|
||||
// Note: The generated DTO doesn't have sponsorships array
|
||||
// This will need to be added when the OpenAPI spec is updated
|
||||
sponsorships: SponsorshipDetailViewModel[] = [];
|
||||
|
||||
/** UI-specific: Total sponsorships count */
|
||||
get totalCount(): number {
|
||||
return this.sponsorships.length;
|
||||
@@ -51,4 +44,4 @@ export class SponsorSponsorshipsViewModel extends ViewModel {
|
||||
const firstCurrency = this.sponsorships[0]?.currency || 'USD';
|
||||
return `${firstCurrency} ${this.totalInvestment.toLocaleString()}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
// Note: No generated DTO available for Sponsor yet
|
||||
interface SponsorDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { SponsorViewData } from "../view-data/SponsorViewData";
|
||||
|
||||
export class SponsorViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
declare logoUrl?: string;
|
||||
declare websiteUrl?: string;
|
||||
private readonly data: SponsorViewData;
|
||||
|
||||
constructor(dto: SponsorDTO) {
|
||||
this.id = dto.id;
|
||||
this.name = dto.name;
|
||||
if (dto.logoUrl !== undefined) this.logoUrl = dto.logoUrl;
|
||||
if (dto.websiteUrl !== undefined) this.websiteUrl = dto.websiteUrl;
|
||||
constructor(data: SponsorViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get logoUrl(): string | undefined { return this.data.logoUrl; }
|
||||
get websiteUrl(): string | undefined { return this.data.websiteUrl; }
|
||||
|
||||
/** UI-specific: Display name */
|
||||
get displayName(): string {
|
||||
return this.name;
|
||||
@@ -35,4 +28,4 @@ export class SponsorViewModel extends ViewModel {
|
||||
get websiteLinkText(): string {
|
||||
return 'Visit Website';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SponsorshipDetailDTO } from '@/lib/types/generated/SponsorshipDetailDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { CurrencyDisplay } from "../display-objects/CurrencyDisplay";
|
||||
import type { SponsorshipDetailViewData } from "../view-data/SponsorshipDetailViewData";
|
||||
|
||||
export class SponsorshipDetailViewModel extends ViewModel {
|
||||
id: string;
|
||||
@@ -8,29 +8,35 @@ export class SponsorshipDetailViewModel extends ViewModel {
|
||||
leagueName: string;
|
||||
seasonId: string;
|
||||
seasonName: string;
|
||||
tier: 'main' | 'secondary';
|
||||
status: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
type: string;
|
||||
entityName: string;
|
||||
price: number;
|
||||
impressions: number;
|
||||
|
||||
constructor(dto: SponsorshipDetailDTO) {
|
||||
this.id = dto.id;
|
||||
this.leagueId = dto.leagueId;
|
||||
this.leagueName = dto.leagueName;
|
||||
this.seasonId = dto.seasonId;
|
||||
this.seasonName = dto.seasonName;
|
||||
constructor(data: SponsorshipDetailViewData) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.leagueId = data.leagueId;
|
||||
this.leagueName = data.leagueName;
|
||||
this.seasonId = data.seasonId;
|
||||
this.seasonName = data.seasonName;
|
||||
this.tier = data.tier;
|
||||
this.status = data.status;
|
||||
this.amount = data.amount;
|
||||
this.currency = data.currency;
|
||||
this.type = data.type;
|
||||
this.entityName = data.entityName;
|
||||
this.price = data.price;
|
||||
this.impressions = data.impressions;
|
||||
}
|
||||
|
||||
// Note: The generated DTO is incomplete
|
||||
// These fields will need to be added when the OpenAPI spec is updated
|
||||
tier: 'main' | 'secondary' = 'secondary';
|
||||
status: string = 'active';
|
||||
amount: number = 0;
|
||||
currency: string = 'USD';
|
||||
type: string = 'league';
|
||||
entityName: string = '';
|
||||
price: number = 0;
|
||||
impressions: number = 0;
|
||||
|
||||
/** UI-specific: Formatted amount */
|
||||
get formattedAmount(): string {
|
||||
return `${this.currency} ${this.amount.toLocaleString()}`;
|
||||
return CurrencyDisplay.format(this.amount, this.currency);
|
||||
}
|
||||
|
||||
/** UI-specific: Tier badge variant */
|
||||
@@ -52,4 +58,4 @@ export class SponsorshipDetailViewModel extends ViewModel {
|
||||
get statusDisplay(): string {
|
||||
return this.status.charAt(0).toUpperCase() + this.status.slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
// Note: No generated DTO available for SponsorshipPricing yet
|
||||
interface SponsorshipPricingDTO {
|
||||
mainSlotPrice: number;
|
||||
secondarySlotPrice: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sponsorship Pricing View Model
|
||||
*
|
||||
* View model for sponsorship pricing data with UI-specific transformations.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { CurrencyDisplay } from "../display-objects/CurrencyDisplay";
|
||||
import type { SponsorshipPricingViewData } from "../view-data/SponsorshipPricingViewData";
|
||||
|
||||
export class SponsorshipPricingViewModel extends ViewModel {
|
||||
mainSlotPrice: number;
|
||||
secondarySlotPrice: number;
|
||||
currency: string;
|
||||
|
||||
constructor(dto: SponsorshipPricingDTO) {
|
||||
this.mainSlotPrice = dto.mainSlotPrice;
|
||||
this.secondarySlotPrice = dto.secondarySlotPrice;
|
||||
this.currency = dto.currency;
|
||||
constructor(data: SponsorshipPricingViewData) {
|
||||
super();
|
||||
this.mainSlotPrice = data.mainSlotPrice;
|
||||
this.secondarySlotPrice = data.secondarySlotPrice;
|
||||
this.currency = data.currency;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted main slot price */
|
||||
get formattedMainSlotPrice(): string {
|
||||
return `${this.currency} ${this.mainSlotPrice.toLocaleString()}`;
|
||||
return CurrencyDisplay.format(this.mainSlotPrice, this.currency);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted secondary slot price */
|
||||
get formattedSecondarySlotPrice(): string {
|
||||
return `${this.currency} ${this.secondarySlotPrice.toLocaleString()}`;
|
||||
return CurrencyDisplay.format(this.secondarySlotPrice, this.currency);
|
||||
}
|
||||
|
||||
/** UI-specific: Price difference */
|
||||
@@ -40,7 +31,7 @@ export class SponsorshipPricingViewModel extends ViewModel {
|
||||
|
||||
/** UI-specific: Formatted price difference */
|
||||
get formattedPriceDifference(): string {
|
||||
return `${this.currency} ${this.priceDifference.toLocaleString()}`;
|
||||
return CurrencyDisplay.format(this.priceDifference, this.currency);
|
||||
}
|
||||
|
||||
/** UI-specific: Discount percentage for secondary slot */
|
||||
@@ -48,4 +39,4 @@ export class SponsorshipPricingViewModel extends ViewModel {
|
||||
if (this.mainSlotPrice === 0) return 0;
|
||||
return Math.round((1 - this.secondarySlotPrice / this.mainSlotPrice) * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SponsorshipRequestDTO } from '@/lib/types/generated/SponsorshipRequestDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { CurrencyDisplay } from "../display-objects/CurrencyDisplay";
|
||||
import { DateDisplay } from "../display-objects/DateDisplay";
|
||||
import type { SponsorshipRequestViewData } from "../view-data/SponsorshipRequestViewData";
|
||||
|
||||
export class SponsorshipRequestViewModel extends ViewModel {
|
||||
id: string;
|
||||
@@ -16,33 +17,30 @@ export class SponsorshipRequestViewModel extends ViewModel {
|
||||
platformFee: number;
|
||||
netAmount: number;
|
||||
|
||||
constructor(dto: SponsorshipRequestDTO) {
|
||||
this.id = dto.id;
|
||||
this.sponsorId = dto.sponsorId;
|
||||
this.sponsorName = dto.sponsorName;
|
||||
if (dto.sponsorLogo !== undefined) this.sponsorLogo = dto.sponsorLogo;
|
||||
// Backend currently returns tier as string; normalize to our supported tiers.
|
||||
this.tier = dto.tier === 'main' ? 'main' : 'secondary';
|
||||
this.offeredAmount = dto.offeredAmount;
|
||||
this.currency = dto.currency;
|
||||
this.formattedAmount = dto.formattedAmount;
|
||||
if (dto.message !== undefined) this.message = dto.message;
|
||||
this.createdAt = new Date(dto.createdAt);
|
||||
this.platformFee = dto.platformFee;
|
||||
this.netAmount = dto.netAmount;
|
||||
constructor(data: SponsorshipRequestViewData) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.sponsorId = data.sponsorId;
|
||||
this.sponsorName = data.sponsorName;
|
||||
this.sponsorLogo = data.sponsorLogo;
|
||||
this.tier = data.tier;
|
||||
this.offeredAmount = data.offeredAmount;
|
||||
this.currency = data.currency;
|
||||
this.formattedAmount = data.formattedAmount;
|
||||
this.message = data.message;
|
||||
this.createdAt = new Date(data.createdAt);
|
||||
this.platformFee = data.platformFee;
|
||||
this.netAmount = data.netAmount;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted date */
|
||||
get formattedDate(): string {
|
||||
return this.createdAt.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
return DateDisplay.formatMonthDay(this.createdAt);
|
||||
}
|
||||
|
||||
/** UI-specific: Net amount in dollars */
|
||||
get netAmountDollars(): string {
|
||||
return `$${(this.netAmount / 100).toFixed(2)}`;
|
||||
return CurrencyDisplay.format(this.netAmount / 100, 'USD');
|
||||
}
|
||||
|
||||
/** UI-specific: Tier display */
|
||||
|
||||
@@ -1,38 +1,14 @@
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { CurrencyDisplay } from '../display-objects/CurrencyDisplay';
|
||||
import { DateDisplay } from '../display-objects/DateDisplay';
|
||||
import { NumberDisplay } from '../display-objects/NumberDisplay';
|
||||
|
||||
/**
|
||||
* Interface for sponsorship data input
|
||||
*/
|
||||
export interface SponsorshipDataInput {
|
||||
id: string;
|
||||
type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';
|
||||
entityId: string;
|
||||
entityName: string;
|
||||
tier?: 'main' | 'secondary';
|
||||
status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';
|
||||
applicationDate?: string | Date;
|
||||
approvalDate?: string | Date;
|
||||
rejectionReason?: string;
|
||||
startDate: string | Date;
|
||||
endDate: string | Date;
|
||||
price: number;
|
||||
impressions: number;
|
||||
impressionsChange?: number;
|
||||
engagement?: number;
|
||||
details?: string;
|
||||
entityOwner?: string;
|
||||
applicationMessage?: string;
|
||||
}
|
||||
import type { SponsorshipViewData } from "../view-data/SponsorshipViewData";
|
||||
|
||||
/**
|
||||
* Sponsorship View Model
|
||||
*
|
||||
* View model for individual sponsorship data.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class SponsorshipViewModel extends ViewModel {
|
||||
id: string;
|
||||
type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';
|
||||
@@ -53,7 +29,8 @@ export class SponsorshipViewModel extends ViewModel {
|
||||
entityOwner?: string;
|
||||
applicationMessage?: string;
|
||||
|
||||
constructor(data: SponsorshipDataInput) {
|
||||
constructor(data: SponsorshipViewData) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.type = data.type;
|
||||
this.entityId = data.entityId;
|
||||
@@ -119,4 +96,4 @@ export class SponsorshipViewModel extends ViewModel {
|
||||
const end = DateDisplay.formatMonthYear(this.endDate);
|
||||
return `${start} - ${end}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,48 @@
|
||||
import { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { FinishDisplay } from "../display-objects/FinishDisplay";
|
||||
import type { StandingEntryViewData } from "../view-data/StandingEntryViewData";
|
||||
|
||||
export class StandingEntryViewModel extends ViewModel {
|
||||
driverId: string;
|
||||
position: number;
|
||||
points: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
races: number;
|
||||
private readonly data: StandingEntryViewData;
|
||||
|
||||
private leaderPoints: number;
|
||||
private nextPoints: number;
|
||||
private currentUserId: string;
|
||||
private previousPosition?: number;
|
||||
|
||||
constructor(dto: LeagueStandingDTO, leaderPoints: number, nextPoints: number, currentUserId: string, previousPosition?: number) {
|
||||
this.driverId = dto.driverId;
|
||||
this.position = dto.position;
|
||||
this.points = dto.points;
|
||||
this.wins = dto.wins ?? 0;
|
||||
this.podiums = dto.podiums ?? 0;
|
||||
this.races = dto.races ?? 0;
|
||||
this.leaderPoints = leaderPoints;
|
||||
this.nextPoints = nextPoints;
|
||||
this.currentUserId = currentUserId;
|
||||
this.previousPosition = previousPosition;
|
||||
constructor(data: StandingEntryViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get driverId(): string { return this.data.driverId; }
|
||||
get position(): number { return this.data.position; }
|
||||
get points(): number { return this.data.points; }
|
||||
get wins(): number { return this.data.wins; }
|
||||
get podiums(): number { return this.data.podiums; }
|
||||
get races(): number { return this.data.races; }
|
||||
get driver(): any { return this.data.driver; }
|
||||
|
||||
/** UI-specific: Badge for position display */
|
||||
get positionBadge(): string {
|
||||
return this.position.toString();
|
||||
return FinishDisplay.format(this.position);
|
||||
}
|
||||
|
||||
// Note: The generated DTO is incomplete
|
||||
// These fields will need to be added when the OpenAPI spec is updated
|
||||
driver?: any;
|
||||
|
||||
/** UI-specific: Points difference to leader */
|
||||
get pointsGapToLeader(): number {
|
||||
return this.points - this.leaderPoints;
|
||||
return this.points - this.data.leaderPoints;
|
||||
}
|
||||
|
||||
/** UI-specific: Points difference to next position */
|
||||
get pointsGapToNext(): number {
|
||||
return this.points - this.nextPoints;
|
||||
return this.points - this.data.nextPoints;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether this entry is the current user */
|
||||
get isCurrentUser(): boolean {
|
||||
return this.driverId === this.currentUserId;
|
||||
return this.driverId === this.data.currentUserId;
|
||||
}
|
||||
|
||||
/** UI-specific: Trend compared to previous */
|
||||
get trend(): 'up' | 'down' | 'same' {
|
||||
if (!this.previousPosition) return 'same';
|
||||
if (this.position < this.previousPosition) return 'up';
|
||||
if (this.position > this.previousPosition) return 'down';
|
||||
if (!this.data.previousPosition) return 'same';
|
||||
if (this.position < this.data.previousPosition) return 'up';
|
||||
if (this.position > this.data.previousPosition) return 'down';
|
||||
return 'same';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
|
||||
|
||||
interface TeamCardDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Team card view model
|
||||
* UI representation of a team on the landing page.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { TeamCardViewData } from "@/lib/view-data/TeamCardViewData";
|
||||
|
||||
export class TeamCardViewModel extends ViewModel {
|
||||
readonly id: string;
|
||||
@@ -21,11 +12,12 @@ export class TeamCardViewModel extends ViewModel {
|
||||
readonly description: string;
|
||||
readonly logoUrl?: string;
|
||||
|
||||
constructor(dto: TeamCardDTO | TeamListItemDTO) {
|
||||
this.id = dto.id;
|
||||
this.name = dto.name;
|
||||
this.tag = dto.tag;
|
||||
this.description = dto.description;
|
||||
this.logoUrl = 'logoUrl' in dto ? dto.logoUrl : undefined;
|
||||
constructor(data: TeamCardViewData) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.tag = data.tag;
|
||||
this.description = data.description;
|
||||
this.logoUrl = data.logoUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,28 @@
|
||||
import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { TeamDetailsViewData } from "../view-data/TeamDetailsViewData";
|
||||
|
||||
export class TeamDetailsViewModel extends ViewModel {
|
||||
id!: string;
|
||||
name!: string;
|
||||
tag!: string;
|
||||
description?: string;
|
||||
ownerId!: string;
|
||||
leagues!: string[];
|
||||
createdAt: string | undefined;
|
||||
specialization: string | undefined;
|
||||
region: string | undefined;
|
||||
languages: string[] | undefined;
|
||||
category: string | undefined;
|
||||
membership: { role: string; joinedAt: string; isActive: boolean } | null;
|
||||
private _canManage: boolean;
|
||||
private currentUserId: string;
|
||||
private readonly data: TeamDetailsViewData;
|
||||
|
||||
constructor(dto: GetTeamDetailsOutputDTO, currentUserId: string) {
|
||||
this.id = dto.team.id;
|
||||
this.name = dto.team.name;
|
||||
this.tag = dto.team.tag;
|
||||
this.description = dto.team.description;
|
||||
this.ownerId = dto.team.ownerId;
|
||||
this.leagues = dto.team.leagues;
|
||||
this.createdAt = dto.team.createdAt;
|
||||
|
||||
const teamExtras = dto.team as typeof dto.team & {
|
||||
specialization?: string;
|
||||
region?: string;
|
||||
languages?: string[];
|
||||
category?: string;
|
||||
};
|
||||
|
||||
this.specialization = teamExtras.specialization ?? undefined;
|
||||
this.region = teamExtras.region ?? undefined;
|
||||
this.languages = teamExtras.languages ?? undefined;
|
||||
this.category = teamExtras.category ?? undefined;
|
||||
this.membership = dto.membership ? {
|
||||
role: dto.membership.role,
|
||||
joinedAt: dto.membership.joinedAt,
|
||||
isActive: dto.membership.isActive
|
||||
} : null;
|
||||
this._canManage = dto.canManage;
|
||||
this.currentUserId = currentUserId;
|
||||
constructor(data: TeamDetailsViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.team.id; }
|
||||
get name(): string { return this.data.team.name; }
|
||||
get tag(): string { return this.data.team.tag; }
|
||||
get description(): string | undefined { return this.data.team.description; }
|
||||
get ownerId(): string { return this.data.team.ownerId; }
|
||||
get leagues(): string[] { return this.data.team.leagues; }
|
||||
get createdAt(): string | undefined { return this.data.team.createdAt; }
|
||||
get specialization(): string | undefined { return this.data.team.specialization; }
|
||||
get region(): string | undefined { return this.data.team.region; }
|
||||
get languages(): string[] | undefined { return this.data.team.languages; }
|
||||
get category(): string | undefined { return this.data.team.category; }
|
||||
get membership() { return this.data.membership; }
|
||||
get currentUserId(): string { return this.data.currentUserId; }
|
||||
|
||||
/** UI-specific: Whether current user is owner */
|
||||
get isOwner(): boolean {
|
||||
return this.membership?.role === 'owner';
|
||||
@@ -54,7 +30,7 @@ export class TeamDetailsViewModel extends ViewModel {
|
||||
|
||||
/** UI-specific: Whether can manage team */
|
||||
get canManage(): boolean {
|
||||
return this._canManage;
|
||||
return this.data.canManage;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether current user is member */
|
||||
@@ -66,4 +42,4 @@ export class TeamDetailsViewModel extends ViewModel {
|
||||
get userRole(): string {
|
||||
return this.membership?.role || 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
import type { TeamJoinRequestDTO } from '@/lib/types/generated/TeamJoinRequestDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { DateDisplay } from "../display-objects/DateDisplay";
|
||||
import type { TeamJoinRequestViewData } from "../view-data/TeamJoinRequestViewData";
|
||||
|
||||
export class TeamJoinRequestViewModel extends ViewModel {
|
||||
requestId: string;
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
teamId: string;
|
||||
requestStatus: string;
|
||||
requestedAt: string;
|
||||
avatarUrl: string;
|
||||
private readonly data: TeamJoinRequestViewData;
|
||||
|
||||
private readonly currentUserId: string;
|
||||
private readonly isOwner: boolean;
|
||||
|
||||
constructor(dto: TeamJoinRequestDTO, currentUserId: string, isOwner: boolean) {
|
||||
this.requestId = dto.requestId;
|
||||
this.driverId = dto.driverId;
|
||||
this.driverName = dto.driverName;
|
||||
this.teamId = dto.teamId;
|
||||
this.requestStatus = dto.status;
|
||||
this.requestedAt = dto.requestedAt;
|
||||
this.avatarUrl = dto.avatarUrl || '';
|
||||
this.currentUserId = currentUserId;
|
||||
this.isOwner = isOwner;
|
||||
constructor(data: TeamJoinRequestViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.requestId;
|
||||
}
|
||||
get id(): string { return this.data.requestId; }
|
||||
get requestId(): string { return this.data.requestId; }
|
||||
get driverId(): string { return this.data.driverId; }
|
||||
get driverName(): string { return this.data.driverName; }
|
||||
get teamId(): string { return this.data.teamId; }
|
||||
get requestStatus(): string { return this.data.status; }
|
||||
get requestedAt(): string { return this.data.requestedAt; }
|
||||
get avatarUrl(): string { return this.data.avatarUrl || ''; }
|
||||
get currentUserId(): string { return this.data.currentUserId; }
|
||||
get isOwner(): boolean { return this.data.isOwner; }
|
||||
|
||||
get status(): string {
|
||||
if (this.requestStatus === 'pending') return 'Pending';
|
||||
@@ -44,7 +35,7 @@ export class TeamJoinRequestViewModel extends ViewModel {
|
||||
|
||||
/** UI-specific: Formatted requested date */
|
||||
get formattedRequestedAt(): string {
|
||||
return new Date(this.requestedAt).toLocaleString();
|
||||
return DateDisplay.formatDateTime(this.requestedAt);
|
||||
}
|
||||
|
||||
/** UI-specific: Status color */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TeamMemberDTO } from '@/lib/types/generated/TeamMemberDTO';
|
||||
|
||||
type TeamMemberRole = 'owner' | 'manager' | 'member';
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { DateDisplay } from "../display-objects/DateDisplay";
|
||||
import type { TeamMemberViewData, TeamMemberRole } from "../view-data/TeamMemberViewData";
|
||||
|
||||
function normalizeTeamRole(role: string): TeamMemberRole {
|
||||
if (role === 'owner' || role === 'manager' || role === 'member') return role;
|
||||
@@ -9,30 +9,23 @@ function normalizeTeamRole(role: string): TeamMemberRole {
|
||||
return 'member';
|
||||
}
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class TeamMemberViewModel extends ViewModel {
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
role: TeamMemberRole;
|
||||
joinedAt: string;
|
||||
isActive: boolean;
|
||||
avatarUrl: string;
|
||||
private readonly data: TeamMemberViewData;
|
||||
|
||||
private currentUserId: string;
|
||||
private teamOwnerId: string;
|
||||
|
||||
constructor(dto: TeamMemberDTO, currentUserId: string, teamOwnerId: string) {
|
||||
this.driverId = dto.driverId;
|
||||
this.driverName = dto.driverName;
|
||||
this.role = normalizeTeamRole(dto.role);
|
||||
this.joinedAt = dto.joinedAt;
|
||||
this.isActive = dto.isActive;
|
||||
this.avatarUrl = dto.avatarUrl || '';
|
||||
this.currentUserId = currentUserId;
|
||||
this.teamOwnerId = teamOwnerId;
|
||||
constructor(data: TeamMemberViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get driverId(): string { return this.data.driverId; }
|
||||
get driverName(): string { return this.data.driverName; }
|
||||
get role(): TeamMemberRole { return normalizeTeamRole(this.data.role); }
|
||||
get joinedAt(): string { return this.data.joinedAt; }
|
||||
get isActive(): boolean { return this.data.isActive; }
|
||||
get avatarUrl(): string { return this.data.avatarUrl || ''; }
|
||||
get currentUserId(): string { return this.data.currentUserId; }
|
||||
get teamOwnerId(): string { return this.data.teamOwnerId; }
|
||||
|
||||
/** UI-specific: Role badge variant */
|
||||
get roleBadgeVariant(): string {
|
||||
switch (this.role) {
|
||||
@@ -60,6 +53,6 @@ export class TeamMemberViewModel extends ViewModel {
|
||||
|
||||
/** UI-specific: Formatted joined date */
|
||||
get formattedJoinedAt(): string {
|
||||
return new Date(this.joinedAt).toLocaleDateString();
|
||||
return DateDisplay.formatShort(this.joinedAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
|
||||
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import type { TeamSummaryViewData } from "../view-data/TeamSummaryViewData";
|
||||
|
||||
export class TeamSummaryViewModel extends ViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
memberCount: number;
|
||||
description?: string;
|
||||
totalWins: number = 0;
|
||||
totalRaces: number = 0;
|
||||
performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro' = 'intermediate';
|
||||
isRecruiting: boolean = false;
|
||||
specialization: 'endurance' | 'sprint' | 'mixed' | undefined;
|
||||
region: string | undefined;
|
||||
languages: string[] = [];
|
||||
leagues: string[] = [];
|
||||
logoUrl: string | undefined;
|
||||
rating: number | undefined;
|
||||
category: string | undefined;
|
||||
private readonly data: TeamSummaryViewData;
|
||||
private readonly maxMembers = 10; // Assuming max members
|
||||
|
||||
private maxMembers = 10; // Assuming max members
|
||||
|
||||
constructor(dto: TeamListItemDTO) {
|
||||
this.id = dto.id;
|
||||
this.name = dto.name;
|
||||
this.tag = dto.tag;
|
||||
this.memberCount = dto.memberCount;
|
||||
this.description = dto.description;
|
||||
this.specialization = dto.specialization as 'endurance' | 'sprint' | 'mixed' | undefined;
|
||||
this.region = dto.region;
|
||||
this.languages = dto.languages ?? [];
|
||||
this.leagues = dto.leagues;
|
||||
|
||||
// Map stats fields from DTO
|
||||
this.totalWins = dto.totalWins ?? 0;
|
||||
this.totalRaces = dto.totalRaces ?? 0;
|
||||
this.performanceLevel = (dto.performanceLevel as 'beginner' | 'intermediate' | 'advanced' | 'pro') ?? 'intermediate';
|
||||
this.logoUrl = dto.logoUrl;
|
||||
this.rating = dto.rating;
|
||||
this.category = dto.category;
|
||||
this.isRecruiting = dto.isRecruiting ?? false;
|
||||
constructor(data: TeamSummaryViewData) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get id(): string { return this.data.id; }
|
||||
get name(): string { return this.data.name; }
|
||||
get tag(): string { return this.data.tag; }
|
||||
get memberCount(): number { return this.data.memberCount; }
|
||||
get description(): string | undefined { return this.data.description; }
|
||||
get totalWins(): number { return this.data.totalWins; }
|
||||
get totalRaces(): number { return this.data.totalRaces; }
|
||||
get performanceLevel(): string { return this.data.performanceLevel; }
|
||||
get isRecruiting(): boolean { return this.data.isRecruiting; }
|
||||
get specialization(): string | undefined { return this.data.specialization; }
|
||||
get region(): string | undefined { return this.data.region; }
|
||||
get languages(): string[] { return this.data.languages; }
|
||||
get leagues(): string[] { return this.data.leagues; }
|
||||
get logoUrl(): string | undefined { return this.data.logoUrl; }
|
||||
get rating(): number | undefined { return this.data.rating; }
|
||||
get category(): string | undefined { return this.data.category; }
|
||||
|
||||
/** UI-specific: Whether team is full */
|
||||
get isFull(): boolean {
|
||||
return this.memberCount >= this.maxMembers;
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
interface UpcomingRaceCardDTO {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
}
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
import { DateDisplay } from "../display-objects/DateDisplay";
|
||||
import type { UpcomingRaceCardViewData } from "../view-data/UpcomingRaceCardViewData";
|
||||
|
||||
/**
|
||||
* Upcoming race card view model
|
||||
* UI representation of an upcoming race on the landing page.
|
||||
*/
|
||||
import { ViewModel } from "../contracts/view-models/ViewModel";
|
||||
|
||||
export class UpcomingRaceCardViewModel extends ViewModel {
|
||||
readonly id: string;
|
||||
readonly track: string;
|
||||
readonly car: string;
|
||||
readonly scheduledAt: string;
|
||||
|
||||
constructor(dto: UpcomingRaceCardDTO) {
|
||||
this.id = dto.id;
|
||||
this.track = dto.track;
|
||||
this.car = dto.car;
|
||||
this.scheduledAt = dto.scheduledAt;
|
||||
constructor(data: UpcomingRaceCardViewData) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.track = data.track;
|
||||
this.car = data.car;
|
||||
this.scheduledAt = data.scheduledAt;
|
||||
}
|
||||
|
||||
/** UI-specific: formatted date label */
|
||||
get formattedDate(): string {
|
||||
return new Date(this.scheduledAt).toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
return DateDisplay.formatMonthDay(this.scheduledAt);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user