website refactor

This commit is contained in:
2026-01-12 01:01:49 +01:00
parent 5ca6023a5a
commit fefd8d1cd6
294 changed files with 4628 additions and 4991 deletions

View File

@@ -0,0 +1,47 @@
'use client';
import type { UserDto, DashboardStats, UserListResponse } from '@/lib/api/admin/AdminApiClient';
import { AdminUserViewModel, DashboardStatsViewModel, UserListViewModel } from './AdminUserViewModel';
/**
* AdminViewModelPresenter
*
* Presenter layer for transforming API DTOs to ViewModels.
* Runs in client code only ('use client').
* Deterministic, side-effect free transformations.
*/
export class AdminViewModelPresenter {
/**
* Map a single user DTO to a View Model
*/
static mapUser(dto: UserDto): AdminUserViewModel {
return new AdminUserViewModel(dto);
}
/**
* Map an array of user DTOs to View Models
*/
static mapUsers(dtos: UserDto[]): AdminUserViewModel[] {
return dtos.map(dto => this.mapUser(dto));
}
/**
* Map dashboard stats DTO to View Model
*/
static mapDashboardStats(dto: DashboardStats): DashboardStatsViewModel {
return new DashboardStatsViewModel(dto);
}
/**
* Map user list response to View Model
*/
static mapUserList(response: UserListResponse): UserListViewModel {
return new UserListViewModel({
users: response.users,
total: response.total,
page: response.page,
limit: response.limit,
totalPages: response.totalPages,
});
}
}

View File

@@ -0,0 +1,91 @@
import type { DashboardPageDto } from '@/lib/page-queries/DashboardPageDto';
import type { DashboardViewData } from '@/templates/DashboardViewData';
import {
formatDashboardDate,
formatRating,
formatRank,
formatConsistency,
formatRaceCount,
formatFriendCount,
formatLeaguePosition,
formatPoints,
formatTotalDrivers,
} from '@/lib/display-objects/DashboardDisplay';
/**
* DashboardPresenter - Client-side presenter for dashboard page
* Transforms Page DTO into ViewData for the template
* Deterministic; no hooks; no side effects
*/
export class DashboardPresenter {
static createViewData(pageDto: DashboardPageDto): DashboardViewData {
return {
currentDriver: {
name: pageDto.currentDriver?.name || '',
avatarUrl: pageDto.currentDriver?.avatarUrl || '',
country: pageDto.currentDriver?.country || '',
rating: pageDto.currentDriver ? formatRating(pageDto.currentDriver.rating) : '0.0',
rank: pageDto.currentDriver ? formatRank(pageDto.currentDriver.globalRank) : '0',
totalRaces: pageDto.currentDriver ? formatRaceCount(pageDto.currentDriver.totalRaces) : '0',
wins: pageDto.currentDriver ? formatRaceCount(pageDto.currentDriver.wins) : '0',
podiums: pageDto.currentDriver ? formatRaceCount(pageDto.currentDriver.podiums) : '0',
consistency: pageDto.currentDriver ? formatConsistency(pageDto.currentDriver.consistency) : '0%',
},
nextRace: pageDto.nextRace ? (() => {
const dateInfo = formatDashboardDate(new Date(pageDto.nextRace.scheduledAt));
return {
id: pageDto.nextRace.id,
track: pageDto.nextRace.track,
car: pageDto.nextRace.car,
scheduledAt: pageDto.nextRace.scheduledAt,
formattedDate: dateInfo.date,
formattedTime: dateInfo.time,
timeUntil: dateInfo.relative,
isMyLeague: pageDto.nextRace.isMyLeague,
};
})() : null,
upcomingRaces: pageDto.upcomingRaces.map((race) => {
const dateInfo = formatDashboardDate(new Date(race.scheduledAt));
return {
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
formattedDate: dateInfo.date,
formattedTime: dateInfo.time,
timeUntil: dateInfo.relative,
isMyLeague: race.isMyLeague,
};
}),
leagueStandings: pageDto.leagueStandingsSummaries.map((standing) => ({
leagueId: standing.leagueId,
leagueName: standing.leagueName,
position: formatLeaguePosition(standing.position),
points: formatPoints(standing.points),
totalDrivers: formatTotalDrivers(standing.totalDrivers),
})),
feedItems: pageDto.feedSummary.items.map((item) => ({
id: item.id,
type: item.type,
headline: item.headline,
body: item.body,
timestamp: item.timestamp,
formattedTime: formatDashboardDate(new Date(item.timestamp)).relative,
ctaHref: item.ctaHref,
ctaLabel: item.ctaLabel,
})),
friends: pageDto.friends.map((friend) => ({
id: friend.id,
name: friend.name,
avatarUrl: friend.avatarUrl,
country: friend.country,
})),
activeLeaguesCount: formatRaceCount(pageDto.activeLeaguesCount),
friendCount: formatFriendCount(pageDto.friends.length),
hasUpcomingRaces: pageDto.upcomingRaces.length > 0,
hasLeagueStandings: pageDto.leagueStandingsSummaries.length > 0,
hasFeedItems: pageDto.feedSummary.items.length > 0,
hasFriends: pageDto.friends.length > 0,
};
}
}

