Files
gridpilot.gg/apps/website/lib/presenters/LeagueDetailPresenter.ts
2026-01-14 02:02:24 +01:00

212 lines
7.1 KiB
TypeScript

'use client';
/**
* LeagueDetailPresenter
* Pure client-side presenter for LeagueDetailTemplate
* Converts ViewModels to ViewData and removes DisplayObject usage
*/
import type { Presenter } from '@/lib/contracts/presenters/Presenter';
import type { DriverSummaryData, LeagueDetailViewData, LeagueInfoData, LiveRaceData, SponsorInfo, SponsorMetric } from '@/lib/view-data/LeagueDetailViewData';
import type { DriverSummary, LeagueDetailPageViewModel } from '@/lib/view-models/LeagueDetailPageViewModel';
import type { RaceViewModel } from '@/lib/view-models/RaceViewModel';
import { Eye, TrendingUp, Users, Zap } from 'lucide-react';
interface SponsorshipSlot {
tier: 'main' | 'secondary';
available: boolean;
price: number;
benefits: string[];
}
interface LeagueDetailInput {
viewModel: LeagueDetailPageViewModel;
leagueId: string;
isSponsor: boolean;
}
// League role display data (moved from LeagueRoleDisplay)
const leagueRoleDisplay = {
owner: {
text: 'Owner',
badgeClasses: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30',
},
admin: {
text: 'Admin',
badgeClasses: 'bg-purple-500/10 text-purple-400 border-purple-500/30',
},
steward: {
text: 'Steward',
badgeClasses: 'bg-blue-500/10 text-blue-400 border-blue-500/30',
},
member: {
text: 'Member',
badgeClasses: 'bg-primary-blue/10 text-primary-blue border-primary-blue/30',
},
} as const;
export class LeagueDetailPresenter implements Presenter<LeagueDetailInput, LeagueDetailViewData> {
/**
* Convert RaceViewModel[] to LiveRaceData[]
*/
private static convertRunningRaces(races: RaceViewModel[]): LiveRaceData[] {
return races.map(race => ({
id: race.id,
name: race.name,
date: race.date,
registeredCount: race.registeredCount,
strengthOfField: race.strengthOfField,
}));
}
/**
* Convert DriverSummary to DriverSummaryData with role badge info
*/
private static convertDriverSummary(
summary: DriverSummary | null,
role: 'owner' | 'admin' | 'steward' | 'member',
leagueId: string
): DriverSummaryData | null {
if (!summary) return null;
const roleDisplay = leagueRoleDisplay[role];
return {
driverId: summary.driver.id,
driverName: summary.driver.name,
avatarUrl: summary.driver.avatarUrl,
rating: summary.rating,
rank: summary.rank,
roleBadgeText: roleDisplay.text,
roleBadgeClasses: roleDisplay.badgeClasses,
profileUrl: `/drivers/${summary.driver.id}?from=league-management&leagueId=${leagueId}`,
};
}
/**
* Transform input to output
*/
present(input: LeagueDetailInput): LeagueDetailViewData {
const { viewModel, leagueId, isSponsor } = input;
// Build info data
const info: LeagueInfoData = {
name: viewModel.name,
description: viewModel.description ?? '',
membersCount: viewModel.memberships.length,
racesCount: viewModel.completedRacesCount,
avgSOF: viewModel.averageSOF,
structure: `Solo • ${viewModel.settings.maxDrivers ?? 32} max`,
scoring: viewModel.scoringConfig?.scoringPresetName ?? 'Standard',
createdAt: viewModel.createdAt,
discordUrl: viewModel.socialLinks?.discordUrl,
youtubeUrl: viewModel.socialLinks?.youtubeUrl,
websiteUrl: viewModel.socialLinks?.websiteUrl,
};
// Convert running races
const runningRaces = LeagueDetailPresenter.convertRunningRaces(viewModel.runningRaces);
// Convert sponsors
const sponsors: SponsorInfo[] = viewModel.sponsors.map(s => ({
id: s.id,
name: s.name,
tier: s.tier,
logoUrl: s.logoUrl,
websiteUrl: s.websiteUrl,
tagline: s.tagline,
}));
// Convert driver summaries with role badges
const ownerSummary = LeagueDetailPresenter.convertDriverSummary(viewModel.ownerSummary, 'owner', leagueId);
const adminSummaries = viewModel.adminSummaries
.map(s => LeagueDetailPresenter.convertDriverSummary(s, 'admin', leagueId))
.filter((s): s is DriverSummaryData => s !== null);
const stewardSummaries = viewModel.stewardSummaries
.map(s => LeagueDetailPresenter.convertDriverSummary(s, 'steward', leagueId))
.filter((s): s is DriverSummaryData => s !== null);
// Sponsor insights (only if sponsor mode)
const sponsorInsights = isSponsor ? {
avgViewsPerRace: viewModel.sponsorInsights.avgViewsPerRace,
engagementRate: viewModel.sponsorInsights.engagementRate,
estimatedReach: viewModel.sponsorInsights.estimatedReach,
tier: viewModel.sponsorInsights.tier,
trustScore: viewModel.sponsorInsights.trustScore,
discordMembers: viewModel.sponsorInsights.discordMembers,
monthlyActivity: viewModel.sponsorInsights.monthlyActivity,
mainSponsorAvailable: viewModel.sponsorInsights.mainSponsorAvailable,
secondarySlotsAvailable: viewModel.sponsorInsights.secondarySlotsAvailable,
mainSponsorPrice: viewModel.sponsorInsights.mainSponsorPrice,
secondaryPrice: viewModel.sponsorInsights.secondaryPrice,
totalImpressions: viewModel.sponsorInsights.totalImpressions,
metrics: [
{
icon: Eye,
label: 'Avg Views/Race',
value: viewModel.sponsorInsights.avgViewsPerRace,
color: 'text-primary-blue',
},
{
icon: TrendingUp,
label: 'Engagement',
value: viewModel.sponsorInsights.engagementRate,
color: 'text-performance-green',
},
{
icon: Users,
label: 'Est. Reach',
value: viewModel.sponsorInsights.estimatedReach,
color: 'text-purple-400',
},
{
icon: Zap,
label: 'Avg SOF',
value: viewModel.averageSOF ?? '—',
color: 'text-warning-amber',
},
],
slots: [
{
tier: 'main' as const,
available: viewModel.sponsorInsights.mainSponsorAvailable,
price: viewModel.sponsorInsights.mainSponsorPrice,
benefits: ['Hood placement', 'League banner', 'Prominent logo'],
},
{
tier: 'secondary' as const,
available: viewModel.sponsorInsights.secondarySlotsAvailable > 0,
price: viewModel.sponsorInsights.secondaryPrice,
benefits: ['Side logo placement', 'League page listing'],
},
{
tier: 'secondary' as const,
available: viewModel.sponsorInsights.secondarySlotsAvailable > 1,
price: viewModel.sponsorInsights.secondaryPrice,
benefits: ['Side logo placement', 'League page listing'],
},
],
} : null;
return {
leagueId: viewModel.id,
name: viewModel.name,
description: viewModel.description ?? '',
info,
runningRaces,
sponsors,
ownerSummary,
adminSummaries,
stewardSummaries,
sponsorInsights,
};
}
/**
* Static helper for backward compatibility
*/
static createViewData(viewModel: LeagueDetailPageViewModel, leagueId: string, isSponsor: boolean): LeagueDetailViewData {
const presenter = new LeagueDetailPresenter();
return presenter.present({ viewModel, leagueId, isSponsor });
}
}