website refactor

This commit is contained in:
2026-01-14 10:51:05 +01:00
parent 4522d41aef
commit 0d89ad027e
291 changed files with 6887 additions and 3685 deletions

View File

@@ -0,0 +1,18 @@
/**
* AvatarViewDataBuilder
*
* Transforms MediaBinaryDTO into AvatarViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { AvatarViewData } from '@/lib/view-data/AvatarViewData';
export class AvatarViewDataBuilder {
static build(apiDto: MediaBinaryDTO): AvatarViewData {
return {
buffer: apiDto.buffer,
contentType: apiDto.contentType,
};
}
}

View File

@@ -0,0 +1,18 @@
/**
* CategoryIconViewDataBuilder
*
* Transforms MediaBinaryDTO into CategoryIconViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { CategoryIconViewData } from '@/lib/view-data/CategoryIconViewData';
export class CategoryIconViewDataBuilder {
static build(apiDto: MediaBinaryDTO): CategoryIconViewData {
return {
buffer: apiDto.buffer,
contentType: apiDto.contentType,
};
}
}

View File

@@ -0,0 +1,15 @@
import type { DriverProfileDriverSummaryDTO } from '@/lib/types/generated/DriverProfileDriverSummaryDTO';
import type { DriverProfileStatsDTO } from '@/lib/types/generated/DriverProfileStatsDTO';
import type { DriverProfileFinishDistributionDTO } from '@/lib/types/generated/DriverProfileFinishDistributionDTO';
import type { DriverProfileTeamMembershipDTO } from '@/lib/types/generated/DriverProfileTeamMembershipDTO';
import type { DriverProfileSocialSummaryDTO } from '@/lib/types/generated/DriverProfileSocialSummaryDTO';
import type { DriverProfileExtendedProfileDTO } from '@/lib/types/generated/DriverProfileExtendedProfileDTO';
export interface DriverProfileViewData {
currentDriver: DriverProfileDriverSummaryDTO | null;
stats: DriverProfileStatsDTO | null;
finishDistribution: DriverProfileFinishDistributionDTO | null;
teamMemberships: DriverProfileTeamMembershipDTO[];
socialSummary: DriverProfileSocialSummaryDTO;
extendedProfile: DriverProfileExtendedProfileDTO | null;
}

View File

@@ -0,0 +1,21 @@
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
import type { DriverProfileViewData } from './DriverProfileViewData';
/**
* DriverProfileViewDataBuilder
*
* Transforms GetDriverProfileOutputDTO into ViewData for the driver profile page.
* Deterministic, side-effect free, no HTTP calls.
*/
export class DriverProfileViewDataBuilder {
static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData {
return {
currentDriver: apiDto.currentDriver || null,
stats: apiDto.stats || null,
finishDistribution: apiDto.finishDistribution || null,
teamMemberships: apiDto.teamMemberships,
socialSummary: apiDto.socialSummary,
extendedProfile: apiDto.extendedProfile || null,
};
}
}

View File

