212 lines
7.1 KiB
TypeScript
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 });
|
|
}
|
|
} |