website refactor

This commit is contained in:
2026-01-16 01:00:03 +01:00
parent ce7be39155
commit a98e3e3166
286 changed files with 5522 additions and 5261 deletions

View File

@@ -13,32 +13,22 @@ import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-
*
* Follows Clean Architecture: Uses builders for transformation.
*/
export class AdminDashboardPageQuery implements PageQuery<AdminDashboardViewData, void> {
export class AdminDashboardPageQuery implements PageQuery<AdminDashboardViewData, void, PresentationError> {
async execute(): Promise<Result<AdminDashboardViewData, PresentationError>> {
try {
// Manual construction: Service creates its own dependencies
const adminService = new AdminService();
// Manual construction: Service creates its own dependencies
const adminService = new AdminService();
// Fetch dashboard stats
const apiDtoResult = await adminService.getDashboardStats();
// Fetch dashboard stats
const apiDtoResult = await adminService.getDashboardStats();
if (apiDtoResult.isErr()) {
return Result.err(mapToPresentationError(apiDtoResult.getError()));
}
// Transform to ViewData using builder
const output = AdminDashboardViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(output);
} catch (err) {
console.error('AdminDashboardPageQuery failed:', err);
if (err instanceof Error && (err.message.includes('403') || err.message.includes('401'))) {
return Result.err('notFound');
}
return Result.err('serverError');
if (apiDtoResult.isErr()) {
return Result.err(mapToPresentationError(apiDtoResult.getError()));
}
// Transform to ViewData using builder
const output = AdminDashboardViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(output);
}
// Static method to avoid object construction in server code
@@ -46,4 +36,4 @@ export class AdminDashboardPageQuery implements PageQuery<AdminDashboardViewData
const query = new AdminDashboardPageQuery();
return query.execute();
}
}
}

View File

@@ -9,8 +9,8 @@ import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
*
* Fetches data needed for the create league page.
*/
export class CreateLeaguePageQuery implements PageQuery<any, void> {
async execute(): Promise<Result<any, 'notFound' | 'redirect' | 'CREATE_LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
export class CreateLeaguePageQuery implements PageQuery<unknown, void> {
async execute(): Promise<Result<unknown, 'notFound' | 'redirect' | 'CREATE_LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
// Manual wiring: create API client
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const errorReporter = new ConsoleErrorReporter();
@@ -43,7 +43,7 @@ export class CreateLeaguePageQuery implements PageQuery<any, void> {
}
}
static async execute(): Promise<Result<any, 'notFound' | 'redirect' | 'CREATE_LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
static async execute(): Promise<Result<unknown, 'notFound' | 'redirect' | 'CREATE_LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new CreateLeaguePageQuery();
return query.execute();
}

View File

@@ -0,0 +1,28 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeagueService } from '@/lib/services/leagues/LeagueService';
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
/**
* LeagueDetail page query
* Returns the raw API DTO for the league detail page
* No DI container usage - constructs dependencies explicitly
*/
export class LeagueDetailPageQuery implements PageQuery<unknown, string, PresentationError> {
async execute(leagueId: string): Promise<Result<unknown, PresentationError>> {
const service = new LeagueService();
const result = await service.getLeagueDetailData(leagueId);
if (result.isErr()) {
return Result.err(mapToPresentationError(result.getError()));
}
return Result.ok(result.unwrap());
}
// Static method to avoid object construction in server code
static async execute(leagueId: string): Promise<Result<unknown, PresentationError>> {
const query = new LeagueDetailPageQuery();
return query.execute(leagueId);
}
}

View File