@@ -1,9 +1,9 @@
import type { DriverRankingsPageDto } from '@/lib/page-queries/page-dtos/DriverRankingsPageDto';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewData';
export class DriverRankingsViewDataBuilder {
static build(dto: DriverRankingsPageDto | null): DriverRankingsViewData {
if (!dto || !dto.drivers) {
static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData {
if (!apiDto || apiDto.length === 0) {
return {
drivers: [],
podium: [],
@@ -15,7 +15,7 @@ export class DriverRankingsViewDataBuilder {
}
return {
drivers: dto.drivers.map(driver => ({
drivers: apiDto.map(driver => ({
id: driver.id,
name: driver.name,
rating: driver.rating,
@@ -36,7 +36,7 @@ export class DriverRankingsViewDataBuilder {
driver.rank === 3 ? 'text-amber-600' :
'text-gray-500',
})),
podium: dto.drivers.slice(0, 3).map((driver, index) => {
podium: apiDto.slice(0, 3).map((driver, index) => {
const positions = [2, 1, 3]; // Display order: 2nd, 1st, 3rd
const position = positions[index];
return {

View File

@@ -0,0 +1,9 @@
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
export interface DriversViewData {
drivers: DriverLeaderboardItemDTO[];
totalRaces: number;
totalWins: number;
activeCount: number;
}

View File

@@ -0,0 +1,22 @@
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
import type { DriversViewData } from './DriversViewData';
/**
* DriversViewDataBuilder
*
* Transforms DriversLeaderboardDTO into ViewData for the drivers listing page.
* Deterministic, side-effect free, no HTTP calls.
*
* This builder does NOT perform filtering or sorting - that belongs in the API.
* If the API doesn't support filtering, it should be marked as NotImplemented.
*/
export class DriversViewDataBuilder {
static build(apiDto: DriversLeaderboardDTO): DriversViewData {
return {
drivers: apiDto.drivers,
totalRaces: apiDto.totalRaces,
totalWins: apiDto.totalWins,
activeCount: apiDto.activeCount,
};
}
}

View File

@@ -1,26 +1,17 @@
/**
* Forgot Password View Data Builder
*
*
* Transforms ForgotPasswordPageDTO into ViewData for the forgot password template.
* Deterministic, side-effect free, no business logic.
*/
import { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO';
export interface ForgotPasswordViewData {
returnTo: string;
showSuccess: boolean;
successMessage?: string;
magicLink?: string;
formState: any; // Will be managed by client component
isSubmitting: boolean;
submitError?: string;
}
import { ForgotPasswordViewData } from './types/ForgotPasswordViewData';
export class ForgotPasswordViewDataBuilder {
static build(data: ForgotPasswordPageDTO): ForgotPasswordViewData {
static build(apiDto: ForgotPasswordPageDTO): ForgotPasswordViewData {
return {
returnTo: data.returnTo,
returnTo: apiDto.returnTo,
showSuccess: false,
formState: {
fields: {

View File

@@ -4,11 +4,10 @@ import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData'
export class LeaderboardsViewDataBuilder {
static build(
driversDto: { drivers: DriverLeaderboardItemDTO[] } | null,
teamsDto: { teams: TeamListItemDTO[] } | null
apiDto: { drivers: { drivers: DriverLeaderboardItemDTO[] }; teams: { teams: TeamListItemDTO[] } }
): LeaderboardsViewData {
return {
drivers: driversDto?.drivers.slice(0, 5).map((driver, index) => ({
drivers: apiDto.drivers.drivers.map(driver => ({
id: driver.id,
name: driver.name,
rating: driver.rating,
@@ -17,9 +16,9 @@ export class LeaderboardsViewDataBuilder {
wins: driver.wins,
rank: driver.rank,
avatarUrl: driver.avatarUrl || '',
position: index + 1,
})) || [],
teams: teamsDto?.teams.slice(0, 5).map((team, index) => ({
position: driver.rank,
})),
teams: apiDto.teams.teams.map(team => ({
id: team.id,
name: team.name,
tag: team.tag,
@@ -27,8 +26,8 @@ export class LeaderboardsViewDataBuilder {
category: team.category,
totalWins: team.totalWins || 0,
logoUrl: team.logoUrl || '',
position: index + 1,
})) || [],
position: 0, // API doesn't provide team ranking
})),
};
}
}

View File

@@ -0,0 +1,18 @@
/**
* LeagueCoverViewDataBuilder
*
* Transforms MediaBinaryDTO into LeagueCoverViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { LeagueCoverViewData } from '@/lib/view-data/LeagueCoverViewData';
export class LeagueCoverViewDataBuilder {
static build(apiDto: MediaBinaryDTO): LeagueCoverViewData {
return {
buffer: apiDto.buffer,
contentType: apiDto.contentType,
};
}
}

View File

@@ -22,22 +22,22 @@ export class LeagueDetailViewDataBuilder {
}): LeagueDetailViewData {
const { league, owner, scoringConfig, memberships, races, sponsors } = input;
// Calculate running races
// Calculate running races - using available fields from RaceDTO
const runningRaces: LiveRaceData[] = races
.filter(r => r.status === 'running')
.filter(r => r.name.includes('Running')) // Placeholder filter
.map(r => ({
id: r.id,
name: r.name,
date: r.scheduledAt,
registeredCount: r.registeredCount,
strengthOfField: r.strengthOfField,
date: r.date,
registeredCount: 0,
strengthOfField: 0,
}));
// Calculate info data
const membersCount = Array.isArray(memberships.members) ? memberships.members.length : 0;
const completedRacesCount = races.filter(r => r.status === 'completed').length;
const avgSOF = races.length > 0
? Math.round(races.reduce((sum, r) => sum + (r.strengthOfField || 0), 0) / races.length)
const completedRacesCount = races.filter(r => r.name.includes('Completed')).length; // Placeholder
const avgSOF = races.length > 0
? Math.round(races.reduce((sum, r) => sum + 0, 0) / races.length)
: null;
const info: LeagueInfoData = {
@@ -47,7 +47,7 @@ export class LeagueDetailViewDataBuilder {
racesCount: completedRacesCount,
avgSOF,
structure: `Solo • ${league.settings?.maxDrivers ?? 32} max`,
scoring: scoringConfig?.name || 'Standard',
scoring: scoringConfig?.scoringPresetId || 'Standard',
createdAt: league.createdAt,
discordUrl: league.socialLinks?.discordUrl,
youtubeUrl: league.socialLinks?.youtubeUrl,

View File

@@ -0,0 +1,18 @@
/**
* LeagueLogoViewDataBuilder
*
* Transforms MediaBinaryDTO into LeagueLogoViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { LeagueLogoViewData } from '@/lib/view-data/LeagueLogoViewData';
export class LeagueLogoViewDataBuilder {
static build(apiDto: MediaBinaryDTO): LeagueLogoViewData {
return {
buffer: apiDto.buffer,
contentType: apiDto.contentType,
};
}
}

View File

@@ -0,0 +1,47 @@
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
import type { LeagueRosterAdminViewData, RosterMemberData, JoinRequestData } from '@/lib/view-data/LeagueRosterAdminViewData';
/**
* LeagueRosterAdminViewDataBuilder
*
* Transforms API DTOs into LeagueRosterAdminViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
export class LeagueRosterAdminViewDataBuilder {
static build(input: {
leagueId: string;
members: LeagueRosterMemberDTO[];
joinRequests: LeagueRosterJoinRequestDTO[];
}): LeagueRosterAdminViewData {
const { leagueId, members, joinRequests } = input;
// Transform members
const rosterMembers: RosterMemberData[] = members.map(member => ({
driverId: member.driverId,
driver: {
id: member.driverId,
name: member.driver?.name || 'Unknown Driver',
},
role: member.role,
joinedAt: member.joinedAt,
}));
// Transform join requests
const requests: JoinRequestData[] = joinRequests.map(req => ({
id: req.id,
driver: {
id: req.driverId,
name: 'Unknown Driver', // driver field is unknown type
},
requestedAt: req.requestedAt,
message: req.message,
}));
return {
leagueId,
members: rosterMembers,
joinRequests: requests,
};
}
}

View File

@@ -0,0 +1,39 @@
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO';
import type { LeagueScheduleViewData, ScheduleRaceData } from '@/lib/view-data/LeagueScheduleViewData';
/**
* LeagueScheduleViewDataBuilder
*
* Transforms API DTOs into LeagueScheduleViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
export class LeagueScheduleViewDataBuilder {
static build(input: {
schedule: LeagueScheduleDTO;
seasons: LeagueSeasonSummaryDTO[];
leagueId: string;
}): LeagueScheduleViewData {
const { schedule, seasons, leagueId } = input;
// Transform races - using available fields from RaceDTO
const races: ScheduleRaceData[] = (schedule.races || []).map(race => ({
id: race.id,
name: race.name,
track: race.leagueName || 'Unknown Track',
car: 'Unknown Car',
scheduledAt: race.date,
status: 'scheduled',
}));
return {
leagueId,
races,
seasons: seasons.map(s => ({
seasonId: s.seasonId,
name: s.name,
status: s.status,
})),
};
}
}

View File

@@ -1,46 +1,18 @@
/**
* Login View Data Builder
*
*
* Transforms LoginPageDTO into ViewData for the login template.
* Deterministic, side-effect free, no business logic.
*/
import { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
export interface FormFieldState {
value: string | boolean;
error?: string;
touched: boolean;
validating: boolean;
}
export interface FormState {
fields: {
email: FormFieldState;
password: FormFieldState;
rememberMe: FormFieldState;
};
isValid: boolean;
isSubmitting: boolean;
submitError?: string;
submitCount: number;
}
export interface LoginViewData {
returnTo: string;
hasInsufficientPermissions: boolean;
showPassword: boolean;
showErrorDetails: boolean;
formState: FormState;
isSubmitting: boolean;
submitError?: string;
}
import { LoginViewData } from './types/LoginViewData';
export class LoginViewDataBuilder {
static build(data: LoginPageDTO): LoginViewData {
static build(apiDto: LoginPageDTO): LoginViewData {
return {
returnTo: data.returnTo,
hasInsufficientPermissions: data.hasInsufficientPermissions,
returnTo: apiDto.returnTo,
hasInsufficientPermissions: apiDto.hasInsufficientPermissions,
showPassword: false,
showErrorDetails: false,
formState: {

View File

@@ -0,0 +1,39 @@
import type { ProfileLeaguesViewData } from '@/lib/view-data/ProfileLeaguesViewData';
interface ProfileLeaguesPageDto {
ownedLeagues: Array<{
leagueId: string;
name: string;
description: string;
membershipRole: 'owner' | 'admin' | 'steward' | 'member';
}>;
memberLeagues: Array<{
leagueId: string;
name: string;
description: string;
membershipRole: 'owner' | 'admin' | 'steward' | 'member';
}>;
}
/**
* ViewData Builder for Profile Leagues page
* Transforms Page DTO to ViewData for templates
*/
export class ProfileLeaguesViewDataBuilder {
static build(apiDto: ProfileLeaguesPageDto): ProfileLeaguesViewData {
return {
ownedLeagues: apiDto.ownedLeagues.map((league: { leagueId: string; name: string; description: string; membershipRole: 'owner' | 'admin' | 'steward' | 'member'; }) => ({
leagueId: league.leagueId,
name: league.name,
description: league.description,
membershipRole: league.membershipRole,
})),
memberLeagues: apiDto.memberLeagues.map((league: { leagueId: string; name: string; description: string; membershipRole: 'owner' | 'admin' | 'steward' | 'member'; }) => ({
leagueId: league.leagueId,
name: league.name,
description: league.description,
membershipRole: league.membershipRole,
})),
};
}
}

View File

@@ -0,0 +1,82 @@
import { RaceDetailViewData, RaceDetailRace, RaceDetailLeague, RaceDetailEntry, RaceDetailRegistration, RaceDetailUserResult } from '@/lib/view-data/races/RaceDetailViewData';
/**
* Race Detail View Data Builder
*
* Transforms API DTO into ViewData for the race detail template.
* Deterministic, side-effect free.
*/
export class RaceDetailViewDataBuilder {
static build(apiDto: any): RaceDetailViewData {
if (!apiDto || !apiDto.race) {
return {
race: {
id: '',
track: '',
car: '',
scheduledAt: '',
status: 'scheduled',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
}
const race: RaceDetailRace = {
id: apiDto.race.id,
track: apiDto.race.track,
car: apiDto.race.car,
scheduledAt: apiDto.race.scheduledAt,
status: apiDto.race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
sessionType: apiDto.race.sessionType,
};
const league: RaceDetailLeague | undefined = apiDto.league ? {
id: apiDto.league.id,
name: apiDto.league.name,
description: apiDto.league.description || undefined,
settings: {
maxDrivers: apiDto.league.settings?.maxDrivers || 32,
qualifyingFormat: apiDto.league.settings?.qualifyingFormat || 'Open',
},
} : undefined;
const entryList: RaceDetailEntry[] = apiDto.entryList.map((entry: any) => ({
id: entry.id,
name: entry.name,
avatarUrl: entry.avatarUrl,
country: entry.country,
rating: entry.rating,
isCurrentUser: entry.isCurrentUser,
}));
const registration: RaceDetailRegistration = {
isUserRegistered: apiDto.registration.isUserRegistered,
canRegister: apiDto.registration.canRegister,
};
const userResult: RaceDetailUserResult | undefined = apiDto.userResult ? {
position: apiDto.userResult.position,
startPosition: apiDto.userResult.startPosition,
positionChange: apiDto.userResult.positionChange,
incidents: apiDto.userResult.incidents,
isClean: apiDto.userResult.isClean,
isPodium: apiDto.userResult.isPodium,
ratingChange: apiDto.userResult.ratingChange,
} : undefined;
return {
race,
league,
entryList,
registration,
userResult,
canReopenRace: apiDto.canReopenRace || false,
};
}
}

View File

@@ -0,0 +1,59 @@
import { RaceResultsViewData, RaceResultsResult, RaceResultsPenalty } from '@/lib/view-data/races/RaceResultsViewData';
/**
* Race Results View Data Builder
*
* Transforms API DTO into ViewData for the race results template.
* Deterministic, side-effect free.
*/
export class RaceResultsViewDataBuilder {
static build(apiDto: any): RaceResultsViewData {
if (!apiDto) {
return {
raceSOF: null,
results: [],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
}
// Transform results
const results: RaceResultsResult[] = (apiDto.results || []).map((result: any) => ({
position: result.position,
driverId: result.driverId,
driverName: result.driverName,
driverAvatar: result.avatarUrl,
country: result.country || 'US',
car: result.car || 'Unknown',
laps: result.laps || 0,
time: result.time || '0:00.00',
fastestLap: result.fastestLap?.toString() || '0.00',
points: result.points || 0,
incidents: result.incidents || 0,
isCurrentUser: result.isCurrentUser || false,
}));
// Transform penalties
const penalties: RaceResultsPenalty[] = (apiDto.penalties || []).map((penalty: any) => ({
driverId: penalty.driverId,
driverName: penalty.driverName || 'Unknown',
type: penalty.type as 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points',
value: penalty.value || 0,
reason: penalty.reason || 'Penalty applied',
notes: penalty.notes,
}));
return {
raceTrack: apiDto.race?.track,
raceScheduledAt: apiDto.race?.scheduledAt,
totalDrivers: apiDto.stats?.totalDrivers,
leagueName: apiDto.league?.name,
raceSOF: apiDto.strengthOfField || null,
results,
penalties,
pointsSystem: apiDto.pointsSystem || {},
fastestLapTime: apiDto.fastestLapTime || 0,
};
}
}

View File

@@ -0,0 +1,86 @@
import { RaceStewardingViewData, Protest, Penalty, Driver } from '@/lib/view-data/races/RaceStewardingViewData';
/**
* Race Stewarding View Data Builder
*
* Transforms API DTO into ViewData for the race stewarding template.
* Deterministic, side-effect free.
*/
export class RaceStewardingViewDataBuilder {
static build(apiDto: any): RaceStewardingViewData {
if (!apiDto) {
return {
race: null,
league: null,
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
}
const race = apiDto.race ? {
id: apiDto.race.id,
track: apiDto.race.track,
scheduledAt: apiDto.race.scheduledAt,
} : null;
const league = apiDto.league ? {
id: apiDto.league.id,
} : null;
const pendingProtests: Protest[] = (apiDto.pendingProtests || []).map((p: any) => ({
id: p.id,
protestingDriverId: p.protestingDriverId,
accusedDriverId: p.accusedDriverId,
incident: {
lap: p.incident?.lap || 0,
description: p.incident?.description || '',
},
filedAt: p.filedAt,
status: p.status,
proofVideoUrl: p.proofVideoUrl,
decisionNotes: p.decisionNotes,
}));
const resolvedProtests: Protest[] = (apiDto.resolvedProtests || []).map((p: any) => ({
id: p.id,
protestingDriverId: p.protestingDriverId,
accusedDriverId: p.accusedDriverId,
incident: {
lap: p.incident?.lap || 0,
description: p.incident?.description || '',
},
filedAt: p.filedAt,
status: p.status,
proofVideoUrl: p.proofVideoUrl,
decisionNotes: p.decisionNotes,
}));
const penalties: Penalty[] = (apiDto.penalties || []).map((p: any) => ({
id: p.id,
driverId: p.driverId,
type: p.type,
value: p.value || 0,
reason: p.reason || '',
notes: p.notes,
}));
const driverMap: Record<string, Driver> = apiDto.driverMap || {};
return {
race,
league,
pendingProtests,
resolvedProtests,
penalties,
driverMap,
pendingCount: apiDto.pendingCount || pendingProtests.length,
resolvedCount: apiDto.resolvedCount || resolvedProtests.length,
penaltiesCount: apiDto.penaltiesCount || penalties.length,
};
}
}

View File

@@ -0,0 +1,27 @@
import { RacesAllViewData, RacesAllRace } from '@/lib/view-data/races/RacesAllViewData';
/**
* Races All View Data Builder
*
* Transforms API DTO into ViewData for the all races template.
* Deterministic, side-effect free.
*/
export class RacesAllViewDataBuilder {
static build(apiDto: any): RacesAllViewData {
const races = apiDto.races.map((race: any) => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
status: race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
sessionType: 'race',
leagueId: race.leagueId,
leagueName: race.leagueName,
strengthOfField: race.strengthOfField ?? undefined,
}));
return {
races,
};
}
}

View File

@@ -0,0 +1,39 @@
import { RacesViewData, RacesRace } from '@/lib/view-data/races/RacesViewData';
/**
* Races View Data Builder
*
* Transforms API DTO into ViewData for the races template.
* Deterministic, side-effect free.
*/
export class RacesViewDataBuilder {
static build(apiDto: any): RacesViewData {
const races = apiDto.races.map((race: any) => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
status: race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
sessionType: 'race',
leagueId: race.leagueId,
leagueName: race.leagueName,
strengthOfField: race.strengthOfField ?? undefined,
isUpcoming: race.status === 'scheduled',
isLive: race.status === 'running',
isPast: race.status === 'completed',
}));
const totalCount = races.length;
const scheduledRaces = races.filter((r: RacesRace) => r.isUpcoming);
const runningRaces = races.filter((r: RacesRace) => r.isLive);
const completedRaces = races.filter((r: RacesRace) => r.isPast);
return {
races,
totalCount,
scheduledRaces,
runningRaces,
completedRaces,
};
}
}

View File

@@ -1,27 +1,18 @@
/**
* Reset Password View Data Builder
*
*
* Transforms ResetPasswordPageDTO into ViewData for the reset password template.
* Deterministic, side-effect free, no business logic.
*/
import { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO';
export interface ResetPasswordViewData {
token: string;
returnTo: string;
showSuccess: boolean;
successMessage?: string;
formState: any; // Will be managed by client component
isSubmitting: boolean;
submitError?: string;
}
import { ResetPasswordViewData } from './types/ResetPasswordViewData';
export class ResetPasswordViewDataBuilder {
static build(data: ResetPasswordPageDTO): ResetPasswordViewData {
static build(apiDto: ResetPasswordPageDTO): ResetPasswordViewData {
return {
token: data.token,
returnTo: data.returnTo,
token: apiDto.token,
returnTo: apiDto.returnTo,
showSuccess: false,
formState: {
fields: {

View File

@@ -1,23 +1,17 @@
/**
* Signup View Data Builder
*
*
* Transforms SignupPageDTO into ViewData for the signup template.
* Deterministic, side-effect free, no business logic.
*/
import { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO';
export interface SignupViewData {
returnTo: string;
formState: any; // Will be managed by client component
isSubmitting: boolean;
submitError?: string;
}
import { SignupViewData } from './types/SignupViewData';
export class SignupViewDataBuilder {
static build(data: SignupPageDTO): SignupViewData {
static build(apiDto: SignupPageDTO): SignupViewData {
return {
returnTo: data.returnTo,
returnTo: apiDto.returnTo,
formState: {
fields: {
firstName: { value: '', error: undefined, touched: false, validating: false },

View File

@@ -0,0 +1,18 @@
/**
* SponsorLogoViewDataBuilder
*
* Transforms MediaBinaryDTO into SponsorLogoViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { SponsorLogoViewData } from '@/lib/view-data/SponsorLogoViewData';
export class SponsorLogoViewDataBuilder {
static build(apiDto: MediaBinaryDTO): SponsorLogoViewData {
return {
buffer: apiDto.buffer,
contentType: apiDto.contentType,
};
}
}

View File

@@ -1,16 +1,26 @@
import type { SponsorshipRequestDTO } from '@/lib/types/generated/SponsorshipRequestDTO';
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
import type { SponsorshipRequestsViewData } from '@/lib/view-data/SponsorshipRequestsViewData';
export interface SponsorshipRequestsViewData {
requests: SponsorshipRequestDTO[];
isEmpty: boolean;
}
/**
* ViewData Builder for Sponsorship Requests page
* Transforms API DTO to ViewData for templates
*/
export class SponsorshipRequestsPageViewDataBuilder {
build(queryResult: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData {
static build(apiDto: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData {
return {
requests: queryResult.requests,
isEmpty: queryResult.requests.length === 0,
sections: [{
entityType: apiDto.entityType as 'driver' | 'team' | 'season',
entityId: apiDto.entityId,
entityName: apiDto.entityType,
requests: apiDto.requests.map(request => ({
id: request.id,
sponsorId: request.sponsorId,
sponsorName: request.sponsorName,
sponsorLogoUrl: request.sponsorLogo || null,
message: request.message || null,
createdAtIso: request.createdAt,
})),
}],
};
}
}

View File

@@ -1,22 +1,24 @@
import type { SponsorshipRequestsPageDto } from '@/lib/page-queries/page-queries/SponsorshipRequestsPageQuery';
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
import type { SponsorshipRequestsViewData } from '@/lib/view-data/SponsorshipRequestsViewData';
export class SponsorshipRequestsViewDataBuilder {
static build(apiDto: SponsorshipRequestsPageDto): SponsorshipRequestsViewData {
static build(apiDto: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData {
return {
sections: apiDto.sections.map((section) => ({
entityType: section.entityType,
entityId: section.entityId,
entityName: section.entityName,
requests: section.requests.map((request) => ({
id: request.requestId,
sponsorId: request.sponsorId,
sponsorName: request.sponsorName,
sponsorLogoUrl: null,
message: request.message,
createdAtIso: request.createdAtIso,
})),
})),
sections: [
{
entityType: apiDto.entityType as 'driver' | 'team' | 'season',
entityId: apiDto.entityId,
entityName: apiDto.entityType === 'driver' ? 'Driver' : apiDto.entityType,
requests: apiDto.requests.map((request) => ({
id: request.id,
sponsorId: request.sponsorId,
sponsorName: request.sponsorName,
sponsorLogoUrl: request.sponsorLogo || null,
message: request.message || null,
createdAtIso: request.createdAt,
})),
},
],
};
}
}

View File

@@ -0,0 +1,86 @@
import type { TeamDetailPageDto } from '@/lib/page-queries/page-queries/TeamDetailPageQuery';
import type { TeamDetailViewData, TeamDetailData, TeamMemberData, SponsorMetric, TeamTab } from '@/lib/view-data/TeamDetailViewData';
import { Users, Zap, Calendar } from 'lucide-react';
/**
* TeamDetailViewDataBuilder - Transforms TeamDetailPageDto into ViewData
* Deterministic; side-effect free; no HTTP calls
*/
export class TeamDetailViewDataBuilder {
static build(apiDto: TeamDetailPageDto): TeamDetailViewData {
const team: TeamDetailData = {
id: apiDto.team.id,
name: apiDto.team.name,
tag: apiDto.team.tag,
description: apiDto.team.description,
ownerId: apiDto.team.ownerId,
leagues: apiDto.team.leagues,
createdAt: apiDto.team.createdAt,
specialization: apiDto.team.specialization,
region: apiDto.team.region,
languages: apiDto.team.languages,
category: apiDto.team.category,
membership: apiDto.team.membership,
canManage: apiDto.team.canManage,
};
const memberships: TeamMemberData[] = apiDto.memberships.map((membership) => ({
driverId: membership.driverId,
driverName: membership.driverName,
role: membership.role,
joinedAt: membership.joinedAt,
isActive: membership.isActive,
avatarUrl: membership.avatarUrl,
}));
// Calculate isAdmin based on current driver's role
const currentDriverMembership = memberships.find(m => m.driverId === apiDto.currentDriverId);
const isAdmin = currentDriverMembership?.role === 'owner' || currentDriverMembership?.role === 'manager';
// Build sponsor metrics
const leagueCount = team.leagues?.length ?? 0;
const teamMetrics: SponsorMetric[] = [
{
icon: Users,
label: 'Members',
value: memberships.length,
color: 'text-primary-blue',
},
{
icon: Zap,
label: 'Est. Reach',
value: memberships.length * 15,
color: 'text-purple-400',
},
{
icon: Calendar,
label: 'Races',
value: leagueCount,
color: 'text-neon-aqua',
},
{
icon: Users,
label: 'Engagement',
value: '82%',
color: 'text-performance-green',
},
];
// Build tabs
const tabs: TeamTab[] = [
{ id: 'overview', label: 'Overview', visible: true },
{ id: 'roster', label: 'Roster', visible: true },
{ id: 'standings', label: 'Standings', visible: true },
{ id: 'admin', label: 'Admin', visible: isAdmin },
];
return {
team,
memberships,
currentDriverId: apiDto.currentDriverId,
isAdmin,
teamMetrics,
tabs,
};
}
}

View File

@@ -0,0 +1,18 @@
/**
* TeamLogoViewDataBuilder
*
* Transforms MediaBinaryDTO into TeamLogoViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { TeamLogoViewData } from '@/lib/view-data/TeamLogoViewData';
export class TeamLogoViewDataBuilder {
static build(apiDto: MediaBinaryDTO): TeamLogoViewData {
return {
buffer: apiDto.buffer,
contentType: apiDto.contentType,
};
}
}

View File

@@ -0,0 +1,20 @@
import type { TeamsPageDto } from '@/lib/page-queries/page-queries/TeamsPageQuery';
import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData';
/**
* TeamsViewDataBuilder - Transforms TeamsPageDto into ViewData for TeamsTemplate
* Deterministic; side-effect free; no HTTP calls
*/
export class TeamsViewDataBuilder {
static build(apiDto: TeamsPageDto): TeamsViewData {
const teams: TeamSummaryData[] = apiDto.teams.map((team): TeamSummaryData => ({
teamId: team.id,
teamName: team.name,
leagueName: team.leagues[0] || '',
memberCount: team.memberCount,
logoUrl: team.logoUrl,
}));
return { teams };
}
}

View File

@@ -0,0 +1,18 @@
/**
* TrackImageViewDataBuilder
*
* Transforms MediaBinaryDTO into TrackImageViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { TrackImageViewData } from '@/lib/view-data/TrackImageViewData';
export class TrackImageViewDataBuilder {
static build(apiDto: MediaBinaryDTO): TrackImageViewData {
return {
buffer: apiDto.buffer,
contentType: apiDto.contentType,
};
}
}

View File

@@ -0,0 +1,15 @@
/**
* Forgot Password View Data
*
* ViewData for the forgot password template.
*/
export interface ForgotPasswordViewData {
returnTo: string;
showSuccess: boolean;
successMessage?: string;
magicLink?: string;
formState: any; // Will be managed by client component
isSubmitting: boolean;
submitError?: string;
}

View File

@@ -0,0 +1,12 @@
/**
* Form Field State
*
* State for a single form field.
*/
export interface FormFieldState {
value: string | boolean;
error?: string;
touched: boolean;
validating: boolean;
}

View File

@@ -0,0 +1,19 @@
/**
* Form State
*
* Complete state for a form.
*/
import { FormFieldState } from './FormFieldState';
export interface FormState {
fields: {
email: FormFieldState;
password: FormFieldState;
rememberMe: FormFieldState;
};
isValid: boolean;
isSubmitting: boolean;
submitError?: string;
submitCount: number;
}

View File

@@ -0,0 +1,17 @@
/**
* Login View Data
*
* ViewData for the login template.
*/
import { FormState } from './FormState';
export interface LoginViewData {
returnTo: string;
hasInsufficientPermissions: boolean;
showPassword: boolean;
showErrorDetails: boolean;
formState: FormState;
isSubmitting: boolean;
submitError?: string;
}

View File

@@ -0,0 +1,15 @@
/**
* Reset Password View Data
*
* ViewData for the reset password template.
*/
export interface ResetPasswordViewData {
token: string;
returnTo: string;
showSuccess: boolean;
successMessage?: string;
formState: any; // Will be managed by client component
isSubmitting: boolean;
submitError?: string;
}

View File

@@ -0,0 +1,12 @@
/**
* Signup View Data
*
* ViewData for the signup template.
*/
export interface SignupViewData {
returnTo: string;
formState: any; // Will be managed by client component
isSubmitting: boolean;
submitError?: string;
}