website refactor
This commit is contained in:
47
apps/website/lib/presenters/AdminViewModelPresenter.ts
Normal file
47
apps/website/lib/presenters/AdminViewModelPresenter.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
91
apps/website/lib/presenters/DashboardPresenter.ts
Normal file
91
apps/website/lib/presenters/DashboardPresenter.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
25
apps/website/lib/presenters/ProfileLeaguesPresenter.ts
Normal file
25
apps/website/lib/presenters/ProfileLeaguesPresenter.ts
Normal 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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
47
apps/website/lib/presenters/TeamDetailPresenter.ts
Normal file
47
apps/website/lib/presenters/TeamDetailPresenter.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
21
apps/website/lib/presenters/TeamsPresenter.ts
Normal file
21
apps/website/lib/presenters/TeamsPresenter.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user