@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
import { ProtestDetailService } from '@/lib/services/leagues/ProtestDetailService';
import { ProtestDetailViewDataBuilder } from '@/lib/builders/view-data/ProtestDetailViewDataBuilder';
import { ProtestDetailViewData } from '@/lib/view-data/leagues/ProtestDetailViewData';
interface PresentationError {
type: 'notFound' | 'forbidden' | 'serverError';
message: string;
}
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class LeagueProtestDetailPageQuery implements PageQuery<ProtestDetailViewData, { leagueId: string; protestId: string }, PresentationError> {
async execute(params: { leagueId: string; protestId: string }): Promise<Result<ProtestDetailViewData, PresentationError>> {
@@ -15,7 +11,7 @@ export class LeagueProtestDetailPageQuery implements PageQuery<ProtestDetailView
const result = await service.getProtestDetail(params.leagueId, params.protestId);
if (result.isErr()) {
return Result.err({ type: 'serverError', message: 'Failed to load protest details' });
return Result.err(mapToPresentationError(result.getError()));
}
const viewData = ProtestDetailViewDataBuilder.build(result.unwrap());

View File

@@ -10,14 +10,14 @@ import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
*
* Fetches protest detail data for review.
*/
export class LeagueProtestReviewPageQuery implements PageQuery<any, { leagueId: string; protestId: string }> {
async execute(input: { leagueId: string; protestId: string }): Promise<Result<any, 'notFound' | 'redirect' | 'PROTEST_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
export class LeagueProtestReviewPageQuery implements PageQuery<unknown, { leagueId: string; protestId: string }> {
async execute(input: { leagueId: string; protestId: string }): Promise<Result<unknown, 'notFound' | 'redirect' | 'PROTEST_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
// Manual wiring: create API clients
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const leaguesApiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
const protestsApiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
new LeaguesApiClient(baseUrl, errorReporter, logger);
new ProtestsApiClient(baseUrl, errorReporter, logger);
try {
// Get protest details
@@ -70,7 +70,7 @@ export class LeagueProtestReviewPageQuery implements PageQuery<any, { leagueId:
}
}
static async execute(input: { leagueId: string; protestId: string }): Promise<Result<any, 'notFound' | 'redirect' | 'PROTEST_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
static async execute(input: { leagueId: string; protestId: string }): Promise<Result<unknown, 'notFound' | 'redirect' | 'PROTEST_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueProtestReviewPageQuery();
return query.execute(input);
}

View File

@@ -0,0 +1,29 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeagueService } from '@/lib/services/leagues/LeagueService';
import { LeagueRosterAdminViewDataBuilder } from '@/lib/builders/view-data/LeagueRosterAdminViewDataBuilder';
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
/**
* LeagueRosterAdminPageQuery
*
* Fetches league roster admin data (members and join requests).
*/
export class LeagueRosterAdminPageQuery implements PageQuery<unknown, string, PresentationError> {
async execute(leagueId: string): Promise<Result<unknown, PresentationError>> {
const service = new LeagueService();
const result = await service.getRosterAdminData(leagueId);
if (result.isErr()) {
return Result.err(mapToPresentationError(result.getError()));
}
const viewData = LeagueRosterAdminViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
static async execute(leagueId: string): Promise<Result<unknown, PresentationError>> {
const query = new LeagueRosterAdminPageQuery();
return query.execute(leagueId);
}
}

View File

@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
import { LeagueRulebookService } from '@/lib/services/leagues/LeagueRulebookService';
import { RulebookViewDataBuilder } from '@/lib/builders/view-data/RulebookViewDataBuilder';
import { RulebookViewData } from '@/lib/view-data/leagues/RulebookViewData';
interface PresentationError {
type: 'notFound' | 'forbidden' | 'serverError';
message: string;
}
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class LeagueRulebookPageQuery implements PageQuery<RulebookViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<RulebookViewData, PresentationError>> {
@@ -15,7 +11,7 @@ export class LeagueRulebookPageQuery implements PageQuery<RulebookViewData, stri
const result = await service.getRulebookData(leagueId);
if (result.isErr()) {
return Result.err({ type: 'serverError', message: 'Failed to load rulebook data' });
return Result.err(mapToPresentationError(result.getError()));
}
const viewData = RulebookViewDataBuilder.build(result.unwrap());
@@ -26,4 +22,4 @@ export class LeagueRulebookPageQuery implements PageQuery<RulebookViewData, stri
const query = new LeagueRulebookPageQuery();
return query.execute(leagueId);
}
}
}

View File

@@ -0,0 +1,41 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeagueService } from '@/lib/services/leagues/LeagueService';
import { LeagueScheduleViewDataBuilder } from '@/lib/builders/view-data/LeagueScheduleViewDataBuilder';
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
/**
* LeagueScheduleAdminPageQuery
*
* Fetches league schedule admin data.
*/
export class LeagueScheduleAdminPageQuery implements PageQuery<unknown, { leagueId: string; seasonId?: string }> {
async execute(input: { leagueId: string; seasonId?: string }): Promise<Result<unknown, PresentationError>> {
const service = new LeagueService();
const result = await service.getScheduleAdminData(input.leagueId, input.seasonId);
if (result.isErr()) {
return Result.err(mapToPresentationError(result.getError()));
}
const data = result.unwrap();
const viewData = LeagueScheduleViewDataBuilder.build({
leagueId: data.leagueId,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
races: data.schedule.races.map((r: any) => ({
id: r.id,
name: r.name,
date: r.scheduledAt,
track: r.track,
car: r.car,
sessionType: r.sessionType,
})),
});
return Result.ok(viewData);
}
static async execute(input: { leagueId: string; seasonId?: string }): Promise<Result<unknown, PresentationError>> {
const query = new LeagueScheduleAdminPageQuery();
return query.execute(input);
}
}

