fix data flow issues
This commit is contained in:
35
apps/website/lib/view-models/ActivityItemViewModel.ts
Normal file
35
apps/website/lib/view-models/ActivityItemViewModel.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Activity Item View Model
|
||||
*
|
||||
* View model for recent activity items.
|
||||
*/
|
||||
export class ActivityItemViewModel {
|
||||
id: string;
|
||||
type: 'race' | 'league' | 'team' | 'driver' | 'platform';
|
||||
message: string;
|
||||
time: string;
|
||||
impressions?: number;
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.type = data.type;
|
||||
this.message = data.message;
|
||||
this.time = data.time;
|
||||
this.impressions = data.impressions;
|
||||
}
|
||||
|
||||
get typeColor(): string {
|
||||
const colors = {
|
||||
race: 'bg-warning-amber',
|
||||
league: 'bg-primary-blue',
|
||||
team: 'bg-purple-400',
|
||||
driver: 'bg-performance-green',
|
||||
platform: 'bg-racing-red',
|
||||
};
|
||||
return colors[this.type] || 'bg-gray-500';
|
||||
}
|
||||
|
||||
get formattedImpressions(): string | null {
|
||||
return this.impressions ? this.impressions.toLocaleString() : null;
|
||||
}
|
||||
}
|
||||
76
apps/website/lib/view-models/AvailableLeaguesViewModel.ts
Normal file
76
apps/website/lib/view-models/AvailableLeaguesViewModel.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Available Leagues View Model
|
||||
*
|
||||
* View model for leagues available for sponsorship.
|
||||
*/
|
||||
export class AvailableLeaguesViewModel {
|
||||
leagues: AvailableLeagueViewModel[];
|
||||
|
||||
constructor(leagues: any[]) {
|
||||
this.leagues = leagues.map(league => new AvailableLeagueViewModel(league));
|
||||
}
|
||||
}
|
||||
|
||||
export class AvailableLeagueViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
game: string;
|
||||
drivers: number;
|
||||
avgViewsPerRace: number;
|
||||
mainSponsorSlot: { available: boolean; price: number };
|
||||
secondarySlots: { available: number; total: number; price: number };
|
||||
rating: number;
|
||||
tier: 'premium' | 'standard' | 'starter';
|
||||
nextRace?: string;
|
||||
seasonStatus: 'active' | 'upcoming' | 'completed';
|
||||
description: string;
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.game = data.game;
|
||||
this.drivers = data.drivers;
|
||||
this.avgViewsPerRace = data.avgViewsPerRace;
|
||||
this.mainSponsorSlot = data.mainSponsorSlot;
|
||||
this.secondarySlots = data.secondarySlots;
|
||||
this.rating = data.rating;
|
||||
this.tier = data.tier;
|
||||
this.nextRace = data.nextRace;
|
||||
this.seasonStatus = data.seasonStatus;
|
||||
this.description = data.description;
|
||||
}
|
||||
|
||||
get formattedAvgViews(): string {
|
||||
return `${(this.avgViewsPerRace / 1000).toFixed(1)}k`;
|
||||
}
|
||||
|
||||
get cpm(): number {
|
||||
return Math.round((this.mainSponsorSlot.price / this.avgViewsPerRace) * 1000);
|
||||
}
|
||||
|
||||
get formattedCpm(): string {
|
||||
return `$${this.cpm}`;
|
||||
}
|
||||
|
||||
get hasAvailableSlots(): boolean {
|
||||
return this.mainSponsorSlot.available || this.secondarySlots.available > 0;
|
||||
}
|
||||
|
||||
get tierConfig() {
|
||||
const configs = {
|
||||
premium: { color: 'text-yellow-400', bgColor: 'bg-yellow-500/10', border: 'border-yellow-500/30', icon: '⭐' },
|
||||
standard: { color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', border: 'border-primary-blue/30', icon: '🏆' },
|
||||
starter: { color: 'text-gray-400', bgColor: 'bg-gray-500/10', border: 'border-gray-500/30', icon: '🚀' },
|
||||
};
|
||||
return configs[this.tier];
|
||||
}
|
||||
|
||||
get statusConfig() {
|
||||
const configs = {
|
||||
active: { color: 'text-performance-green', bg: 'bg-performance-green/10', label: 'Active Season' },
|
||||
upcoming: { color: 'text-warning-amber', bg: 'bg-warning-amber/10', label: 'Starting Soon' },
|
||||
completed: { color: 'text-gray-400', bg: 'bg-gray-400/10', label: 'Season Ended' },
|
||||
};
|
||||
return configs[this.seasonStatus];
|
||||
}
|
||||
}
|
||||
138
apps/website/lib/view-models/BillingViewModel.ts
Normal file
138
apps/website/lib/view-models/BillingViewModel.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Billing View Model
|
||||
*
|
||||
* View model for sponsor billing data with UI-specific transformations.
|
||||
*/
|
||||
export class BillingViewModel {
|
||||
paymentMethods: PaymentMethodViewModel[];
|
||||
invoices: InvoiceViewModel[];
|
||||
stats: BillingStatsViewModel;
|
||||
|
||||
constructor(data: {
|
||||
paymentMethods: any[];
|
||||
invoices: any[];
|
||||
stats: any;
|
||||
}) {
|
||||
this.paymentMethods = data.paymentMethods.map(pm => new PaymentMethodViewModel(pm));
|
||||
this.invoices = data.invoices.map(inv => new InvoiceViewModel(inv));
|
||||
this.stats = new BillingStatsViewModel(data.stats);
|
||||
}
|
||||
}
|
||||
|
||||
export class PaymentMethodViewModel {
|
||||
id: string;
|
||||
type: 'card' | 'bank' | 'sepa';
|
||||
last4: string;
|
||||
brand?: string;
|
||||
isDefault: boolean;
|
||||
expiryMonth?: number;
|
||||
expiryYear?: number;
|
||||
bankName?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.type = data.type;
|
||||
this.last4 = data.last4;
|
||||
this.brand = data.brand;
|
||||
this.isDefault = data.isDefault;
|
||||
this.expiryMonth = data.expiryMonth;
|
||||
this.expiryYear = data.expiryYear;
|
||||
this.bankName = data.bankName;
|
||||
}
|
||||
|
||||
get displayLabel(): string {
|
||||
if (this.type === 'sepa' && this.bankName) {
|
||||
return `${this.bankName} •••• ${this.last4}`;
|
||||
}
|
||||
return `${this.brand} •••• ${this.last4}`;
|
||||
}
|
||||
|
||||
get expiryDisplay(): string | null {
|
||||
if (this.expiryMonth && this.expiryYear) {
|
||||
return `${String(this.expiryMonth).padStart(2, '0')}/${this.expiryYear}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class InvoiceViewModel {
|
||||
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;
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.invoiceNumber = data.invoiceNumber;
|
||||
this.date = new Date(data.date);
|
||||
this.dueDate = new Date(data.dueDate);
|
||||
this.amount = data.amount;
|
||||
this.vatAmount = data.vatAmount;
|
||||
this.totalAmount = data.totalAmount;
|
||||
this.status = data.status;
|
||||
this.description = data.description;
|
||||
this.sponsorshipType = data.sponsorshipType;
|
||||
this.pdfUrl = data.pdfUrl;
|
||||
}
|
||||
|
||||
get formattedTotalAmount(): string {
|
||||
return `€${this.totalAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
get formattedVatAmount(): string {
|
||||
return `€${this.vatAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
get formattedDate(): string {
|
||||
return this.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
get isOverdue(): boolean {
|
||||
return this.status === 'overdue' || (this.status === 'pending' && new Date() > this.dueDate);
|
||||
}
|
||||
}
|
||||
|
||||
export class BillingStatsViewModel {
|
||||
totalSpent: number;
|
||||
pendingAmount: number;
|
||||
nextPaymentDate: Date;
|
||||
nextPaymentAmount: number;
|
||||
activeSponsorships: number;
|
||||
averageMonthlySpend: number;
|
||||
|
||||
constructor(data: any) {
|
||||
this.totalSpent = data.totalSpent;
|
||||
this.pendingAmount = data.pendingAmount;
|
||||
this.nextPaymentDate = new Date(data.nextPaymentDate);
|
||||
this.nextPaymentAmount = data.nextPaymentAmount;
|
||||
this.activeSponsorships = data.activeSponsorships;
|
||||
this.averageMonthlySpend = data.averageMonthlySpend;
|
||||
}
|
||||
|
||||
get formattedTotalSpent(): string {
|
||||
return `€${this.totalSpent.toLocaleString('de-DE')}`;
|
||||
}
|
||||
|
||||
get formattedPendingAmount(): string {
|
||||
return `€${this.pendingAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
get formattedNextPaymentAmount(): string {
|
||||
return `€${this.nextPaymentAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
get formattedAverageMonthlySpend(): string {
|
||||
return `€${this.averageMonthlySpend.toLocaleString('de-DE')}`;
|
||||
}
|
||||
|
||||
get formattedNextPaymentDate(): string {
|
||||
return this.nextPaymentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
}
|
||||
25
apps/website/lib/view-models/HomeDiscoveryViewModel.ts
Normal file
25
apps/website/lib/view-models/HomeDiscoveryViewModel.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { LeagueCardViewModel } from './LeagueCardViewModel';
|
||||
import { TeamCardViewModel } from './TeamCardViewModel';
|
||||
import { UpcomingRaceCardViewModel } from './UpcomingRaceCardViewModel';
|
||||
|
||||
interface HomeDiscoveryDTO {
|
||||
topLeagues: LeagueCardViewModel[];
|
||||
teams: TeamCardViewModel[];
|
||||
upcomingRaces: UpcomingRaceCardViewModel[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Home discovery view model
|
||||
* Aggregates discovery data for the landing page.
|
||||
*/
|
||||
export class HomeDiscoveryViewModel {
|
||||
readonly topLeagues: LeagueCardViewModel[];
|
||||
readonly teams: TeamCardViewModel[];
|
||||
readonly upcomingRaces: UpcomingRaceCardViewModel[];
|
||||
|
||||
constructor(dto: HomeDiscoveryDTO) {
|
||||
this.topLeagues = dto.topLeagues;
|
||||
this.teams = dto.teams;
|
||||
this.upcomingRaces = dto.upcomingRaces;
|
||||
}
|
||||
}
|
||||
23
apps/website/lib/view-models/LeagueCardViewModel.ts
Normal file
23
apps/website/lib/view-models/LeagueCardViewModel.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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.
|
||||
*/
|
||||
export class LeagueCardViewModel {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
|
||||
constructor(dto: LeagueCardDTO | LeagueSummaryDTO & { description?: string }) {
|
||||
this.id = dto.id;
|
||||
this.name = dto.name;
|
||||
this.description = dto.description ?? 'Competitive iRacing league';
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,144 @@
|
||||
export interface MainSponsorInfo {
|
||||
name: string;
|
||||
logoUrl: string;
|
||||
websiteUrl: string;
|
||||
/**
|
||||
* League Detail View Model
|
||||
*
|
||||
* View model for detailed league information for sponsors.
|
||||
*/
|
||||
export class LeagueDetailViewModel {
|
||||
league: LeagueViewModel;
|
||||
drivers: DriverViewModel[];
|
||||
races: RaceViewModel[];
|
||||
|
||||
constructor(data: { league: any; drivers: any[]; races: any[] }) {
|
||||
this.league = new LeagueViewModel(data.league);
|
||||
this.drivers = data.drivers.map(driver => new DriverViewModel(driver));
|
||||
this.races = data.races.map(race => new RaceViewModel(race));
|
||||
}
|
||||
}
|
||||
|
||||
export class LeagueDetailViewModel {
|
||||
export class LeagueViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
game: string;
|
||||
tier: 'premium' | 'standard' | 'starter';
|
||||
season: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
ownerName: string;
|
||||
mainSponsor: MainSponsorInfo | null;
|
||||
isAdmin: boolean;
|
||||
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(
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
ownerId: string,
|
||||
ownerName: string,
|
||||
mainSponsor: MainSponsorInfo | null,
|
||||
isAdmin: boolean
|
||||
) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.ownerId = ownerId;
|
||||
this.ownerName = ownerName;
|
||||
this.mainSponsor = mainSponsor;
|
||||
this.isAdmin = isAdmin;
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.game = data.game;
|
||||
this.tier = data.tier;
|
||||
this.season = data.season;
|
||||
this.description = data.description;
|
||||
this.drivers = data.drivers;
|
||||
this.races = data.races;
|
||||
this.completedRaces = data.completedRaces;
|
||||
this.totalImpressions = data.totalImpressions;
|
||||
this.avgViewsPerRace = data.avgViewsPerRace;
|
||||
this.engagement = data.engagement;
|
||||
this.rating = data.rating;
|
||||
this.seasonStatus = data.seasonStatus;
|
||||
this.seasonDates = data.seasonDates;
|
||||
this.nextRace = data.nextRace;
|
||||
this.sponsorSlots = data.sponsorSlots;
|
||||
}
|
||||
|
||||
// UI-specific getters can be added here if needed
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
export class DriverViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
position: number;
|
||||
races: number;
|
||||
impressions: number;
|
||||
team: string;
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.country = data.country;
|
||||
this.position = data.position;
|
||||
this.races = data.races;
|
||||
this.impressions = data.impressions;
|
||||
this.team = data.team;
|
||||
}
|
||||
|
||||
get formattedImpressions(): string {
|
||||
return this.impressions.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
export class RaceViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
date: Date;
|
||||
views: number;
|
||||
status: 'upcoming' | 'completed';
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.date = new Date(data.date);
|
||||
this.views = data.views;
|
||||
this.status = data.status;
|
||||
}
|
||||
|
||||
get formattedDate(): string {
|
||||
return this.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
get formattedViews(): string {
|
||||
return this.views.toLocaleString();
|
||||
}
|
||||
}
|
||||
49
apps/website/lib/view-models/RenewalAlertViewModel.ts
Normal file
49
apps/website/lib/view-models/RenewalAlertViewModel.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Renewal Alert View Model
|
||||
*
|
||||
* View model for upcoming renewal alerts.
|
||||
*/
|
||||
export class RenewalAlertViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'league' | 'team' | 'driver' | 'race' | 'platform';
|
||||
renewDate: Date;
|
||||
price: number;
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.type = data.type;
|
||||
this.renewDate = new Date(data.renewDate);
|
||||
this.price = data.price;
|
||||
}
|
||||
|
||||
get formattedPrice(): string {
|
||||
return `$${this.price}`;
|
||||
}
|
||||
|
||||
get formattedRenewDate(): string {
|
||||
return this.renewDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
get typeIcon() {
|
||||
const icons = {
|
||||
league: 'Trophy',
|
||||
team: 'Users',
|
||||
driver: 'Car',
|
||||
race: 'Flag',
|
||||
platform: 'Megaphone',
|
||||
};
|
||||
return icons[this.type] || 'Trophy';
|
||||
}
|
||||
|
||||
get daysUntilRenewal(): number {
|
||||
const now = new Date();
|
||||
const diffTime = this.renewDate.getTime() - now.getTime();
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
get isUrgent(): boolean {
|
||||
return this.daysUntilRenewal <= 30;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { SponsorDashboardDTO } from '../types/generated/SponsorDashboardDTO';
|
||||
import { SponsorshipViewModel } from './SponsorshipViewModel';
|
||||
import { ActivityItemViewModel } from './ActivityItemViewModel';
|
||||
import { RenewalAlertViewModel } from './RenewalAlertViewModel';
|
||||
|
||||
/**
|
||||
* Sponsor Dashboard View Model
|
||||
@@ -8,17 +11,72 @@ import type { SponsorDashboardDTO } from '../types/generated/SponsorDashboardDTO
|
||||
export class SponsorDashboardViewModel {
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
metrics: any;
|
||||
sponsorships: {
|
||||
leagues: SponsorshipViewModel[];
|
||||
teams: SponsorshipViewModel[];
|
||||
drivers: SponsorshipViewModel[];
|
||||
races: SponsorshipViewModel[];
|
||||
platform: SponsorshipViewModel[];
|
||||
};
|
||||
recentActivity: ActivityItemViewModel[];
|
||||
upcomingRenewals: RenewalAlertViewModel[];
|
||||
|
||||
constructor(dto: SponsorDashboardDTO) {
|
||||
this.sponsorId = dto.sponsorId;
|
||||
this.sponsorName = dto.sponsorName;
|
||||
this.metrics = dto.metrics;
|
||||
this.sponsorships = {
|
||||
leagues: (dto.sponsorships?.leagues || []).map(s => new SponsorshipViewModel(s)),
|
||||
teams: (dto.sponsorships?.teams || []).map(s => new SponsorshipViewModel(s)),
|
||||
drivers: (dto.sponsorships?.drivers || []).map(s => new SponsorshipViewModel(s)),
|
||||
races: (dto.sponsorships?.races || []).map(s => new SponsorshipViewModel(s)),
|
||||
platform: (dto.sponsorships?.platform || []).map(s => new SponsorshipViewModel(s)),
|
||||
};
|
||||
this.recentActivity = (dto.recentActivity || []).map(a => new ActivityItemViewModel(a));
|
||||
this.upcomingRenewals = (dto.upcomingRenewals || []).map(r => new RenewalAlertViewModel(r));
|
||||
}
|
||||
|
||||
// Note: The generated DTO doesn't include these fields yet
|
||||
// These will need to be added when the OpenAPI spec is updated
|
||||
totalSponsorships: number = 0;
|
||||
activeSponsorships: number = 0;
|
||||
totalInvestment: number = 0;
|
||||
get totalSponsorships(): number {
|
||||
return this.sponsorships.leagues.length +
|
||||
this.sponsorships.teams.length +
|
||||
this.sponsorships.drivers.length +
|
||||
this.sponsorships.races.length +
|
||||
this.sponsorships.platform.length;
|
||||
}
|
||||
|
||||
get activeSponsorships(): number {
|
||||
const all = [
|
||||
...this.sponsorships.leagues,
|
||||
...this.sponsorships.teams,
|
||||
...this.sponsorships.drivers,
|
||||
...this.sponsorships.races,
|
||||
...this.sponsorships.platform,
|
||||
];
|
||||
return all.filter(s => s.status === 'active').length;
|
||||
}
|
||||
|
||||
get totalInvestment(): number {
|
||||
const all = [
|
||||
...this.sponsorships.leagues,
|
||||
...this.sponsorships.teams,
|
||||
...this.sponsorships.drivers,
|
||||
...this.sponsorships.races,
|
||||
...this.sponsorships.platform,
|
||||
];
|
||||
return all.filter(s => s.status === 'active').reduce((sum, s) => sum + s.price, 0);
|
||||
}
|
||||
|
||||
get totalImpressions(): number {
|
||||
const all = [
|
||||
...this.sponsorships.leagues,
|
||||
...this.sponsorships.teams,
|
||||
...this.sponsorships.drivers,
|
||||
...this.sponsorships.races,
|
||||
...this.sponsorships.platform,
|
||||
];
|
||||
return all.reduce((sum, s) => sum + s.impressions, 0);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted total investment */
|
||||
get formattedTotalInvestment(): string {
|
||||
@@ -42,4 +100,36 @@ export class SponsorDashboardViewModel {
|
||||
if (this.activeSponsorships === this.totalSponsorships) return 'All sponsorships active';
|
||||
return `${this.activeSponsorships} of ${this.totalSponsorships} active`;
|
||||
}
|
||||
|
||||
/** UI-specific: Cost per 1K views */
|
||||
get costPerThousandViews(): string {
|
||||
if (this.totalImpressions === 0) return '$0.00';
|
||||
return `$${(this.totalInvestment / this.totalImpressions * 1000).toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Category data for charts */
|
||||
get categoryData() {
|
||||
return {
|
||||
leagues: {
|
||||
count: this.sponsorships.leagues.length,
|
||||
impressions: this.sponsorships.leagues.reduce((sum, l) => sum + l.impressions, 0),
|
||||
},
|
||||
teams: {
|
||||
count: this.sponsorships.teams.length,
|
||||
impressions: this.sponsorships.teams.reduce((sum, t) => sum + t.impressions, 0),
|
||||
},
|
||||
drivers: {
|
||||
count: this.sponsorships.drivers.length,
|
||||
impressions: this.sponsorships.drivers.reduce((sum, d) => sum + d.impressions, 0),
|
||||
},
|
||||
races: {
|
||||
count: this.sponsorships.races.length,
|
||||
impressions: this.sponsorships.races.reduce((sum, r) => sum + r.impressions, 0),
|
||||
},
|
||||
platform: {
|
||||
count: this.sponsorships.platform.length,
|
||||
impressions: this.sponsorships.platform.reduce((sum, p) => sum + p.impressions, 0),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
89
apps/website/lib/view-models/SponsorSettingsViewModel.ts
Normal file
89
apps/website/lib/view-models/SponsorSettingsViewModel.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Sponsor Settings View Model
|
||||
*
|
||||
* View model for sponsor settings data.
|
||||
*/
|
||||
export class SponsorSettingsViewModel {
|
||||
profile: SponsorProfileViewModel;
|
||||
notifications: NotificationSettingsViewModel;
|
||||
privacy: PrivacySettingsViewModel;
|
||||
|
||||
constructor(data: { profile: any; notifications: any; privacy: any }) {
|
||||
this.profile = new SponsorProfileViewModel(data.profile);
|
||||
this.notifications = new NotificationSettingsViewModel(data.notifications);
|
||||
this.privacy = new PrivacySettingsViewModel(data.privacy);
|
||||
}
|
||||
}
|
||||
|
||||
export class SponsorProfileViewModel {
|
||||
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: any) {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationSettingsViewModel {
|
||||
emailNewSponsorships: boolean;
|
||||
emailWeeklyReport: boolean;
|
||||
emailRaceAlerts: boolean;
|
||||
emailPaymentAlerts: boolean;
|
||||
emailNewOpportunities: boolean;
|
||||
emailContractExpiry: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
this.emailNewSponsorships = data.emailNewSponsorships;
|
||||
this.emailWeeklyReport = data.emailWeeklyReport;
|
||||
this.emailRaceAlerts = data.emailRaceAlerts;
|
||||
this.emailPaymentAlerts = data.emailPaymentAlerts;
|
||||
this.emailNewOpportunities = data.emailNewOpportunities;
|
||||
this.emailContractExpiry = data.emailContractExpiry;
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivacySettingsViewModel {
|
||||
publicProfile: boolean;
|
||||
showStats: boolean;
|
||||
showActiveSponsorships: boolean;
|
||||
allowDirectContact: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
this.publicProfile = data.publicProfile;
|
||||
this.showStats = data.showStats;
|
||||
this.showActiveSponsorships = data.showActiveSponsorships;
|
||||
this.allowDirectContact = data.allowDirectContact;
|
||||
}
|
||||
}
|
||||
92
apps/website/lib/view-models/SponsorshipViewModel.ts
Normal file
92
apps/website/lib/view-models/SponsorshipViewModel.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Sponsorship View Model
|
||||
*
|
||||
* View model for individual sponsorship data.
|
||||
*/
|
||||
export class SponsorshipViewModel {
|
||||
id: string;
|
||||
type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';
|
||||
entityId: string;
|
||||
entityName: string;
|
||||
tier?: 'main' | 'secondary';
|
||||
status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';
|
||||
applicationDate?: Date;
|
||||
approvalDate?: Date;
|
||||
rejectionReason?: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
price: number;
|
||||
impressions: number;
|
||||
impressionsChange?: number;
|
||||
engagement?: number;
|
||||
details?: string;
|
||||
entityOwner?: string;
|
||||
applicationMessage?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.type = data.type;
|
||||
this.entityId = data.entityId;
|
||||
this.entityName = data.entityName;
|
||||
this.tier = data.tier;
|
||||
this.status = data.status;
|
||||
this.applicationDate = data.applicationDate ? new Date(data.applicationDate) : undefined;
|
||||
this.approvalDate = data.approvalDate ? new Date(data.approvalDate) : undefined;
|
||||
this.rejectionReason = data.rejectionReason;
|
||||
this.startDate = new Date(data.startDate);
|
||||
this.endDate = new Date(data.endDate);
|
||||
this.price = data.price;
|
||||
this.impressions = data.impressions;
|
||||
this.impressionsChange = data.impressionsChange;
|
||||
this.engagement = data.engagement;
|
||||
this.details = data.details;
|
||||
this.entityOwner = data.entityOwner;
|
||||
this.applicationMessage = data.applicationMessage;
|
||||
}
|
||||
|
||||
get formattedImpressions(): string {
|
||||
return this.impressions.toLocaleString();
|
||||
}
|
||||
|
||||
get formattedPrice(): string {
|
||||
return `$${this.price}`;
|
||||
}
|
||||
|
||||
get daysRemaining(): number {
|
||||
const now = new Date();
|
||||
const diffTime = this.endDate.getTime() - now.getTime();
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
get isExpiringSoon(): boolean {
|
||||
return this.daysRemaining > 0 && this.daysRemaining <= 30;
|
||||
}
|
||||
|
||||
get statusLabel(): string {
|
||||
const labels = {
|
||||
active: 'Active',
|
||||
pending_approval: 'Awaiting Approval',
|
||||
approved: 'Approved',
|
||||
rejected: 'Declined',
|
||||
expired: 'Expired',
|
||||
};
|
||||
return labels[this.status] || this.status;
|
||||
}
|
||||
|
||||
get typeLabel(): string {
|
||||
const labels = {
|
||||
leagues: 'League',
|
||||
teams: 'Team',
|
||||
drivers: 'Driver',
|
||||
races: 'Race',
|
||||
platform: 'Platform',
|
||||
};
|
||||
return labels[this.type] || this.type;
|
||||
}
|
||||
|
||||
get periodDisplay(): string {
|
||||
const start = this.startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
||||
const end = this.endDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
||||
return `${start} - ${end}`;
|
||||
}
|
||||
}
|
||||
26
apps/website/lib/view-models/TeamCardViewModel.ts
Normal file
26
apps/website/lib/view-models/TeamCardViewModel.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { TeamListItemDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO';
|
||||
|
||||
interface TeamCardDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Team card view model
|
||||
* UI representation of a team on the landing page.
|
||||
*/
|
||||
export class TeamCardViewModel {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly tag: string;
|
||||
readonly description: string;
|
||||
|
||||
constructor(dto: TeamCardDTO | TeamListItemDTO) {
|
||||
this.id = dto.id;
|
||||
this.name = dto.name;
|
||||
this.tag = dto.tag;
|
||||
this.description = dto.description;
|
||||
}
|
||||
}
|
||||
32
apps/website/lib/view-models/UpcomingRaceCardViewModel.ts
Normal file
32
apps/website/lib/view-models/UpcomingRaceCardViewModel.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
interface UpcomingRaceCardDTO {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upcoming race card view model
|
||||
* UI representation of an upcoming race on the landing page.
|
||||
*/
|
||||
export class UpcomingRaceCardViewModel {
|
||||
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;
|
||||
}
|
||||
|
||||
/** UI-specific: formatted date label */
|
||||
get formattedDate(): string {
|
||||
return new Date(this.scheduledAt).toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user