wip
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import type { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import type {
|
||||
IAllLeaguesWithCapacityAndScoringPresenter,
|
||||
LeagueEnrichedData,
|
||||
LeagueSummaryViewModel,
|
||||
AllLeaguesWithCapacityAndScoringViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter';
|
||||
|
||||
export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter {
|
||||
private viewModel: AllLeaguesWithCapacityAndScoringViewModel | null = null;
|
||||
|
||||
present(enrichedLeagues: LeagueEnrichedData[]): AllLeaguesWithCapacityAndScoringViewModel {
|
||||
const leagueItems: LeagueSummaryViewModel[] = enrichedLeagues.map((data) => {
|
||||
const { league, usedDriverSlots, season, scoringConfig, game, preset } = data;
|
||||
|
||||
const configuredMaxDrivers = league.settings.maxDrivers ?? usedDriverSlots;
|
||||
const safeMaxDrivers = Math.max(configuredMaxDrivers, usedDriverSlots);
|
||||
|
||||
const structureSummary = `Solo • ${safeMaxDrivers} drivers`;
|
||||
|
||||
const qualifyingMinutes = 30;
|
||||
const mainRaceMinutes =
|
||||
typeof league.settings.sessionDuration === 'number'
|
||||
? league.settings.sessionDuration
|
||||
: 40;
|
||||
const timingSummary = `${qualifyingMinutes} min Quali • ${mainRaceMinutes} min Race`;
|
||||
|
||||
let scoringSummary: LeagueSummaryViewModel['scoring'] | undefined;
|
||||
let scoringPatternSummary: string | undefined;
|
||||
|
||||
if (season && scoringConfig && game) {
|
||||
const dropPolicySummary =
|
||||
preset?.dropPolicySummary ?? this.deriveDropPolicySummary(scoringConfig);
|
||||
const primaryChampionshipType =
|
||||
preset?.primaryChampionshipType ??
|
||||
(scoringConfig.championships[0]?.type ?? 'driver');
|
||||
|
||||
const scoringPresetName = preset?.name ?? 'Custom';
|
||||
scoringPatternSummary = `${scoringPresetName} • ${dropPolicySummary}`;
|
||||
|
||||
scoringSummary = {
|
||||
gameId: game.id,
|
||||
gameName: game.name,
|
||||
primaryChampionshipType,
|
||||
scoringPresetId: scoringConfig.scoringPresetId ?? 'custom',
|
||||
scoringPresetName,
|
||||
dropPolicySummary,
|
||||
scoringPatternSummary,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
createdAt: league.createdAt,
|
||||
maxDrivers: safeMaxDrivers,
|
||||
usedDriverSlots,
|
||||
maxTeams: undefined,
|
||||
usedTeamSlots: undefined,
|
||||
structureSummary,
|
||||
scoringPatternSummary,
|
||||
timingSummary,
|
||||
scoring: scoringSummary,
|
||||
};
|
||||
});
|
||||
|
||||
this.viewModel = {
|
||||
leagues: leagueItems,
|
||||
totalCount: leagueItems.length,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
private deriveDropPolicySummary(config: {
|
||||
championships: Array<{
|
||||
dropScorePolicy: { strategy: string; count?: number; dropCount?: number };
|
||||
}>;
|
||||
}): string {
|
||||
const championship = config.championships[0];
|
||||
if (!championship) {
|
||||
return 'All results count';
|
||||
}
|
||||
|
||||
const policy = championship.dropScorePolicy;
|
||||
if (!policy || policy.strategy === 'none') {
|
||||
return 'All results count';
|
||||
}
|
||||
|
||||
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
|
||||
return `Best ${policy.count} results count`;
|
||||
}
|
||||
|
||||
if (
|
||||
policy.strategy === 'dropWorstN' &&
|
||||
typeof policy.dropCount === 'number'
|
||||
) {
|
||||
return `Worst ${policy.dropCount} results are dropped`;
|
||||
}
|
||||
|
||||
return 'Custom drop score rules';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import type {
|
||||
IAllLeaguesWithCapacityPresenter,
|
||||
LeagueWithCapacityViewModel,
|
||||
AllLeaguesWithCapacityViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityPresenter';
|
||||
|
||||
export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter {
|
||||
private viewModel: AllLeaguesWithCapacityViewModel | null = null;
|
||||
|
||||
present(
|
||||
leagues: League[],
|
||||
memberCounts: Map<string, number>
|
||||
): AllLeaguesWithCapacityViewModel {
|
||||
const leagueItems: LeagueWithCapacityViewModel[] = leagues.map((league) => {
|
||||
const usedSlots = memberCounts.get(league.id) ?? 0;
|
||||
|
||||
// Ensure we never expose an impossible state like 26/24:
|
||||
// clamp maxDrivers to at least usedSlots at the application boundary.
|
||||
const configuredMax = league.settings.maxDrivers ?? usedSlots;
|
||||
const safeMaxDrivers = Math.max(configuredMax, usedSlots);
|
||||
|
||||
return {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
settings: {
|
||||
...league.settings,
|
||||
maxDrivers: safeMaxDrivers,
|
||||
},
|
||||
createdAt: league.createdAt.toISOString(),
|
||||
socialLinks: league.socialLinks
|
||||
? {
|
||||
discordUrl: league.socialLinks.discordUrl,
|
||||
youtubeUrl: league.socialLinks.youtubeUrl,
|
||||
websiteUrl: league.socialLinks.websiteUrl,
|
||||
}
|
||||
: undefined,
|
||||
usedSlots,
|
||||
};
|
||||
});
|
||||
|
||||
this.viewModel = {
|
||||
leagues: leagueItems,
|
||||
totalCount: leagueItems.length,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): AllLeaguesWithCapacityViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
38
apps/website/lib/presenters/AllTeamsPresenter.ts
Normal file
38
apps/website/lib/presenters/AllTeamsPresenter.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Team } from '@gridpilot/racing/domain/entities/Team';
|
||||
import type {
|
||||
IAllTeamsPresenter,
|
||||
TeamListItemViewModel,
|
||||
AllTeamsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
|
||||
|
||||
export class AllTeamsPresenter implements IAllTeamsPresenter {
|
||||
private viewModel: AllTeamsViewModel | null = null;
|
||||
|
||||
present(teams: Array<Team & { memberCount?: number }>): AllTeamsViewModel {
|
||||
const teamItems: TeamListItemViewModel[] = teams.map((team) => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
memberCount: team.memberCount ?? 0,
|
||||
leagues: team.leagues,
|
||||
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
|
||||
region: team.region,
|
||||
languages: team.languages,
|
||||
}));
|
||||
|
||||
this.viewModel = {
|
||||
teams: teamItems,
|
||||
totalCount: teamItems.length,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): AllTeamsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type {
|
||||
IDriverRegistrationStatusPresenter,
|
||||
DriverRegistrationStatusViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IDriverRegistrationStatusPresenter';
|
||||
|
||||
export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter {
|
||||
private viewModel: DriverRegistrationStatusViewModel | null = null;
|
||||
|
||||
present(
|
||||
isRegistered: boolean,
|
||||
raceId: string,
|
||||
driverId: string
|
||||
): DriverRegistrationStatusViewModel {
|
||||
this.viewModel = {
|
||||
isRegistered,
|
||||
raceId,
|
||||
driverId,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): DriverRegistrationStatusViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
48
apps/website/lib/presenters/DriverTeamPresenter.ts
Normal file
48
apps/website/lib/presenters/DriverTeamPresenter.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Team, TeamMembership } from '@gridpilot/racing/domain/entities/Team';
|
||||
import type {
|
||||
IDriverTeamPresenter,
|
||||
DriverTeamViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
|
||||
|
||||
export class DriverTeamPresenter implements IDriverTeamPresenter {
|
||||
private viewModel: DriverTeamViewModel | null = null;
|
||||
|
||||
present(
|
||||
team: Team,
|
||||
membership: TeamMembership,
|
||||
driverId: string
|
||||
): DriverTeamViewModel {
|
||||
const isOwner = team.ownerId === driverId;
|
||||
const canManage = membership.role === 'owner' || membership.role === 'manager';
|
||||
|
||||
this.viewModel = {
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
ownerId: team.ownerId,
|
||||
leagues: team.leagues,
|
||||
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
|
||||
region: team.region,
|
||||
languages: team.languages,
|
||||
},
|
||||
membership: {
|
||||
role: membership.role,
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
isActive: membership.isActive,
|
||||
},
|
||||
isOwner,
|
||||
canManage,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): DriverTeamViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
81
apps/website/lib/presenters/DriversLeaderboardPresenter.ts
Normal file
81
apps/website/lib/presenters/DriversLeaderboardPresenter.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import type { SkillLevel } from '@gridpilot/racing/domain/services/SkillLevelService';
|
||||
import { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService';
|
||||
import type {
|
||||
IDriversLeaderboardPresenter,
|
||||
DriverLeaderboardItemViewModel,
|
||||
DriversLeaderboardViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
|
||||
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
|
||||
private viewModel: DriversLeaderboardViewModel | null = null;
|
||||
|
||||
present(
|
||||
drivers: Driver[],
|
||||
rankings: Array<{ driverId: string; rating: number; overallRank: number }>,
|
||||
stats: Record<string, { rating: number; wins: number; podiums: number; totalRaces: number; overallRank: number }>,
|
||||
avatarUrls: Record<string, string>
|
||||
): DriversLeaderboardViewModel {
|
||||
const items: DriverLeaderboardItemViewModel[] = drivers.map((driver) => {
|
||||
const driverStats = stats[driver.id];
|
||||
const rating = driverStats?.rating ?? 0;
|
||||
const wins = driverStats?.wins ?? 0;
|
||||
const podiums = driverStats?.podiums ?? 0;
|
||||
const totalRaces = driverStats?.totalRaces ?? 0;
|
||||
|
||||
let effectiveRank = Number.POSITIVE_INFINITY;
|
||||
if (typeof driverStats?.overallRank === 'number' && driverStats.overallRank > 0) {
|
||||
effectiveRank = driverStats.overallRank;
|
||||
} else {
|
||||
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
|
||||
if (indexInGlobal !== -1) {
|
||||
effectiveRank = indexInGlobal + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const skillLevel = SkillLevelService.getSkillLevel(rating);
|
||||
const isActive = rankings.some((r) => r.driverId === driver.id);
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
rating,
|
||||
skillLevel,
|
||||
nationality: driver.country,
|
||||
racesCompleted: totalRaces,
|
||||
wins,
|
||||
podiums,
|
||||
isActive,
|
||||
rank: effectiveRank,
|
||||
avatarUrl: avatarUrls[driver.id] ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
items.sort((a, b) => {
|
||||
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
|
||||
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
|
||||
if (rankA !== rankB) return rankA - rankB;
|
||||
return b.rating - a.rating;
|
||||
});
|
||||
|
||||
const totalRaces = items.reduce((sum, d) => sum + d.racesCompleted, 0);
|
||||
const totalWins = items.reduce((sum, d) => sum + d.wins, 0);
|
||||
const activeCount = items.filter((d) => d.isActive).length;
|
||||
|
||||
this.viewModel = {
|
||||
drivers: items,
|
||||
totalRaces,
|
||||
totalWins,
|
||||
activeCount,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): DriversLeaderboardViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { IEntitySponsorshipPricingPresenter } from '@racing/application/presenters/IEntitySponsorshipPricingPresenter';
|
||||
import type { GetEntitySponsorshipPricingResultDTO } from '@racing/application/use-cases/GetEntitySponsorshipPricingQuery';
|
||||
|
||||
export class EntitySponsorshipPricingPresenter implements IEntitySponsorshipPricingPresenter {
|
||||
private data: GetEntitySponsorshipPricingResultDTO | null = null;
|
||||
|
||||
present(data: GetEntitySponsorshipPricingResultDTO | null): void {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
getData(): GetEntitySponsorshipPricingResultDTO | null {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type {
|
||||
ILeagueDriverSeasonStatsPresenter,
|
||||
LeagueDriverSeasonStatsItemViewModel,
|
||||
LeagueDriverSeasonStatsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ILeagueDriverSeasonStatsPresenter';
|
||||
|
||||
export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStatsPresenter {
|
||||
private viewModel: LeagueDriverSeasonStatsViewModel | null = null;
|
||||
|
||||
present(
|
||||
leagueId: string,
|
||||
standings: Array<{
|
||||
driverId: string;
|
||||
position: number;
|
||||
points: number;
|
||||
racesCompleted: number;
|
||||
}>,
|
||||
penalties: Map<string, { baseDelta: number; bonusDelta: number }>,
|
||||
driverResults: Map<string, Array<{ position: number }>>,
|
||||
driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>
|
||||
): LeagueDriverSeasonStatsViewModel {
|
||||
const stats: LeagueDriverSeasonStatsItemViewModel[] = standings.map((standing) => {
|
||||
const penalty = penalties.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
|
||||
const totalPenaltyPoints = penalty.baseDelta;
|
||||
const bonusPoints = penalty.bonusDelta;
|
||||
|
||||
const racesCompleted = standing.racesCompleted;
|
||||
const pointsPerRace = racesCompleted > 0 ? standing.points / racesCompleted : 0;
|
||||
|
||||
const ratingInfo = driverRatings.get(standing.driverId) ?? { rating: null, ratingChange: null };
|
||||
|
||||
const results = driverResults.get(standing.driverId) ?? [];
|
||||
let avgFinish: number | null = null;
|
||||
if (results.length > 0) {
|
||||
const totalPositions = results.reduce((sum, r) => sum + r.position, 0);
|
||||
const avg = totalPositions / results.length;
|
||||
avgFinish = Number.isFinite(avg) ? Number(avg.toFixed(2)) : null;
|
||||
}
|
||||
|
||||
return {
|
||||
leagueId,
|
||||
driverId: standing.driverId,
|
||||
position: standing.position,
|
||||
driverName: '',
|
||||
teamId: undefined,
|
||||
teamName: undefined,
|
||||
totalPoints: standing.points + totalPenaltyPoints + bonusPoints,
|
||||
basePoints: standing.points,
|
||||
penaltyPoints: Math.abs(totalPenaltyPoints),
|
||||
bonusPoints,
|
||||
pointsPerRace,
|
||||
racesStarted: results.length,
|
||||
racesFinished: results.length,
|
||||
dnfs: 0,
|
||||
noShows: 0,
|
||||
avgFinish,
|
||||
rating: ratingInfo.rating,
|
||||
ratingChange: ratingInfo.ratingChange,
|
||||
};
|
||||
});
|
||||
|
||||
stats.sort((a, b) => a.position - b.position);
|
||||
|
||||
this.viewModel = {
|
||||
leagueId,
|
||||
stats,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): LeagueDriverSeasonStatsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
119
apps/website/lib/presenters/LeagueFullConfigPresenter.ts
Normal file
119
apps/website/lib/presenters/LeagueFullConfigPresenter.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { DropScorePolicy } from '@gridpilot/racing/domain/value-objects/DropScorePolicy';
|
||||
import type {
|
||||
ILeagueFullConfigPresenter,
|
||||
LeagueFullConfigData,
|
||||
LeagueConfigFormViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ILeagueFullConfigPresenter';
|
||||
|
||||
export class LeagueFullConfigPresenter implements ILeagueFullConfigPresenter {
|
||||
private viewModel: LeagueConfigFormViewModel | null = null;
|
||||
|
||||
present(data: LeagueFullConfigData): LeagueConfigFormViewModel {
|
||||
const { league, activeSeason, scoringConfig, game } = data;
|
||||
|
||||
const patternId = scoringConfig?.scoringPresetId;
|
||||
|
||||
const primaryChampionship =
|
||||
scoringConfig && scoringConfig.championships && scoringConfig.championships.length > 0
|
||||
? scoringConfig.championships[0]
|
||||
: undefined;
|
||||
|
||||
const dropPolicy = primaryChampionship?.dropScorePolicy ?? undefined;
|
||||
const dropPolicyForm = this.mapDropPolicy(dropPolicy);
|
||||
|
||||
const defaultQualifyingMinutes = 30;
|
||||
const defaultMainRaceMinutes = 40;
|
||||
const mainRaceMinutes =
|
||||
typeof league.settings.sessionDuration === 'number'
|
||||
? league.settings.sessionDuration
|
||||
: defaultMainRaceMinutes;
|
||||
const qualifyingMinutes = defaultQualifyingMinutes;
|
||||
|
||||
const roundsPlanned = 8;
|
||||
|
||||
let sessionCount = 2;
|
||||
if (
|
||||
primaryChampionship &&
|
||||
Array.isArray((primaryChampionship as any).sessionTypes) &&
|
||||
(primaryChampionship as any).sessionTypes.length > 0
|
||||
) {
|
||||
sessionCount = (primaryChampionship as any).sessionTypes.length;
|
||||
}
|
||||
|
||||
const practiceMinutes = 20;
|
||||
const sprintRaceMinutes = patternId === 'sprint-main-driver' ? 20 : undefined;
|
||||
|
||||
this.viewModel = {
|
||||
leagueId: league.id,
|
||||
basics: {
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
visibility: 'public',
|
||||
gameId: game?.id ?? 'iracing',
|
||||
},
|
||||
structure: {
|
||||
mode: 'solo',
|
||||
maxDrivers: league.settings.maxDrivers ?? 32,
|
||||
maxTeams: undefined,
|
||||
driversPerTeam: undefined,
|
||||
multiClassEnabled: false,
|
||||
},
|
||||
championships: {
|
||||
enableDriverChampionship: true,
|
||||
enableTeamChampionship: false,
|
||||
enableNationsChampionship: false,
|
||||
enableTrophyChampionship: false,
|
||||
},
|
||||
scoring: {
|
||||
patternId: patternId ?? undefined,
|
||||
customScoringEnabled: !patternId,
|
||||
},
|
||||
dropPolicy: dropPolicyForm,
|
||||
timings: {
|
||||
practiceMinutes,
|
||||
qualifyingMinutes,
|
||||
sprintRaceMinutes,
|
||||
mainRaceMinutes,
|
||||
sessionCount,
|
||||
roundsPlanned,
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: 'admin_only',
|
||||
requireDefense: true,
|
||||
defenseTimeLimit: 48,
|
||||
voteTimeLimit: 72,
|
||||
protestDeadlineHours: 72,
|
||||
stewardingClosesHours: 168,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
},
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): LeagueConfigFormViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
private mapDropPolicy(policy: DropScorePolicy | undefined): { strategy: string; n?: number } {
|
||||
if (!policy || policy.strategy === 'none') {
|
||||
return { strategy: 'none' };
|
||||
}
|
||||
|
||||
if (policy.strategy === 'bestNResults') {
|
||||
const n = typeof policy.count === 'number' ? policy.count : undefined;
|
||||
return n !== undefined ? { strategy: 'bestNResults', n } : { strategy: 'none' };
|
||||
}
|
||||
|
||||
if (policy.strategy === 'dropWorstN') {
|
||||
const n = typeof policy.dropCount === 'number' ? policy.dropCount : undefined;
|
||||
return n !== undefined ? { strategy: 'dropWorstN', n } : { strategy: 'none' };
|
||||
}
|
||||
|
||||
return { strategy: 'none' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ILeagueSchedulePreviewPresenter } from '@racing/application/presenters/ILeagueSchedulePreviewPresenter';
|
||||
import type { LeagueSchedulePreviewDTO } from '@racing/application/dto/LeagueScheduleDTO';
|
||||
|
||||
export class LeagueSchedulePreviewPresenter implements ILeagueSchedulePreviewPresenter {
|
||||
private data: LeagueSchedulePreviewDTO | null = null;
|
||||
|
||||
present(data: LeagueSchedulePreviewDTO): void {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
getData(): LeagueSchedulePreviewDTO | null {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
149
apps/website/lib/presenters/LeagueScoringConfigPresenter.ts
Normal file
149
apps/website/lib/presenters/LeagueScoringConfigPresenter.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { ChampionshipConfig } from '@gridpilot/racing/domain/value-objects/ChampionshipConfig';
|
||||
import type { BonusRule } from '@gridpilot/racing/domain/value-objects/BonusRule';
|
||||
import type {
|
||||
ILeagueScoringConfigPresenter,
|
||||
LeagueScoringConfigData,
|
||||
LeagueScoringConfigViewModel,
|
||||
LeagueScoringChampionshipViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ILeagueScoringConfigPresenter';
|
||||
|
||||
export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter {
|
||||
private viewModel: LeagueScoringConfigViewModel | null = null;
|
||||
|
||||
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel {
|
||||
const championships: LeagueScoringChampionshipViewModel[] =
|
||||
data.championships.map((champ) => this.mapChampionship(champ));
|
||||
|
||||
const dropPolicySummary =
|
||||
data.preset?.dropPolicySummary ??
|
||||
this.deriveDropPolicyDescriptionFromChampionships(data.championships);
|
||||
|
||||
this.viewModel = {
|
||||
leagueId: data.leagueId,
|
||||
seasonId: data.seasonId,
|
||||
gameId: data.gameId,
|
||||
gameName: data.gameName,
|
||||
scoringPresetId: data.scoringPresetId,
|
||||
scoringPresetName: data.preset?.name,
|
||||
dropPolicySummary,
|
||||
championships,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): LeagueScoringConfigViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipViewModel {
|
||||
const sessionTypes = championship.sessionTypes.map((s) => s.toString());
|
||||
const pointsPreview = this.buildPointsPreview(championship.pointsTableBySessionType);
|
||||
const bonusSummary = this.buildBonusSummary(
|
||||
championship.bonusRulesBySessionType ?? {},
|
||||
);
|
||||
const dropPolicyDescription = this.deriveDropPolicyDescription(
|
||||
championship.dropScorePolicy,
|
||||
);
|
||||
|
||||
return {
|
||||
id: championship.id,
|
||||
name: championship.name,
|
||||
type: championship.type,
|
||||
sessionTypes,
|
||||
pointsPreview,
|
||||
bonusSummary,
|
||||
dropPolicyDescription,
|
||||
};
|
||||
}
|
||||
|
||||
private buildPointsPreview(
|
||||
tables: Record<string, any>,
|
||||
): Array<{ sessionType: string; position: number; points: number }> {
|
||||
const preview: Array<{
|
||||
sessionType: string;
|
||||
position: number;
|
||||
points: number;
|
||||
}> = [];
|
||||
|
||||
const maxPositions = 10;
|
||||
|
||||
for (const [sessionType, table] of Object.entries(tables)) {
|
||||
for (let pos = 1; pos <= maxPositions; pos++) {
|
||||
const points = table.getPointsForPosition(pos);
|
||||
if (points && points !== 0) {
|
||||
preview.push({
|
||||
sessionType,
|
||||
position: pos,
|
||||
points,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
private buildBonusSummary(
|
||||
bonusRulesBySessionType: Record<string, BonusRule[]>,
|
||||
): string[] {
|
||||
const summaries: string[] = [];
|
||||
|
||||
for (const [sessionType, rules] of Object.entries(bonusRulesBySessionType)) {
|
||||
for (const rule of rules) {
|
||||
if (rule.type === 'fastestLap') {
|
||||
const base = `Fastest lap in ${sessionType}`;
|
||||
if (rule.requiresFinishInTopN) {
|
||||
summaries.push(
|
||||
`${base} +${rule.points} points if finishing P${rule.requiresFinishInTopN} or better`,
|
||||
);
|
||||
} else {
|
||||
summaries.push(`${base} +${rule.points} points`);
|
||||
}
|
||||
} else {
|
||||
summaries.push(
|
||||
`${rule.type} bonus in ${sessionType} worth ${rule.points} points`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
private deriveDropPolicyDescriptionFromChampionships(
|
||||
championships: ChampionshipConfig[],
|
||||
): string {
|
||||
const first = championships[0];
|
||||
if (!first) {
|
||||
return 'All results count';
|
||||
}
|
||||
return this.deriveDropPolicyDescription(first.dropScorePolicy);
|
||||
}
|
||||
|
||||
private deriveDropPolicyDescription(policy: {
|
||||
strategy: string;
|
||||
count?: number;
|
||||
dropCount?: number;
|
||||
}): string {
|
||||
if (!policy || policy.strategy === 'none') {
|
||||
return 'All results count';
|
||||
}
|
||||
|
||||
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
|
||||
return `Best ${policy.count} results count towards the championship`;
|
||||
}
|
||||
|
||||
if (
|
||||
policy.strategy === 'dropWorstN' &&
|
||||
typeof policy.dropCount === 'number'
|
||||
) {
|
||||
return `Worst ${policy.dropCount} results are dropped from the championship total`;
|
||||
}
|
||||
|
||||
return 'Custom drop score rules apply';
|
||||
}
|
||||
}
|
||||
25
apps/website/lib/presenters/LeagueScoringPresetsPresenter.ts
Normal file
25
apps/website/lib/presenters/LeagueScoringPresetsPresenter.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { LeagueScoringPresetDTO } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
|
||||
import type {
|
||||
ILeagueScoringPresetsPresenter,
|
||||
LeagueScoringPresetsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ILeagueScoringPresetsPresenter';
|
||||
|
||||
export class LeagueScoringPresetsPresenter implements ILeagueScoringPresetsPresenter {
|
||||
private viewModel: LeagueScoringPresetsViewModel | null = null;
|
||||
|
||||
present(presets: LeagueScoringPresetDTO[]): LeagueScoringPresetsViewModel {
|
||||
this.viewModel = {
|
||||
presets,
|
||||
totalCount: presets.length,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): LeagueScoringPresetsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
38
apps/website/lib/presenters/LeagueStandingsPresenter.ts
Normal file
38
apps/website/lib/presenters/LeagueStandingsPresenter.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Standing } from '@gridpilot/racing/domain/entities/Standing';
|
||||
import type {
|
||||
ILeagueStandingsPresenter,
|
||||
StandingItemViewModel,
|
||||
LeagueStandingsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ILeagueStandingsPresenter';
|
||||
|
||||
export class LeagueStandingsPresenter implements ILeagueStandingsPresenter {
|
||||
private viewModel: LeagueStandingsViewModel | null = null;
|
||||
|
||||
present(standings: Standing[]): LeagueStandingsViewModel {
|
||||
const standingItems: StandingItemViewModel[] = standings.map((standing) => ({
|
||||
id: standing.id,
|
||||
leagueId: standing.leagueId,
|
||||
seasonId: standing.seasonId,
|
||||
driverId: standing.driverId,
|
||||
position: standing.position,
|
||||
points: standing.points,
|
||||
wins: standing.wins,
|
||||
podiums: standing.podiums,
|
||||
racesCompleted: standing.racesCompleted,
|
||||
}));
|
||||
|
||||
this.viewModel = {
|
||||
leagueId: standings[0]?.leagueId ?? '',
|
||||
standings: standingItems,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): LeagueStandingsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
42
apps/website/lib/presenters/LeagueStatsPresenter.ts
Normal file
42
apps/website/lib/presenters/LeagueStatsPresenter.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type {
|
||||
ILeagueStatsPresenter,
|
||||
LeagueStatsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ILeagueStatsPresenter';
|
||||
|
||||
export class LeagueStatsPresenter implements ILeagueStatsPresenter {
|
||||
private viewModel: LeagueStatsViewModel | null = null;
|
||||
|
||||
present(
|
||||
leagueId: string,
|
||||
totalRaces: number,
|
||||
completedRaces: number,
|
||||
scheduledRaces: number,
|
||||
sofValues: number[]
|
||||
): LeagueStatsViewModel {
|
||||
const averageSOF = sofValues.length > 0
|
||||
? Math.round(sofValues.reduce((a, b) => a + b, 0) / sofValues.length)
|
||||
: null;
|
||||
|
||||
const highestSOF = sofValues.length > 0 ? Math.max(...sofValues) : null;
|
||||
const lowestSOF = sofValues.length > 0 ? Math.min(...sofValues) : null;
|
||||
|
||||
this.viewModel = {
|
||||
leagueId,
|
||||
totalRaces,
|
||||
completedRaces,
|
||||
scheduledRaces,
|
||||
averageSOF,
|
||||
highestSOF,
|
||||
lowestSOF,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): LeagueStatsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { IPendingSponsorshipRequestsPresenter } from '@racing/application/presenters/IPendingSponsorshipRequestsPresenter';
|
||||
import type { GetPendingSponsorshipRequestsResultDTO } from '@racing/application/use-cases/GetPendingSponsorshipRequestsQuery';
|
||||
|
||||
export class PendingSponsorshipRequestsPresenter implements IPendingSponsorshipRequestsPresenter {
|
||||
private data: GetPendingSponsorshipRequestsResultDTO | null = null;
|
||||
|
||||
present(data: GetPendingSponsorshipRequestsResultDTO): void {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
getData(): GetPendingSponsorshipRequestsResultDTO | null {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
60
apps/website/lib/presenters/RacePenaltiesPresenter.ts
Normal file
60
apps/website/lib/presenters/RacePenaltiesPresenter.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type {
|
||||
IRacePenaltiesPresenter,
|
||||
RacePenaltyViewModel,
|
||||
RacePenaltiesViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IRacePenaltiesPresenter';
|
||||
import type { PenaltyType, PenaltyStatus } from '@gridpilot/racing/domain/entities/Penalty';
|
||||
|
||||
export class RacePenaltiesPresenter implements IRacePenaltiesPresenter {
|
||||
private viewModel: RacePenaltiesViewModel | null = null;
|
||||
|
||||
present(
|
||||
penalties: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
type: PenaltyType;
|
||||
value?: number;
|
||||
reason: string;
|
||||
protestId?: string;
|
||||
issuedBy: string;
|
||||
status: PenaltyStatus;
|
||||
issuedAt: Date;
|
||||
appliedAt?: Date;
|
||||
notes?: string;
|
||||
getDescription(): string;
|
||||
}>,
|
||||
driverMap: Map<string, string>
|
||||
): RacePenaltiesViewModel {
|
||||
const penaltyViewModels: RacePenaltyViewModel[] = penalties.map(penalty => ({
|
||||
id: penalty.id,
|
||||
raceId: penalty.raceId,
|
||||
driverId: penalty.driverId,
|
||||
driverName: driverMap.get(penalty.driverId) || 'Unknown',
|
||||
type: penalty.type,
|
||||
value: penalty.value,
|
||||
reason: penalty.reason,
|
||||
protestId: penalty.protestId,
|
||||
issuedBy: penalty.issuedBy,
|
||||
issuedByName: driverMap.get(penalty.issuedBy) || 'Unknown',
|
||||
status: penalty.status,
|
||||
description: penalty.getDescription(),
|
||||
issuedAt: penalty.issuedAt.toISOString(),
|
||||
appliedAt: penalty.appliedAt?.toISOString(),
|
||||
notes: penalty.notes,
|
||||
}));
|
||||
|
||||
this.viewModel = {
|
||||
penalties: penaltyViewModels,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): RacePenaltiesViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
59
apps/website/lib/presenters/RaceProtestsPresenter.ts
Normal file
59
apps/website/lib/presenters/RaceProtestsPresenter.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type {
|
||||
IRaceProtestsPresenter,
|
||||
RaceProtestViewModel,
|
||||
RaceProtestsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IRaceProtestsPresenter';
|
||||
import type { ProtestStatus, ProtestIncident } from '@gridpilot/racing/domain/entities/Protest';
|
||||
|
||||
export class RaceProtestsPresenter implements IRaceProtestsPresenter {
|
||||
private viewModel: RaceProtestsViewModel | null = null;
|
||||
|
||||
present(
|
||||
protests: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
incident: ProtestIncident;
|
||||
comment?: string;
|
||||
proofVideoUrl?: string;
|
||||
status: ProtestStatus;
|
||||
reviewedBy?: string;
|
||||
decisionNotes?: string;
|
||||
filedAt: Date;
|
||||
reviewedAt?: Date;
|
||||
}>,
|
||||
driverMap: Map<string, string>
|
||||
): RaceProtestsViewModel {
|
||||
const protestViewModels: RaceProtestViewModel[] = protests.map(protest => ({
|
||||
id: protest.id,
|
||||
raceId: protest.raceId,
|
||||
protestingDriverId: protest.protestingDriverId,
|
||||
protestingDriverName: driverMap.get(protest.protestingDriverId) || 'Unknown',
|
||||
accusedDriverId: protest.accusedDriverId,
|
||||
accusedDriverName: driverMap.get(protest.accusedDriverId) || 'Unknown',
|
||||
incident: protest.incident,
|
||||
comment: protest.comment,
|
||||
proofVideoUrl: protest.proofVideoUrl,
|
||||
status: protest.status,
|
||||
reviewedBy: protest.reviewedBy,
|
||||
reviewedByName: protest.reviewedBy ? driverMap.get(protest.reviewedBy) : undefined,
|
||||
decisionNotes: protest.decisionNotes,
|
||||
filedAt: protest.filedAt.toISOString(),
|
||||
reviewedAt: protest.reviewedAt?.toISOString(),
|
||||
}));
|
||||
|
||||
this.viewModel = {
|
||||
protests: protestViewModels,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): RaceProtestsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
24
apps/website/lib/presenters/RaceRegistrationsPresenter.ts
Normal file
24
apps/website/lib/presenters/RaceRegistrationsPresenter.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type {
|
||||
IRaceRegistrationsPresenter,
|
||||
RaceRegistrationsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IRaceRegistrationsPresenter';
|
||||
|
||||
export class RaceRegistrationsPresenter implements IRaceRegistrationsPresenter {
|
||||
private viewModel: RaceRegistrationsViewModel | null = null;
|
||||
|
||||
present(registeredDriverIds: string[]): RaceRegistrationsViewModel {
|
||||
this.viewModel = {
|
||||
registeredDriverIds,
|
||||
count: registeredDriverIds.length,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): RaceRegistrationsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
49
apps/website/lib/presenters/RaceWithSOFPresenter.ts
Normal file
49
apps/website/lib/presenters/RaceWithSOFPresenter.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type {
|
||||
IRaceWithSOFPresenter,
|
||||
RaceWithSOFViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IRaceWithSOFPresenter';
|
||||
|
||||
export class RaceWithSOFPresenter implements IRaceWithSOFPresenter {
|
||||
private viewModel: RaceWithSOFViewModel | null = null;
|
||||
|
||||
present(
|
||||
raceId: string,
|
||||
leagueId: string,
|
||||
scheduledAt: Date,
|
||||
track: string,
|
||||
trackId: string,
|
||||
car: string,
|
||||
carId: string,
|
||||
sessionType: string,
|
||||
status: string,
|
||||
strengthOfField: number | null,
|
||||
registeredCount: number,
|
||||
maxParticipants: number,
|
||||
participantCount: number
|
||||
): RaceWithSOFViewModel {
|
||||
this.viewModel = {
|
||||
id: raceId,
|
||||
leagueId,
|
||||
scheduledAt: scheduledAt.toISOString(),
|
||||
track,
|
||||
trackId,
|
||||
car,
|
||||
carId,
|
||||
sessionType,
|
||||
status,
|
||||
strengthOfField,
|
||||
registeredCount,
|
||||
maxParticipants,
|
||||
participantCount,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): RaceWithSOFViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
64
apps/website/lib/presenters/RacesPagePresenter.ts
Normal file
64
apps/website/lib/presenters/RacesPagePresenter.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type {
|
||||
IRacesPagePresenter,
|
||||
RacesPageViewModel,
|
||||
RaceListItemViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IRacesPagePresenter';
|
||||
|
||||
export class RacesPagePresenter implements IRacesPagePresenter {
|
||||
private viewModel: RacesPageViewModel | null = null;
|
||||
|
||||
present(races: any[]): void {
|
||||
const now = new Date();
|
||||
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const raceViewModels: RaceListItemViewModel[] = races.map(race => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt,
|
||||
status: race.status,
|
||||
leagueId: race.leagueId,
|
||||
leagueName: race.leagueName,
|
||||
strengthOfField: race.strengthOfField,
|
||||
isUpcoming: race.isUpcoming,
|
||||
isLive: race.isLive,
|
||||
isPast: race.isPast,
|
||||
}));
|
||||
|
||||
const stats = {
|
||||
total: raceViewModels.length,
|
||||
scheduled: raceViewModels.filter(r => r.status === 'scheduled').length,
|
||||
running: raceViewModels.filter(r => r.status === 'running').length,
|
||||
completed: raceViewModels.filter(r => r.status === 'completed').length,
|
||||
};
|
||||
|
||||
const liveRaces = raceViewModels.filter(r => r.isLive);
|
||||
|
||||
const upcomingThisWeek = raceViewModels
|
||||
.filter(r => {
|
||||
const scheduledDate = new Date(r.scheduledAt);
|
||||
return r.isUpcoming && scheduledDate >= now && scheduledDate <= nextWeek;
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
const recentResults = raceViewModels
|
||||
.filter(r => r.status === 'completed')
|
||||
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
|
||||
.slice(0, 3);
|
||||
|
||||
this.viewModel = {
|
||||
races: raceViewModels,
|
||||
stats,
|
||||
liveRaces,
|
||||
upcomingThisWeek,
|
||||
recentResults,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): RacesPageViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('ViewModel not yet generated. Call present() first.');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
14
apps/website/lib/presenters/SponsorDashboardPresenter.ts
Normal file
14
apps/website/lib/presenters/SponsorDashboardPresenter.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { ISponsorDashboardPresenter } from '@racing/application/presenters/ISponsorDashboardPresenter';
|
||||
import type { SponsorDashboardDTO } from '@racing/application/use-cases/GetSponsorDashboardQuery';
|
||||
|
||||
export class SponsorDashboardPresenter implements ISponsorDashboardPresenter {
|
||||
private data: SponsorDashboardDTO | null = null;
|
||||
|
||||
present(data: SponsorDashboardDTO | null): void {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
getData(): SponsorDashboardDTO | null {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
14
apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts
Normal file
14
apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { ISponsorSponsorshipsPresenter } from '@racing/application/presenters/ISponsorSponsorshipsPresenter';
|
||||
import type { SponsorSponsorshipsDTO } from '@racing/application/use-cases/GetSponsorSponsorshipsQuery';
|
||||
|
||||
export class SponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter {
|
||||
private data: SponsorSponsorshipsDTO | null = null;
|
||||
|
||||
present(data: SponsorSponsorshipsDTO | null): void {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
getData(): SponsorSponsorshipsDTO | null {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
48
apps/website/lib/presenters/TeamDetailsPresenter.ts
Normal file
48
apps/website/lib/presenters/TeamDetailsPresenter.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Team, TeamMembership } from '@gridpilot/racing/domain/entities/Team';
|
||||
import type {
|
||||
ITeamDetailsPresenter,
|
||||
TeamDetailsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter';
|
||||
|
||||
export class TeamDetailsPresenter implements ITeamDetailsPresenter {
|
||||
private viewModel: TeamDetailsViewModel | null = null;
|
||||
|
||||
present(
|
||||
team: Team,
|
||||
membership: TeamMembership | null,
|
||||
driverId: string
|
||||
): TeamDetailsViewModel {
|
||||
const canManage = membership?.role === 'owner' || membership?.role === 'manager';
|
||||
|
||||
this.viewModel = {
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
ownerId: team.ownerId,
|
||||
leagues: team.leagues,
|
||||
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
|
||||
region: team.region,
|
||||
languages: team.languages,
|
||||
},
|
||||
membership: membership
|
||||
? {
|
||||
role: membership.role,
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
isActive: membership.isActive,
|
||||
}
|
||||
: null,
|
||||
canManage,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): TeamDetailsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
43
apps/website/lib/presenters/TeamJoinRequestsPresenter.ts
Normal file
43
apps/website/lib/presenters/TeamJoinRequestsPresenter.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { TeamJoinRequest } from '@gridpilot/racing/domain/entities/Team';
|
||||
import type {
|
||||
ITeamJoinRequestsPresenter,
|
||||
TeamJoinRequestViewModel,
|
||||
TeamJoinRequestsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ITeamJoinRequestsPresenter';
|
||||
|
||||
export class TeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter {
|
||||
private viewModel: TeamJoinRequestsViewModel | null = null;
|
||||
|
||||
present(
|
||||
requests: TeamJoinRequest[],
|
||||
driverNames: Record<string, string>,
|
||||
avatarUrls: Record<string, string>
|
||||
): TeamJoinRequestsViewModel {
|
||||
const requestItems: TeamJoinRequestViewModel[] = requests.map((request) => ({
|
||||
requestId: request.id,
|
||||
driverId: request.driverId,
|
||||
driverName: driverNames[request.driverId] ?? 'Unknown Driver',
|
||||
teamId: request.teamId,
|
||||
status: request.status,
|
||||
requestedAt: request.requestedAt.toISOString(),
|
||||
avatarUrl: avatarUrls[request.driverId] ?? '',
|
||||
}));
|
||||
|
||||
const pendingCount = requestItems.filter((r) => r.status === 'pending').length;
|
||||
|
||||
this.viewModel = {
|
||||
requests: requestItems,
|
||||
pendingCount,
|
||||
totalCount: requestItems.length,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): TeamJoinRequestsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
46
apps/website/lib/presenters/TeamMembersPresenter.ts
Normal file
46
apps/website/lib/presenters/TeamMembersPresenter.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { TeamMembership } from '@gridpilot/racing/domain/entities/Team';
|
||||
import type {
|
||||
ITeamMembersPresenter,
|
||||
TeamMemberViewModel,
|
||||
TeamMembersViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ITeamMembersPresenter';
|
||||
|
||||
export class TeamMembersPresenter implements ITeamMembersPresenter {
|
||||
private viewModel: TeamMembersViewModel | null = null;
|
||||
|
||||
present(
|
||||
memberships: TeamMembership[],
|
||||
driverNames: Record<string, string>,
|
||||
avatarUrls: Record<string, string>
|
||||
): TeamMembersViewModel {
|
||||
const members: TeamMemberViewModel[] = memberships.map((membership) => ({
|
||||
driverId: membership.driverId,
|
||||
driverName: driverNames[membership.driverId] ?? 'Unknown Driver',
|
||||
role: membership.role,
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
isActive: membership.isActive,
|
||||
avatarUrl: avatarUrls[membership.driverId] ?? '',
|
||||
}));
|
||||
|
||||
const ownerCount = members.filter((m) => m.role === 'owner').length;
|
||||
const managerCount = members.filter((m) => m.role === 'manager').length;
|
||||
const memberCount = members.filter((m) => m.role === 'member').length;
|
||||
|
||||
this.viewModel = {
|
||||
members,
|
||||
totalCount: members.length,
|
||||
ownerCount,
|
||||
managerCount,
|
||||
memberCount,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): TeamMembersViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
42
apps/website/lib/presenters/TeamsLeaderboardPresenter.ts
Normal file
42
apps/website/lib/presenters/TeamsLeaderboardPresenter.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type {
|
||||
ITeamsLeaderboardPresenter,
|
||||
TeamsLeaderboardViewModel,
|
||||
TeamLeaderboardItemViewModel,
|
||||
SkillLevel,
|
||||
} from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
|
||||
export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
|
||||
private viewModel: TeamsLeaderboardViewModel | null = null;
|
||||
|
||||
present(teams: any[], recruitingCount: number): void {
|
||||
this.viewModel = {
|
||||
teams: teams.map((team) => this.transformTeam(team)),
|
||||
recruitingCount,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): TeamsLeaderboardViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('ViewModel not yet generated. Call present() first.');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
private transformTeam(team: any): TeamLeaderboardItemViewModel {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
memberCount: team.memberCount,
|
||||
rating: team.rating,
|
||||
totalWins: team.totalWins,
|
||||
totalRaces: team.totalRaces,
|
||||
performanceLevel: team.performanceLevel as SkillLevel,
|
||||
isRecruiting: team.isRecruiting,
|
||||
createdAt: team.createdAt,
|
||||
description: team.description,
|
||||
specialization: team.specialization,
|
||||
region: team.region,
|
||||
languages: team.languages,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user