View File

@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
import { LeagueScheduleService } from '@/lib/services/leagues/LeagueScheduleService';
import { LeagueScheduleViewDataBuilder } from '@/lib/builders/view-data/LeagueScheduleViewDataBuilder';
import { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData';
interface PresentationError {
type: 'notFound' | 'forbidden' | 'serverError';
message: string;
}
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class LeagueSchedulePageQuery implements PageQuery<LeagueScheduleViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<LeagueScheduleViewData, PresentationError>> {
@@ -15,7 +11,7 @@ export class LeagueSchedulePageQuery implements PageQuery<LeagueScheduleViewData
const result = await service.getScheduleData(leagueId);
if (result.isErr()) {
return Result.err({ type: 'serverError', message: 'Failed to load schedule data' });
return Result.err(mapToPresentationError(result.getError()));
}
const viewData = LeagueScheduleViewDataBuilder.build(result.unwrap());
@@ -26,4 +22,4 @@ export class LeagueSchedulePageQuery implements PageQuery<LeagueScheduleViewData
const query = new LeagueSchedulePageQuery();
return query.execute(leagueId);
}
}
}

View File

@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
import { LeagueSettingsService } from '@/lib/services/leagues/LeagueSettingsService';
import { LeagueSettingsViewDataBuilder } from '@/lib/builders/view-data/LeagueSettingsViewDataBuilder';
import { LeagueSettingsViewData } from '@/lib/view-data/leagues/LeagueSettingsViewData';
interface PresentationError {
type: 'notFound' | 'forbidden' | 'serverError';
message: string;
}
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class LeagueSettingsPageQuery implements PageQuery<LeagueSettingsViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<LeagueSettingsViewData, PresentationError>> {
@@ -15,7 +11,7 @@ export class LeagueSettingsPageQuery implements PageQuery<LeagueSettingsViewData
const result = await service.getSettingsData(leagueId);
if (result.isErr()) {
return Result.err({ type: 'serverError', message: 'Failed to load settings data' });
return Result.err(mapToPresentationError(result.getError()));
}
const viewData = LeagueSettingsViewDataBuilder.build(result.unwrap());
@@ -26,4 +22,4 @@ export class LeagueSettingsPageQuery implements PageQuery<LeagueSettingsViewData
const query = new LeagueSettingsPageQuery();
return query.execute(leagueId);
}
}
}

View File

@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
import { LeagueSponsorshipsService } from '@/lib/services/leagues/LeagueSponsorshipsService';
import { LeagueSponsorshipsViewDataBuilder } from '@/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder';
import { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData';
interface PresentationError {
type: 'notFound' | 'forbidden' | 'serverError';
message: string;
}
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class LeagueSponsorshipsPageQuery implements PageQuery<LeagueSponsorshipsViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<LeagueSponsorshipsViewData, PresentationError>> {
@@ -15,7 +11,7 @@ export class LeagueSponsorshipsPageQuery implements PageQuery<LeagueSponsorships
const result = await service.getSponsorshipsData(leagueId);
if (result.isErr()) {
return Result.err({ type: 'serverError', message: 'Failed to load sponsorships data' });
return Result.err(mapToPresentationError(result.getError()));
}
const viewData = LeagueSponsorshipsViewDataBuilder.build(result.unwrap());
@@ -26,4 +22,4 @@ export class LeagueSponsorshipsPageQuery implements PageQuery<LeagueSponsorships
const query = new LeagueSponsorshipsPageQuery();
return query.execute(leagueId);
}
}
}

