Files
gridpilot.gg/apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder.ts
Marc Mintel d97f50ed72
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 6m4s
Contract Testing / contract-snapshot (pull_request) Has been skipped
view data fixes
2026-01-23 11:59:49 +01:00

208 lines
7.1 KiB
TypeScript

import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
import type { LeagueDetailViewData, LeagueInfoData, LiveRaceData, DriverSummaryData, SponsorInfo, NextRaceInfo, SeasonProgress, RecentResult } from '@/lib/view-data/LeagueDetailViewData';
/**
* LeagueDetailViewDataBuilder
*
* Transforms API DTOs into LeagueDetailViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LeagueDetailViewDataBuilder.build(input);
}
static build(
static build(input: {
league: LeagueWithCapacityAndScoringDTO;
owner: GetDriverOutputDTO | null;
scoringConfig: LeagueScoringConfigDTO | null;
memberships: LeagueMembershipsDTO;
races: RaceDTO[];
sponsors: any[];
}): LeagueDetailViewData {
const { league, owner, scoringConfig, memberships, races, sponsors } = input;
// Calculate running races - using available fields from RaceDTO
const runningRaces: LiveRaceData[] = races
.filter(r => r.name.includes('Running')) // Placeholder filter
.map(r => ({
id: r.id,
name: r.name,
date: r.date,
registeredCount: 0,
strengthOfField: 0,
}));
// Calculate info data
const membersCount = Array.isArray(memberships.members) ? memberships.members.length : 0;
// League overview wants total races, not just completed.
// (In seed/demo data many races are `status: running`, which should still count.)
const racesCount = races.length;
// Compute real avgSOF from races
const racesWithSOF = races.filter(r => {
const sof = (r as any).strengthOfField;
return typeof sof === 'number' && sof > 0;
});
const avgSOF = racesWithSOF.length > 0
? Math.round(racesWithSOF.reduce((sum, r) => sum + ((r as any).strengthOfField || 0), 0) / racesWithSOF.length)
: null;
if (process.env.NODE_ENV !== 'production') {
const race0 = races.length > 0 ? races[0] : null;
console.info(
'[LeagueDetailViewDataBuilder] leagueId=%s members=%d races=%d racesWithSOF=%d avgSOF=%s race0=%o',
league.id,
membersCount,
racesCount,
racesWithSOF.length,
String(avgSOF),
race0,
);
}
const info: LeagueInfoData = {
name: league.name,
description: league.description || '',
membersCount,
racesCount,
avgSOF,
structure: `Solo • ${league.settings?.maxDrivers ?? 32} max`,
scoring: scoringConfig?.scoringPresetId || 'Standard',
createdAt: league.createdAt,
discordUrl: league.socialLinks?.discordUrl,
youtubeUrl: league.socialLinks?.youtubeUrl,
websiteUrl: league.socialLinks?.websiteUrl,
};
// Convert owner to driver summary
const ownerSummary: DriverSummaryData | null = owner ? {
driverId: owner.id,
driverName: owner.name,
avatarUrl: owner.avatarUrl || null,
rating: null,
rank: null,
roleBadgeText: 'Owner',
roleBadgeClasses: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30',
profileUrl: `/drivers/${owner.id}`,
} : null;
// Convert sponsors
const sponsorInfo: SponsorInfo[] = sponsors.map(s => ({
id: s.id,
name: s.name,
tier: s.tier,
logoUrl: s.logoUrl,
websiteUrl: s.websiteUrl,
tagline: s.tagline,
}));
// Convert memberships to summaries
const adminSummaries: DriverSummaryData[] = (memberships.members || [])
.filter(m => m.role === 'admin')
.map(m => ({
driverId: m.driverId,
driverName: m.driver.name,
avatarUrl: (m.driver as any).avatarUrl || null,
rating: null,
rank: null,
roleBadgeText: 'Admin',
roleBadgeClasses: 'bg-blue-500/10 text-blue-500 border-blue-500/30',
profileUrl: `/drivers/${m.driverId}`,
}));
const stewardSummaries: DriverSummaryData[] = (memberships.members || [])
.filter(m => m.role === 'steward')
.map(m => ({
driverId: m.driverId,
driverName: m.driver.name,
avatarUrl: (m.driver as any).avatarUrl || null,
rating: null,
rank: null,
roleBadgeText: 'Steward',
roleBadgeClasses: 'bg-purple-500/10 text-purple-500 border-purple-500/30',
profileUrl: `/drivers/${m.driverId}`,
}));
const memberSummaries: DriverSummaryData[] = (memberships.members || [])
.filter(m => m.role === 'member')
.map(m => ({
driverId: m.driverId,
driverName: m.driver.name,
avatarUrl: (m.driver as any).avatarUrl || null,
rating: null,
rank: null,
roleBadgeText: 'Member',
roleBadgeClasses: 'bg-zinc-500/10 text-zinc-500 border-zinc-500/30',
profileUrl: `/drivers/${m.driverId}`,
}));
// Calculate next race (first upcoming race)
const now = new Date();
const nextRace: NextRaceInfo | undefined = races
.filter(r => new Date(r.date) > now)
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
.map(r => ({
id: r.id,
name: r.name,
date: r.date,
track: (r as any).track,
car: (r as any).car,
}))[0];
// Calculate season progress (completed races vs total races)
const completedRaces = races.filter(r => {
const raceDate = new Date(r.date);
return raceDate < now;
}).length;
const totalRaces = races.length;
const percentage = totalRaces > 0 ? Math.round((completedRaces / totalRaces) * 100) : 0;
const seasonProgress: SeasonProgress = {
completedRaces,
totalRaces,
percentage,
};
// Get recent results (top 3 from last completed race)
const recentResults: RecentResult[] = races
.filter(r => new Date(r.date) < now)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.slice(0, 3)
.map(r => ({
raceId: r.id,
raceName: r.name,
position: (r as any).position || 0,
points: (r as any).points || 0,
finishedAt: r.date,
}));
return {
leagueId: league.id,
name: league.name,
description: league.description || '',
logoUrl: league.logoUrl,
info,
runningRaces,
sponsors: sponsorInfo,
ownerSummary,
adminSummaries,
stewardSummaries,
memberSummaries,
sponsorInsights: null, // Only for sponsor mode
nextRace,
seasonProgress,
recentResults,
walletBalance: league.walletBalance,
pendingProtestsCount: league.pendingProtestsCount,
pendingJoinRequestsCount: league.pendingJoinRequestsCount,
};
}
}