View File

@@ -0,0 +1,25 @@
import type { ProfileLeaguesPageDto } from '@/lib/page-queries/page-queries/ProfileLeaguesPageQuery';
import type { ProfileLeaguesViewData } from '@/templates/view-data/ProfileLeaguesViewData';
/**
* Presenter for Profile Leagues page
* Pure mapping from Page DTO to ViewData
*/
export class ProfileLeaguesPresenter {
static toViewData(pageDto: ProfileLeaguesPageDto): ProfileLeaguesViewData {
return {
ownedLeagues: pageDto.ownedLeagues.map(league => ({
leagueId: league.leagueId,
name: league.name,
description: league.description,
membershipRole: league.membershipRole,
})),
memberLeagues: pageDto.memberLeagues.map(league => ({
leagueId: league.leagueId,
name: league.name,
description: league.description,
membershipRole: league.membershipRole,
})),
};
}
}

View File

@@ -0,0 +1,47 @@
import type { TeamDetailPageDto } from '@/lib/page-queries/TeamDetailPageQuery';
import type { TeamDetailViewData, TeamDetailData, TeamMemberData } from '@/templates/TeamDetailViewData';
/**
* TeamDetailPresenter - Client-side presenter for team detail page
* Transforms PageQuery DTO into ViewData for the template
* Deterministic; no hooks; no side effects
*/
export class TeamDetailPresenter {
static createViewData(pageDto: TeamDetailPageDto): TeamDetailViewData {
const team: TeamDetailData = {
id: pageDto.team.id,
name: pageDto.team.name,
tag: pageDto.team.tag,
description: pageDto.team.description,
ownerId: pageDto.team.ownerId,
leagues: pageDto.team.leagues,
createdAt: pageDto.team.createdAt,
specialization: pageDto.team.specialization,
region: pageDto.team.region,
languages: pageDto.team.languages,
category: pageDto.team.category,
membership: pageDto.team.membership,
canManage: pageDto.team.canManage,
};
const memberships: TeamMemberData[] = pageDto.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 === pageDto.currentDriverId);
const isAdmin = currentDriverMembership?.role === 'owner' || currentDriverMembership?.role === 'manager';
return {
team,
memberships,
currentDriverId: pageDto.currentDriverId,
isAdmin,
};
}
}

View File

@@ -0,0 +1,21 @@
import type { TeamsPageDto } from '@/lib/page-queries/TeamsPageQuery';
import type { TeamsViewData, TeamSummaryData } from '@/templates/TeamsViewData';
/**
* TeamsPresenter - Client-side presenter for teams page
* Transforms PageQuery DTO into ViewData for the template
* Deterministic; no hooks; no side effects
*/
export class TeamsPresenter {
static createViewData(pageDto: TeamsPageDto): TeamsViewData {
const teams = pageDto.teams.map((team): TeamSummaryData => ({
teamId: team.id,
teamName: team.name,
leagueName: team.leagues[0] || '',
memberCount: team.memberCount,
logoUrl: team.logoUrl,
}));
return { teams };
}
}