View File

@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
import { LeagueStandingsService } from '@/lib/services/leagues/LeagueStandingsService';
import { LeagueStandingsViewDataBuilder } from '@/lib/builders/view-data/LeagueStandingsViewDataBuilder';
import { LeagueStandingsViewData } from '@/lib/view-data/LeagueStandingsViewData';
interface PresentationError {
type: 'notFound' | 'forbidden' | 'serverError';
message: string;
}
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class LeagueStandingsPageQuery implements PageQuery<LeagueStandingsViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<LeagueStandingsViewData, PresentationError>> {
@@ -15,7 +11,7 @@ export class LeagueStandingsPageQuery implements PageQuery<LeagueStandingsViewDa
const result = await service.getStandingsData(leagueId);
if (result.isErr()) {
return Result.err({ type: 'serverError', message: 'Failed to load standings data' });
return Result.err(mapToPresentationError(result.getError()));
}
const { standings, memberships } = result.unwrap();
@@ -27,4 +23,4 @@ export class LeagueStandingsPageQuery implements PageQuery<LeagueStandingsViewDa
const query = new LeagueStandingsPageQuery();
return query.execute(leagueId);
}
}
}

View File

@@ -3,11 +3,7 @@ import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { LeagueStewardingService } from '@/lib/services/leagues/LeagueStewardingService';
import { StewardingViewDataBuilder } from '@/lib/builders/view-data/StewardingViewDataBuilder';
import { StewardingViewData } from '@/lib/view-data/leagues/StewardingViewData';
interface PresentationError {
type: 'notFound' | 'forbidden' | 'notImplemented' | 'serverError';
message: string;
}
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class LeagueStewardingPageQuery implements PageQuery<StewardingViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<StewardingViewData, PresentationError>> {
@@ -16,7 +12,7 @@ export class LeagueStewardingPageQuery implements PageQuery<StewardingViewData,
if (result.isErr()) {
// Map domain errors to presentation errors
return Result.err({ type: 'serverError', message: 'Failed to load stewarding data' });
return Result.err(mapToPresentationError(result.getError()));
}
const viewData = StewardingViewDataBuilder.build(result.unwrap());
@@ -28,4 +24,4 @@ export class LeagueStewardingPageQuery implements PageQuery<StewardingViewData,
const query = new LeagueStewardingPageQuery();
return query.execute(leagueId);
}
}
}

View File

@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
import { LeagueWalletService } from '@/lib/services/leagues/LeagueWalletService';
import { LeagueWalletViewDataBuilder } from '@/lib/builders/view-data/LeagueWalletViewDataBuilder';
import { LeagueWalletViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
interface PresentationError {
type: 'notFound' | 'forbidden' | 'serverError';
message: string;
}
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class LeagueWalletPageQuery implements PageQuery<LeagueWalletViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<LeagueWalletViewData, PresentationError>> {
@@ -15,7 +11,7 @@ export class LeagueWalletPageQuery implements PageQuery<LeagueWalletViewData, st
const result = await service.getWalletData(leagueId);
if (result.isErr()) {
return Result.err({ type: 'serverError', message: 'Failed to load wallet data' });
return Result.err(mapToPresentationError(result.getError()));
}
const viewData = LeagueWalletViewDataBuilder.build(result.unwrap());
@@ -26,4 +22,4 @@ export class LeagueWalletPageQuery implements PageQuery<LeagueWalletViewData, st
const query = new LeagueWalletPageQuery();
return query.execute(leagueId);
}
}
}

View File

