website refactor
This commit is contained in:
@@ -1,212 +0,0 @@
|
||||
'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 });
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* LeagueStandingsPresenter
|
||||
* Pure client-side presenter for LeagueStandingsTemplate
|
||||
* Converts ViewModels to ViewData
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@/lib/contracts/presenters/Presenter';
|
||||
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
import type { DriverData, LeagueMembershipData, LeagueStandingsViewData, StandingEntryData } from '@/lib/view-data/LeagueStandingsViewData';
|
||||
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import type { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel';
|
||||
|
||||
interface LeagueStandingsInput {
|
||||
standings: StandingEntryViewModel[];
|
||||
drivers: DriverViewModel[];
|
||||
memberships: LeagueMembership[];
|
||||
leagueId: string;
|
||||
currentDriverId: string | null;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export class LeagueStandingsPresenter implements Presenter<LeagueStandingsInput, LeagueStandingsViewData> {
|
||||
/**
|
||||
* Convert StandingEntryViewModel to StandingEntryData
|
||||
*/
|
||||
private static convertStanding(standing: StandingEntryViewModel): StandingEntryData {
|
||||
return {
|
||||
driverId: standing.driverId,
|
||||
position: standing.position,
|
||||
totalPoints: standing.points,
|
||||
racesFinished: standing.races,
|
||||
racesStarted: standing.races,
|
||||
avgFinish: null, // Not available in current ViewModel
|
||||
penaltyPoints: 0, // Not available in current ViewModel
|
||||
bonusPoints: 0, // Not available in current ViewModel
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DriverViewModel to DriverData
|
||||
*/
|
||||
private static convertDriver(driver: DriverViewModel): DriverData {
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
avatarUrl: driver.avatarUrl,
|
||||
iracingId: driver.iracingId,
|
||||
rating: driver.rating,
|
||||
country: driver.country,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LeagueMembership to LeagueMembershipData
|
||||
*/
|
||||
private static convertMembership(membership: LeagueMembership): LeagueMembershipData {
|
||||
return {
|
||||
driverId: membership.driverId,
|
||||
leagueId: membership.leagueId,
|
||||
role: membership.role,
|
||||
joinedAt: membership.joinedAt,
|
||||
status: membership.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform input to output
|
||||
*/
|
||||
present(input: LeagueStandingsInput): LeagueStandingsViewData {
|
||||
return {
|
||||
standings: input.standings.map(s => LeagueStandingsPresenter.convertStanding(s)),
|
||||
drivers: input.drivers.map(d => LeagueStandingsPresenter.convertDriver(d)),
|
||||
memberships: input.memberships.map(m => LeagueStandingsPresenter.convertMembership(m)),
|
||||
leagueId: input.leagueId,
|
||||
currentDriverId: input.currentDriverId,
|
||||
isAdmin: input.isAdmin,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper for backward compatibility
|
||||
*/
|
||||
static createViewData(
|
||||
standings: StandingEntryViewModel[],
|
||||
drivers: DriverViewModel[],
|
||||
memberships: LeagueMembership[],
|
||||
leagueId: string,
|
||||
currentDriverId: string | null,
|
||||
isAdmin: boolean
|
||||
): LeagueStandingsViewData {
|
||||
const presenter = new LeagueStandingsPresenter();
|
||||
return presenter.present({
|
||||
standings,
|
||||
drivers,
|
||||
memberships,
|
||||
leagueId,
|
||||
currentDriverId,
|
||||
isAdmin,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import type { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
|
||||
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
|
||||
/**
|
||||
* Session Presenter
|
||||
*
|
||||
* Converts AuthSessionDTO to SessionViewModel for client-side presentation.
|
||||
* Pure and deterministic - no side effects.
|
||||
*/
|
||||
export class SessionPresenter {
|
||||
/**
|
||||
* Present session data as a view model
|
||||
*/
|
||||
present(sessionDto: AuthSessionDTO | null): SessionViewModel | null {
|
||||
if (!sessionDto || !sessionDto.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SessionViewModel(sessionDto.user);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user