website refactor
This commit is contained in:
212
apps/website/lib/presenters/LeagueDetailPresenter.ts
Normal file
212
apps/website/lib/presenters/LeagueDetailPresenter.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
'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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user