@@ -3,6 +3,7 @@ import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { SponsorService } from '@/lib/services/sponsors/SponsorService';
import { SponsorDashboardViewDataBuilder } from '@/lib/builders/view-data/SponsorDashboardViewDataBuilder';
import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardViewData';
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
/**
* Sponsor Dashboard Page Query
@@ -10,13 +11,13 @@ import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardV
* Composes data for the sponsor dashboard page.
* Maps domain errors to presentation errors.
*/
export class SponsorDashboardPageQuery implements PageQuery<SponsorDashboardViewData, string> {
async execute(sponsorId: string): Promise<Result<SponsorDashboardViewData, string>> {
export class SponsorDashboardPageQuery implements PageQuery<SponsorDashboardViewData, string, PresentationError> {
async execute(sponsorId: string): Promise<Result<SponsorDashboardViewData, PresentationError>> {
const service = new SponsorService();
const dashboardResult = await service.getSponsorDashboard(sponsorId);
if (dashboardResult.isErr()) {
return Result.err(this.mapToPresentationError(dashboardResult.getError()));
return Result.err(mapToPresentationError(dashboardResult.getError()));
}
const dto = dashboardResult.unwrap();
@@ -25,14 +26,8 @@ export class SponsorDashboardPageQuery implements PageQuery<SponsorDashboardView
return Result.ok(viewData);
}
private mapToPresentationError(domainError: { type: string }): string {
switch (domainError.type) {
case 'notFound':
return 'Dashboard not found';
case 'notImplemented':
return 'Dashboard feature not yet implemented';
default:
return 'Failed to load dashboard';
}
static async execute(sponsorId: string): Promise<Result<SponsorDashboardViewData, PresentationError>> {
const query = new SponsorDashboardPageQuery();
return query.execute(sponsorId);
}
}
}

View File

@@ -0,0 +1,119 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { TeamService } from '@/lib/services/teams/TeamService';
import { TeamDetailViewDataBuilder } from '@/lib/builders/view-data/TeamDetailViewDataBuilder';
import type { TeamDetailViewData } from '@/lib/view-data/TeamDetailViewData';
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { SessionGateway } from '@/lib/gateways/SessionGateway';
import type { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
/**
* TeamDetailPageDto - Raw serializable data for team detail page
* Contains only raw data, no derived/computed properties
*/
export interface TeamDetailPageDto {
team: {
id: string;
name: string;
tag: string;
description?: string;
ownerId: string;
leagues: string[];
createdAt?: string;
specialization?: string;
region?: string;
languages?: string[];
category?: string;
membership?: {
role: string;
joinedAt: string;
isActive: boolean;
} | null;
canManage: boolean;
};
memberships: Array<{
driverId: string;
driverName: string;
role: 'owner' | 'manager' | 'member';
joinedAt: string;
isActive: boolean;
avatarUrl: string;
}>;
currentDriverId: string;
}
/**
* TeamDetailPageQuery - Server-side composition for team detail page
*/
export class TeamDetailPageQuery implements PageQuery<TeamDetailViewData, string> {
async execute(teamId: string): Promise<Result<TeamDetailViewData, PresentationError>> {
// Get session to determine current driver
const sessionGateway = new SessionGateway();
const session = await sessionGateway.getSession();
if (!session?.user?.primaryDriverId) {
return Result.err('notFound');
}
const currentDriverId = session.user.primaryDriverId;
const service = new TeamService();
// Fetch team details
const teamResult = await service.getTeamDetails(teamId, currentDriverId);
if (teamResult.isErr()) {
return Result.err(mapToPresentationError(teamResult.getError()));
}
const teamData = teamResult.unwrap();
// Fetch team members
const membersResult = await service.getTeamMembers(teamId, currentDriverId, teamData.ownerId);
if (membersResult.isErr()) {
return Result.err(mapToPresentationError(membersResult.getError()));
}
const membersData = membersResult.unwrap();
// Transform to raw serializable DTO
const dto: TeamDetailPageDto = {
team: {
id: teamData.id,
name: teamData.name,
tag: teamData.tag,
description: teamData.description,
ownerId: teamData.ownerId,
leagues: teamData.leagues,
createdAt: teamData.createdAt,
specialization: teamData.specialization,
region: teamData.region,
languages: teamData.languages,
category: teamData.category,
membership: teamData.membership ? {
role: teamData.membership.role,
joinedAt: teamData.membership.joinedAt,
isActive: teamData.membership.isActive,
} : null,
canManage: teamData.canManage,
},
memberships: membersData.map((member: TeamMemberViewModel) => ({
driverId: member.driverId,
driverName: member.driverName,
role: member.role,
joinedAt: member.joinedAt,
isActive: member.isActive,
avatarUrl: member.avatarUrl,
})),
currentDriverId,
};
const viewData = TeamDetailViewDataBuilder.build(dto);
return Result.ok(viewData);
}
static async execute(teamId: string): Promise<Result<TeamDetailViewData, PresentationError>> {
const query = new TeamDetailPageQuery();
return query.execute(teamId);
}
}

View File

@@ -4,6 +4,11 @@ import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-
import { TeamService } from '@/lib/services/teams/TeamService';
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
import { TeamsViewDataBuilder } from '@/lib/builders/view-data/TeamsViewDataBuilder';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
export interface TeamsPageDto {
teams: TeamListItemDTO[];
}
/**
* TeamsPageQuery - Server-side composition for teams list page

View File

@@ -1,10 +1,3 @@
/**
* Forgot Password Page Query
*
* Composes data for the forgot password page using RSC pattern.
* No business logic, only data composition.
*/
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { ForgotPasswordViewDataBuilder } from '@/lib/builders/view-data/ForgotPasswordViewDataBuilder';
@@ -28,14 +21,14 @@ export class ForgotPasswordPageQuery implements PageQuery<ForgotPasswordViewData
const serviceResult = await authService.processForgotPasswordParams({ returnTo, token });
if (serviceResult.isErr()) {
return Result.err(serviceResult.getError());
return Result.err(serviceResult.getError().message);
}
// Transform to ViewData using builder
const viewData = ForgotPasswordViewDataBuilder.build(serviceResult.unwrap());
return Result.ok(viewData);
} catch (error) {
return Result.err('Failed to execute forgot password page query');
} catch (error: any) {
return Result.err(error.message || 'Failed to execute forgot password page query');
}
}
@@ -44,4 +37,4 @@ export class ForgotPasswordPageQuery implements PageQuery<ForgotPasswordViewData
const query = new ForgotPasswordPageQuery();
return query.execute(searchParams);
}
}
}

