Files
gridpilot.gg/apps/website/lib/view-models/LeagueDetailPageViewModel.ts
2026-01-12 01:01:49 +01:00

256 lines
8.1 KiB
TypeScript

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 { RaceViewModel } from './RaceViewModel';
import { DriverViewModel } from './DriverViewModel';
// 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 }>;
}
export class LeagueDetailPageViewModel {
// League basic info
id: string;
name: string;
description?: string;
ownerId: string;
createdAt: string;
settings: {
maxDrivers?: number;
};
socialLinks: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
} | undefined;
// 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;
// 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.owner = owner;
this.scoringConfig = scoringConfig;
this.drivers = drivers;
// 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
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 = {
avgViewsPerRace: 5400 + memberCount * 50,
totalImpressions: 45000 + memberCount * 500,
engagementRate: (3.5 + (memberCount / 50)).toFixed(1),
estimatedReach: memberCount * 150,
mainSponsorAvailable: !mainSponsorTaken,
secondarySlotsAvailable: Math.max(0, 2 - secondaryTaken),
mainSponsorPrice: 800 + Math.floor(memberCount * 10),
secondaryPrice: 250 + Math.floor(memberCount * 3),
tier: (this.averageSOF && this.averageSOF > 3000 ? 'premium' : this.averageSOF && this.averageSOF > 2000 ? 'standard' : 'starter') as 'premium' | 'standard' | 'starter',
trustScore: Math.min(100, 60 + memberCount + this.completedRacesCount),
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';
}
}