View File

@@ -1,10 +1,3 @@
/**
* Login Page Query
*
* Composes data for the login page using RSC pattern.
* No business logic, only data composition.
*/
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { LoginViewDataBuilder } from '@/lib/builders/view-data/LoginViewDataBuilder';
@@ -28,14 +21,14 @@ export class LoginPageQuery implements PageQuery<LoginViewData, URLSearchParams>
const serviceResult = await authService.processLoginParams({ returnTo, token });
if (serviceResult.isErr()) {
return Result.err(serviceResult.getError());
return Result.err(serviceResult.getError().message);
}
// Transform to ViewData using builder
const viewData = LoginViewDataBuilder.build(serviceResult.unwrap());
return Result.ok(viewData);
} catch (error) {
return Result.err('Failed to execute login page query');
} catch (error: any) {
return Result.err(error.message || 'Failed to execute login page query');
}
}
@@ -44,4 +37,4 @@ export class LoginPageQuery implements PageQuery<LoginViewData, URLSearchParams>
const query = new LoginPageQuery();
return query.execute(searchParams);
}
}
}

View File

@@ -1,10 +1,3 @@
/**
* Reset Password Page Query
*
* Composes data for the reset password page using RSC pattern.
* No business logic, only data composition.
*/
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { ResetPasswordViewDataBuilder } from '@/lib/builders/view-data/ResetPasswordViewDataBuilder';
@@ -28,14 +21,14 @@ export class ResetPasswordPageQuery implements PageQuery<ResetPasswordViewData,
const serviceResult = await authService.processResetPasswordParams({ returnTo, token });
if (serviceResult.isErr()) {
return Result.err(serviceResult.getError());
return Result.err(serviceResult.getError().message);
}
// Transform to ViewData using builder
const viewData = ResetPasswordViewDataBuilder.build(serviceResult.unwrap());
return Result.ok(viewData);
} catch (error) {
return Result.err('Failed to execute reset password page query');
} catch (error: any) {
return Result.err(error.message || 'Failed to execute reset password page query');
}
}
@@ -44,4 +37,4 @@ export class ResetPasswordPageQuery implements PageQuery<ResetPasswordViewData,
const query = new ResetPasswordPageQuery();
return query.execute(searchParams);
}
}
}

View File

@@ -1,10 +1,3 @@
/**
* Signup Page Query
*
* Composes data for the signup page using RSC pattern.
* No business logic, only data composition.
*/
import { SignupViewDataBuilder } from '@/lib/builders/view-data/SignupViewDataBuilder';
import { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData';
import { Result } from '@/lib/contracts/Result';
@@ -28,14 +21,14 @@ export class SignupPageQuery implements PageQuery<SignupViewData, URLSearchParam
const serviceResult = await authService.processSignupParams({ returnTo, token });
if (serviceResult.isErr()) {
return Result.err(serviceResult.getError());
return Result.err(serviceResult.getError().message);
}
// Transform to ViewData using builder
const viewData = SignupViewDataBuilder.build(serviceResult.unwrap());
return Result.ok(viewData);
} catch (error) {
return Result.err('Failed to execute signup page query');
} catch (error: any) {
return Result.err(error.message || 'Failed to execute signup page query');
}
}
@@ -44,4 +37,4 @@ export class SignupPageQuery implements PageQuery<SignupViewData, URLSearchParam
const query = new SignupPageQuery();
return query.execute(searchParams);
}
}
}

View File

@@ -1,10 +0,0 @@
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
/**
* DriverRankingsPageDto - Raw data structure for Driver Rankings page
* Plain data, no methods, no business logic
*/
export interface DriverRankingsPageDto {
drivers: DriverLeaderboardItemDTO[];
}

View File

@@ -1,7 +0,0 @@
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
export interface LeaderboardsPageDto {
drivers: { drivers: DriverLeaderboardItemDTO[] };
teams: { teams: TeamListItemDTO[] };
}

View File

@@ -1,63 +0,0 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
/**
* LeagueDetail page query
* Returns the raw API DTO for the league detail page
* No DI container usage - constructs dependencies explicitly
*/
export class LeagueDetailPageQuery implements PageQuery<any, string> {
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
// Manual wiring: create API client
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
// Fetch data using API client
try {
const apiDto = await apiClient.getAllWithCapacityAndScoring();
if (!apiDto || !apiDto.leagues) {
return Result.err('notFound');
}
// Find the specific league
const league = apiDto.leagues.find(l => l.id === leagueId);
if (!league) {
return Result.err('notFound');
}
// Return the raw DTO - the page will handle ViewModel conversion
return Result.ok({
league,
apiDto,
});
} catch (error) {
console.error('LeagueDetailPageQuery failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err('redirect');
}
if (error.message.includes('404')) {
return Result.err('notFound');
}
if (error.message.includes('5') || error.message.includes('server')) {
return Result.err('LEAGUE_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
// Static method to avoid object construction in server code
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueDetailPageQuery();
return query.execute(leagueId);
}
}

View File

@@ -1,59 +0,0 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
/**
* LeagueRosterAdminPageQuery
*
* Fetches league roster admin data (members and join requests).
*/
export class LeagueRosterAdminPageQuery implements PageQuery<any, string> {
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'ROSTER_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
// Manual wiring: create API client
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
try {
// Get admin roster members and join requests
const [members, joinRequests] = await Promise.all([
apiClient.getAdminRosterMembers(leagueId),
apiClient.getAdminRosterJoinRequests(leagueId),
]);
if (!members || !joinRequests) {
return Result.err('notFound');
}
return Result.ok({
leagueId,
members,
joinRequests,
});
} catch (error) {
console.error('LeagueRosterAdminPageQuery failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err('redirect');
}
if (error.message.includes('404')) {
return Result.err('notFound');
}
if (error.message.includes('5') || error.message.includes('server')) {
return Result.err('ROSTER_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'ROSTER_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueRosterAdminPageQuery();
return query.execute(leagueId);
}
}

View File

@@ -1,63 +0,0 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
/**
* LeagueScheduleAdminPageQuery
*
* Fetches league schedule admin data.
*/
export class LeagueScheduleAdminPageQuery implements PageQuery<any, { leagueId: string; seasonId?: string }> {
async execute(input: { leagueId: string; seasonId?: string }): Promise<Result<any, 'notFound' | 'redirect' | 'SCHEDULE_ADMIN_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
// Manual wiring: create API client
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
try {
// Get seasons
const seasons = await apiClient.getSeasons(input.leagueId);
if (!seasons || seasons.length === 0) {
return Result.err('notFound');
}
// Determine season to use
const seasonId = input.seasonId || (seasons.find(s => s.status === 'active')?.seasonId || seasons[0].seasonId);
// Get schedule
const schedule = await apiClient.getSchedule(input.leagueId, seasonId);
return Result.ok({
leagueId: input.leagueId,
seasonId,
seasons,
schedule,
});
} catch (error) {
console.error('LeagueScheduleAdminPageQuery failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err('redirect');
}
if (error.message.includes('404')) {
return Result.err('notFound');
}
if (error.message.includes('5') || error.message.includes('server')) {
return Result.err('SCHEDULE_ADMIN_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
static async execute(input: { leagueId: string; seasonId?: string }): Promise<Result<any, 'notFound' | 'redirect' | 'SCHEDULE_ADMIN_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueScheduleAdminPageQuery();
return query.execute(input);
}
}

View File

@@ -1,133 +0,0 @@
import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
import { SessionGateway } from '@/lib/gateways/SessionGateway';
import { TeamService } from '@/lib/services/teams/TeamService';
import type { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
/**
* TeamDetailPageDto - Raw serializable data for team detail page
* Contains only raw data, no derived/computed properties
*/
export interface TeamDetailPageDto {
team: {
id: string;
name: string;
tag: string;
description?: string;
ownerId: string;
leagues: string[];
createdAt?: string;
specialization?: string;
region?: string;
languages?: string[];
category?: string;
membership?: {
role: string;
joinedAt: string;
isActive: boolean;
} | null;
canManage: boolean;
};
memberships: Array<{
driverId: string;
driverName: string;
role: 'owner' | 'manager' | 'member';
joinedAt: string;
isActive: boolean;
avatarUrl: string;
}>;
currentDriverId: string;
}
/**
* TeamDetailPageQuery - Server-side composition for team detail page
* Manual wiring only; no ContainerManager; no PageDataFetcher
* Returns raw serializable DTO
*/
export class TeamDetailPageQuery {
static async execute(teamId: string): Promise<PageQueryResult<TeamDetailPageDto>> {
try {
// Validate teamId
if (!teamId) {
return { status: 'notFound' };
}
// Get session to determine current driver
const sessionGateway = new SessionGateway();
const session = await sessionGateway.getSession();
if (!session?.user?.primaryDriverId) {
return { status: 'notFound' };
}
const currentDriverId = session.user.primaryDriverId;
// Manual dependency creation
const service = new TeamService();
// Fetch team details
const teamResult = await service.getTeamDetails(teamId, currentDriverId);
if (teamResult.isErr()) {
return { status: 'notFound' };
}
const teamData = teamResult.unwrap();
// Fetch team members
const membersResult = await service.getTeamMembers(teamId, currentDriverId, teamData.ownerId);
if (membersResult.isErr()) {
return { status: 'error', errorId: 'TEAM_MEMBERS_FETCH_FAILED' };
}
const membersData = membersResult.unwrap();
// Transform to raw serializable DTO
const dto: TeamDetailPageDto = {
team: {
id: teamData.id,
name: teamData.name,
tag: teamData.tag,
description: teamData.description,
ownerId: teamData.ownerId,
leagues: teamData.leagues,
createdAt: teamData.createdAt,
specialization: teamData.specialization,
region: teamData.region,
languages: teamData.languages,
category: teamData.category,
membership: teamData.membership ? {
role: teamData.membership.role,
joinedAt: teamData.membership.joinedAt,
isActive: teamData.membership.isActive,
} : null,
canManage: teamData.canManage,
},
memberships: membersData.map((member: TeamMemberViewModel) => ({
driverId: member.driverId,
driverName: member.driverName,
role: member.role,
joinedAt: member.joinedAt,
isActive: member.isActive,
avatarUrl: member.avatarUrl,
})),
currentDriverId,
};
return { status: 'ok', dto };
} catch (error) {
// Handle specific error types
if (error instanceof Error) {
const errorAny = error as { statusCode?: number; message?: string };
if (errorAny.message?.includes('not found') || errorAny.statusCode === 404) {
return { status: 'notFound' };
}
if (errorAny.message?.includes('redirect') || errorAny.statusCode === 302) {
return { status: 'redirect', to: '/' };
}
return { status: 'error', errorId: 'TEAM_DETAIL_FETCH_FAILED' };
}
return { status: 'error', errorId: 'UNKNOWN_ERROR' };
}
}
}