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

@@ -27,9 +27,9 @@ export class AdminDashboardPageQuery implements PageQuery<AdminDashboardViewData
}
// Transform to ViewData using builder
const viewData = AdminDashboardViewDataBuilder.build(apiDtoResult.unwrap());
const output = AdminDashboardViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(viewData);
return Result.ok(output);
} catch (err) {
console.error('AdminDashboardPageQuery failed:', err);

View File

@@ -31,9 +31,9 @@ export class AdminUsersPageQuery implements PageQuery<AdminUsersViewData, { sear
}
// Transform to ViewData using builder
const viewData = AdminUsersViewDataBuilder.build(apiDtoResult.unwrap());
const output = AdminUsersViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(viewData);
return Result.ok(output);
} catch (error) {
console.error('AdminUsersPageQuery failed:', error);

View File

@@ -7,7 +7,8 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { ForgotPasswordViewDataBuilder, ForgotPasswordViewData } from '@/lib/builders/view-data/ForgotPasswordViewDataBuilder';
import { ForgotPasswordViewDataBuilder } from '@/lib/builders/view-data/ForgotPasswordViewDataBuilder';
import { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData';
import { AuthPageService } from '@/lib/services/auth/AuthPageService';
import { SearchParamParser } from '@/lib/routing/search-params/SearchParamParser';

View File

@@ -7,7 +7,8 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { LoginViewDataBuilder, LoginViewData } from '@/lib/builders/view-data/LoginViewDataBuilder';
import { LoginViewDataBuilder } from '@/lib/builders/view-data/LoginViewDataBuilder';
import { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData';
import { AuthPageService } from '@/lib/services/auth/AuthPageService';
import { SearchParamParser } from '@/lib/routing/search-params/SearchParamParser';

View File

@@ -7,7 +7,8 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { ResetPasswordViewDataBuilder, ResetPasswordViewData } from '@/lib/builders/view-data/ResetPasswordViewDataBuilder';
import { ResetPasswordViewDataBuilder } from '@/lib/builders/view-data/ResetPasswordViewDataBuilder';
import { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData';
import { AuthPageService } from '@/lib/services/auth/AuthPageService';
import { SearchParamParser } from '@/lib/routing/search-params/SearchParamParser';

View File

@@ -5,7 +5,8 @@
* No business logic, only data composition.
*/
import { SignupViewData, SignupViewDataBuilder } from '@/lib/builders/view-data/SignupViewDataBuilder';
import { SignupViewDataBuilder } from '@/lib/builders/view-data/SignupViewDataBuilder';
import { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData';
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { AuthPageService } from '@/lib/services/auth/AuthPageService';

View File

@@ -0,0 +1,43 @@
/**
* GetAvatarPageQuery
*
* Server-side composition for avatar media route.
* Fetches avatar binary data and transforms to ViewData.
*/
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { MediaService } from '@/lib/services/media/MediaService';
import { AvatarViewDataBuilder } from '@/lib/builders/view-data/AvatarViewDataBuilder';
import { AvatarViewData } from '@/lib/view-data/AvatarViewData';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class GetAvatarPageQuery implements PageQuery<AvatarViewData, { driverId: string }> {
async execute(params: { driverId: string }): Promise<Result<AvatarViewData, PresentationError>> {
try {
// Manual construction: Service creates its own dependencies
const service = new MediaService();
// Fetch avatar data
const apiDtoResult = await service.getAvatar(params.driverId);
if (apiDtoResult.isErr()) {
return Result.err(mapToPresentationError(apiDtoResult.getError()));
}
// Transform to ViewData using builder
const output = AvatarViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(output);
} catch (err) {
console.error('GetAvatarPageQuery failed:', err);
return Result.err('serverError');
}
}
// Static method to avoid object construction in server code
static async execute(params: { driverId: string }): Promise<Result<AvatarViewData, PresentationError>> {
const query = new GetAvatarPageQuery();
return query.execute(params);
}
}

View File

@@ -0,0 +1,36 @@
/**
* GetCategoryIconPageQuery
*
* Server-side composition for category icon media route.
*/
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { MediaService } from '@/lib/services/media/MediaService';
import { CategoryIconViewDataBuilder } from '@/lib/builders/view-data/CategoryIconViewDataBuilder';
import { CategoryIconViewData } from '@/lib/view-data/CategoryIconViewData';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class GetCategoryIconPageQuery implements PageQuery<CategoryIconViewData, { categoryId: string }> {
async execute(params: { categoryId: string }): Promise<Result<CategoryIconViewData, PresentationError>> {
try {
const service = new MediaService();
const apiDtoResult = await service.getCategoryIcon(params.categoryId);
if (apiDtoResult.isErr()) {
return Result.err(mapToPresentationError(apiDtoResult.getError()));
}
const output = CategoryIconViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(output);
} catch (err) {
console.error('GetCategoryIconPageQuery failed:', err);
return Result.err('serverError');
}
}
static async execute(params: { categoryId: string }): Promise<Result<CategoryIconViewData, PresentationError>> {
const query = new GetCategoryIconPageQuery();
return query.execute(params);
}
}

View File

@@ -0,0 +1,36 @@
/**
* GetLeagueCoverPageQuery
*
* Server-side composition for league cover media route.
*/
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { MediaService } from '@/lib/services/media/MediaService';
import { LeagueCoverViewDataBuilder } from '@/lib/builders/view-data/LeagueCoverViewDataBuilder';
import { LeagueCoverViewData } from '@/lib/view-data/LeagueCoverViewData';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class GetLeagueCoverPageQuery implements PageQuery<LeagueCoverViewData, { leagueId: string }> {
async execute(params: { leagueId: string }): Promise<Result<LeagueCoverViewData, PresentationError>> {
try {
const service = new MediaService();
const apiDtoResult = await service.getLeagueCover(params.leagueId);
if (apiDtoResult.isErr()) {
return Result.err(mapToPresentationError(apiDtoResult.getError()));
}
const output = LeagueCoverViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(output);
} catch (err) {
console.error('GetLeagueCoverPageQuery failed:', err);
return Result.err('serverError');
}
}
static async execute(params: { leagueId: string }): Promise<Result<LeagueCoverViewData, PresentationError>> {
const query = new GetLeagueCoverPageQuery();
return query.execute(params);
}
}

View File

@@ -0,0 +1,36 @@
/**
* GetLeagueLogoPageQuery
*
* Server-side composition for league logo media route.
*/
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { MediaService } from '@/lib/services/media/MediaService';
import { LeagueLogoViewDataBuilder } from '@/lib/builders/view-data/LeagueLogoViewDataBuilder';
import { LeagueLogoViewData } from '@/lib/view-data/LeagueLogoViewData';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class GetLeagueLogoPageQuery implements PageQuery<LeagueLogoViewData, { leagueId: string }> {
async execute(params: { leagueId: string }): Promise<Result<LeagueLogoViewData, PresentationError>> {
try {
const service = new MediaService();
const apiDtoResult = await service.getLeagueLogo(params.leagueId);
if (apiDtoResult.isErr()) {
return Result.err(mapToPresentationError(apiDtoResult.getError()));
}
const output = LeagueLogoViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(output);
} catch (err) {
console.error('GetLeagueLogoPageQuery failed:', err);
return Result.err('serverError');
}
}
static async execute(params: { leagueId: string }): Promise<Result<LeagueLogoViewData, PresentationError>> {
const query = new GetLeagueLogoPageQuery();
return query.execute(params);
}
}

View File

@@ -0,0 +1,36 @@
/**
* GetSponsorLogoPageQuery
*
* Server-side composition for sponsor logo media route.
*/
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { MediaService } from '@/lib/services/media/MediaService';
import { SponsorLogoViewDataBuilder } from '@/lib/builders/view-data/SponsorLogoViewDataBuilder';
import { SponsorLogoViewData } from '@/lib/view-data/SponsorLogoViewData';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class GetSponsorLogoPageQuery implements PageQuery<SponsorLogoViewData, { sponsorId: string }> {
async execute(params: { sponsorId: string }): Promise<Result<SponsorLogoViewData, PresentationError>> {
try {
const service = new MediaService();
const apiDtoResult = await service.getSponsorLogo(params.sponsorId);
if (apiDtoResult.isErr()) {
return Result.err(mapToPresentationError(apiDtoResult.getError()));
}
const output = SponsorLogoViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(output);
} catch (err) {
console.error('GetSponsorLogoPageQuery failed:', err);
return Result.err('serverError');
}
}
static async execute(params: { sponsorId: string }): Promise<Result<SponsorLogoViewData, PresentationError>> {
const query = new GetSponsorLogoPageQuery();
return query.execute(params);
}
}

View File

@@ -0,0 +1,36 @@
/**
* GetTeamLogoPageQuery
*
* Server-side composition for team logo media route.
*/
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { MediaService } from '@/lib/services/media/MediaService';
import { TeamLogoViewDataBuilder } from '@/lib/builders/view-data/TeamLogoViewDataBuilder';
import { TeamLogoViewData } from '@/lib/view-data/TeamLogoViewData';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class GetTeamLogoPageQuery implements PageQuery<TeamLogoViewData, { teamId: string }> {
async execute(params: { teamId: string }): Promise<Result<TeamLogoViewData, PresentationError>> {
try {
const service = new MediaService();
const apiDtoResult = await service.getTeamLogo(params.teamId);
if (apiDtoResult.isErr()) {
return Result.err(mapToPresentationError(apiDtoResult.getError()));
}
const output = TeamLogoViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(output);
} catch (err) {
console.error('GetTeamLogoPageQuery failed:', err);
return Result.err('serverError');
}
}
static async execute(params: { teamId: string }): Promise<Result<TeamLogoViewData, PresentationError>> {
const query = new GetTeamLogoPageQuery();
return query.execute(params);
}
}

View File

@@ -0,0 +1,36 @@
/**
* GetTrackImagePageQuery
*
* Server-side composition for track image media route.
*/
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { MediaService } from '@/lib/services/media/MediaService';
import { TrackImageViewDataBuilder } from '@/lib/builders/view-data/TrackImageViewDataBuilder';
import { TrackImageViewData } from '@/lib/view-data/TrackImageViewData';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class GetTrackImagePageQuery implements PageQuery<TrackImageViewData, { trackId: string }> {
async execute(params: { trackId: string }): Promise<Result<TrackImageViewData, PresentationError>> {
try {
const service = new MediaService();
const apiDtoResult = await service.getTrackImage(params.trackId);
if (apiDtoResult.isErr()) {
return Result.err(mapToPresentationError(apiDtoResult.getError()));
}
const output = TrackImageViewDataBuilder.build(apiDtoResult.unwrap());
return Result.ok(output);
} catch (err) {
console.error('GetTrackImagePageQuery failed:', err);
return Result.err('serverError');
}
}
static async execute(params: { trackId: string }): Promise<Result<TrackImageViewData, PresentationError>> {
const query = new GetTrackImagePageQuery();
return query.execute(params);
}
}

View File

@@ -1,20 +1,10 @@
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: {
id: string;
name: string;
rating: number;
skillLevel: string;
nationality: string;
racesCompleted: number;
wins: number;
podiums: number;
isActive: boolean;
rank: number;
avatarUrl?: string;
}[];
drivers: DriverLeaderboardItemDTO[];
}

View File

@@ -0,0 +1,50 @@
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';
/**
* CreateLeaguePageQuery
*
* 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'>> {
// 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 scoring presets for the form
const presetsData = await apiClient.getScoringPresets();
return Result.ok({
scoringPresets: presetsData.presets || [],
});
} catch (error) {
console.error('CreateLeaguePageQuery 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('CREATE_LEAGUE_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
static async execute(): Promise<Result<any, 'notFound' | 'redirect' | 'CREATE_LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new CreateLeaguePageQuery();
return query.execute();
}
}

View File

@@ -3,15 +3,15 @@ import { Result } from '@/lib/contracts/Result';
import { DashboardViewDataBuilder } from '@/lib/builders/view-data/DashboardViewDataBuilder';
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
import { DashboardService } from '@/lib/services/analytics/DashboardService';
import { mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { mapToPresentationError, type PresentationError } from '@/lib/contracts/page-queries/PresentationError';
/**
* Dashboard page query
* Returns Result<DashboardViewData, PresentationError>
* No DI container usage - constructs dependencies explicitly
*/
export class DashboardPageQuery implements PageQuery<DashboardViewData, void> {
async execute(): Promise<Result<DashboardViewData, 'notFound' | 'redirect' | 'DASHBOARD_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
export class DashboardPageQuery implements PageQuery<DashboardViewData, void, PresentationError> {
async execute(): Promise<Result<DashboardViewData, PresentationError>> {
// Manual wiring: Service creates its own dependencies
const dashboardService = new DashboardService();
@@ -19,31 +19,18 @@ export class DashboardPageQuery implements PageQuery<DashboardViewData, void> {
const serviceResult = await dashboardService.getDashboardOverview();
if (serviceResult.isErr()) {
const serviceError = serviceResult.getError();
// Map domain errors to presentation errors
switch (serviceError.type) {
case 'notFound':
return Result.err('notFound');
case 'unauthorized':
return Result.err('redirect');
case 'serverError':
case 'networkError':
case 'unknown':
return Result.err('DASHBOARD_FETCH_FAILED');
default:
return Result.err('UNKNOWN_ERROR');
}
// Map domain errors to presentation errors using helper
return Result.err(mapToPresentationError(serviceResult.getError()));
}
// Transform to ViewData using builder
const apiDto = serviceResult.unwrap();
const viewData = DashboardViewDataBuilder.build(apiDto);
return Result.ok(viewData);
const dashboardView = DashboardViewDataBuilder.build(apiDto);
return Result.ok(dashboardView);
}
// Static method to avoid object construction in server code
static async execute(): Promise<Result<DashboardViewData, 'notFound' | 'redirect' | 'DASHBOARD_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
static async execute(): Promise<Result<DashboardViewData, PresentationError>> {
const query = new DashboardPageQuery();
return query.execute();
}

View File

@@ -1,15 +1,14 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
import { DriverProfilePageService } from '@/lib/services/drivers/DriverProfilePageService';
import { DriverProfileViewModelBuilder } from '@/lib/builders/view-models/DriverProfileViewModelBuilder';
import type { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel';
/**
* DriverProfilePageQuery
*
* Server-side data fetcher for the driver profile page.
* Returns a discriminated union with all possible page states.
* API DTO is already JSON-serializable.
* Uses Service for data access and ViewModelBuilder for transformation.
*/
export class DriverProfilePageQuery {
/**
@@ -18,7 +17,7 @@ export class DriverProfilePageQuery {
* @param driverId - The driver ID to fetch profile for
* @returns PageQueryResult with discriminated union of states
*/
static async execute(driverId: string | null): Promise<PageQueryResult<GetDriverProfileOutputDTO>> {
static async execute(driverId: string | null): Promise<PageQueryResult<DriverProfileViewModel>> {
// Handle missing driver ID
if (!driverId) {
return { status: 'notFound' };
@@ -26,20 +25,28 @@ export class DriverProfilePageQuery {
try {
// Manual wiring: construct dependencies explicitly
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
const service = new DriverProfilePageService();
const dto = await apiClient.getDriverProfile(driverId);
const result = await service.getDriverProfile(driverId);
if (!dto.currentDriver) {
return { status: 'notFound' };
if (result.isErr()) {
const error = result.getError();
if (error === 'notFound') {
return { status: 'notFound' };
}
if (error === 'unauthorized') {
return { status: 'error', errorId: 'UNAUTHORIZED' };
}
return { status: 'error', errorId: 'DRIVER_PROFILE_FETCH_FAILED' };
}
// API DTO is already JSON-serializable
return { status: 'ok', dto };
// Build ViewModel from DTO
const dto = result.unwrap();
const viewModel = DriverProfileViewModelBuilder.build(dto);
return { status: 'ok', dto: viewModel };
} catch (error) {
console.error('DriverProfilePageQuery failed:', error);

View File

@@ -1,77 +1,37 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { DriverRankingsPageDto } from '@/lib/page-queries/page-dtos/DriverRankingsPageDto';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
interface ErrorWithStatusCode extends Error {
statusCode?: number;
}
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { DriverRankingsViewDataBuilder } from '@/lib/builders/view-data/DriverRankingsViewDataBuilder';
import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewData';
import { DriverRankingsService } from '@/lib/services/leaderboards/DriverRankingsService';
import { mapToPresentationError, type PresentationError } from '@/lib/contracts/page-queries/PresentationError';
/**
* Transform DriverLeaderboardItemDTO to DriverRankingsPageDto
* Driver Rankings page query
* Returns Result<DriverRankingsViewData, PresentationError>
* No DI container usage - constructs dependencies explicitly
*/
function transformDtoToPageDto(dto: { drivers: DriverLeaderboardItemDTO[] }): DriverRankingsPageDto {
return {
drivers: dto.drivers.map(driver => ({
id: driver.id,
name: driver.name,
rating: driver.rating,
skillLevel: driver.skillLevel,
nationality: driver.nationality,
racesCompleted: driver.racesCompleted,
wins: driver.wins,
podiums: driver.podiums,
isActive: driver.isActive,
rank: driver.rank,
avatarUrl: driver.avatarUrl || '',
})),
};
}
/**
* Driver Rankings page query with manual wiring
* Returns PageQueryResult<DriverRankingsPageDto>
*/
export class DriverRankingsPageQuery {
/**
* Execute the driver rankings page query
*/
static async execute(): Promise<PageQueryResult<DriverRankingsPageDto>> {
try {
// Manual wiring
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
// Fetch data
const dto = await apiClient.getLeaderboard();
if (!dto || !dto.drivers) {
return { status: 'notFound' };
}
// Transform to Page DTO
const pageDto = transformDtoToPageDto(dto);
return { status: 'ok', dto: pageDto };
} catch (error) {
if (error instanceof Error) {
const errorWithStatus = error as ErrorWithStatusCode;
if (errorWithStatus.message?.includes('not found') || errorWithStatus.statusCode === 404) {
return { status: 'notFound' };
}
if (errorWithStatus.message?.includes('redirect') || errorWithStatus.statusCode === 302) {
return { status: 'redirect', to: '/' };
}
return { status: 'error', errorId: 'DRIVER_RANKINGS_FETCH_FAILED' };
}
return { status: 'error', errorId: 'UNKNOWN_ERROR' };
export class DriverRankingsPageQuery implements PageQuery<DriverRankingsViewData, void, PresentationError> {
async execute(): Promise<Result<DriverRankingsViewData, PresentationError>> {
// Manual wiring: Service creates its own dependencies
const service = new DriverRankingsService();
// Fetch data using service
const serviceResult = await service.getDriverRankings();
if (serviceResult.isErr()) {
// Map domain errors to presentation errors
return Result.err(mapToPresentationError(serviceResult.getError()));
}
// Transform to ViewData using builder
const apiDto = serviceResult.unwrap();
const viewData = DriverRankingsViewDataBuilder.build(apiDto.drivers);
return Result.ok(viewData);
}
// Static method to avoid object construction in server code
static async execute(): Promise<Result<DriverRankingsViewData, PresentationError>> {
const query = new DriverRankingsPageQuery();
return query.execute();
}
}

View File

@@ -1,15 +1,14 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
import { DriversPageService } from '@/lib/services/drivers/DriversPageService';
import { DriversViewModelBuilder } from '@/lib/builders/view-models/DriversViewModelBuilder';
import type { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
/**
* DriversPageQuery
*
* Server-side data fetcher for the drivers listing page.
* Returns a discriminated union with all possible page states.
* API DTO is already JSON-serializable.
* Uses Service for data access and ViewModelBuilder for transformation.
*/
export class DriversPageQuery {
/**
@@ -17,30 +16,27 @@ export class DriversPageQuery {
*
* @returns PageQueryResult with discriminated union of states
*/
static async execute(): Promise<PageQueryResult<DriversLeaderboardDTO>> {
static async execute(): Promise<PageQueryResult<DriverLeaderboardViewModel>> {
try {
// Manual wiring: construct dependencies explicitly
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
const service = new DriversPageService();
const result = await apiClient.getLeaderboard();
const result = await service.getLeaderboard();
if (!result || !result.drivers) {
return { status: 'notFound' };
if (result.isErr()) {
const error = result.getError();
if (error === 'notFound') {
return { status: 'notFound' };
}
return { status: 'error', errorId: 'DRIVERS_FETCH_FAILED' };
}
// Transform to the expected DTO format
const dto: DriversLeaderboardDTO = {
drivers: result.drivers,
totalRaces: result.drivers.reduce((sum, driver) => sum + driver.racesCompleted, 0),
totalWins: result.drivers.reduce((sum, driver) => sum + driver.wins, 0),
activeCount: result.drivers.filter(driver => driver.isActive).length,
};
return { status: 'ok', dto };
// Build ViewModel from DTO
const dto = result.unwrap();
const viewModel = DriversViewModelBuilder.build(dto);
return { status: 'ok', dto: viewModel };
} catch (error) {
console.error('DriversPageQuery failed:', error);

View File

@@ -1,64 +1,37 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { LeaderboardsPageDto } from '@/lib/page-queries/page-dtos/LeaderboardsPageDto';
interface ErrorWithStatusCode extends Error {
statusCode?: number;
}
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeaderboardsViewDataBuilder } from '@/lib/builders/view-data/LeaderboardsViewDataBuilder';
import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
import { LeaderboardsService } from '@/lib/services/leaderboards/LeaderboardsService';
import { mapToPresentationError, type PresentationError } from '@/lib/contracts/page-queries/PresentationError';
/**
* Leaderboards page query with manual wiring
* Returns PageQueryResult<LeaderboardsPageDto>
* Leaderboards page query
* Returns Result<LeaderboardsViewData, PresentationError>
* No DI container usage - constructs dependencies explicitly
*/
export class LeaderboardsPageQuery {
/**
* Execute the leaderboards page query
*/
static async execute(): Promise<PageQueryResult<LeaderboardsPageDto>> {
try {
// Manual wiring
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const driversApiClient = new DriversApiClient(baseUrl, errorReporter, logger);
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
// Fetch data in parallel
const [driverResult, teamResult] = await Promise.all([
driversApiClient.getLeaderboard(),
teamsApiClient.getAll(),
]);
if (!driverResult && !teamResult) {
return { status: 'notFound' };
}
// Transform to Page DTO
const pageDto: LeaderboardsPageDto = {
drivers: driverResult || { drivers: [] },
teams: teamResult || { teams: [] },
};
return { status: 'ok', dto: pageDto };
} catch (error) {
if (error instanceof Error) {
const errorWithStatus = error as ErrorWithStatusCode;
if (errorWithStatus.message?.includes('not found') || errorWithStatus.statusCode === 404) {
return { status: 'notFound' };
}
if (errorWithStatus.message?.includes('redirect') || errorWithStatus.statusCode === 302) {
return { status: 'redirect', to: '/' };
}
return { status: 'error', errorId: 'LEADERBOARDS_FETCH_FAILED' };
}
return { status: 'error', errorId: 'UNKNOWN_ERROR' };
export class LeaderboardsPageQuery implements PageQuery<LeaderboardsViewData, void, PresentationError> {
async execute(): Promise<Result<LeaderboardsViewData, PresentationError>> {
// Manual wiring: Service creates its own dependencies
const service = new LeaderboardsService();
// Fetch data using service
const serviceResult = await service.getLeaderboards();
if (serviceResult.isErr()) {
// Map domain errors to presentation errors
return Result.err(mapToPresentationError(serviceResult.getError()));
}
// Transform to ViewData using builder
const apiDto = serviceResult.unwrap();
const viewData = LeaderboardsViewDataBuilder.build(apiDto);
return Result.ok(viewData);
}
// Static method to avoid object construction in server code
static async execute(): Promise<Result<LeaderboardsViewData, PresentationError>> {
const query = new LeaderboardsPageQuery();
return query.execute();
}
}

View File

@@ -0,0 +1,77 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { ProtestsApiClient } from '@/lib/api/protests/ProtestsApiClient';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
/**
* LeagueProtestReviewPageQuery
*
* 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'>> {
// 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);
try {
// Get protest details
// Note: This would need a getProtestDetail method on ProtestsApiClient
// For now, return placeholder data
const protestDetail = {
protest: {
id: input.protestId,
raceId: 'placeholder',
protestingDriverId: 'placeholder',
accusedDriverId: 'placeholder',
description: 'Placeholder protest',
status: 'pending',
submittedAt: new Date().toISOString(),
},
race: {
id: 'placeholder',
name: 'Placeholder Race',
scheduledAt: new Date().toISOString(),
},
protestingDriver: {
id: 'placeholder',
name: 'Placeholder Protester',
},
accusedDriver: {
id: 'placeholder',
name: 'Placeholder Accused',
},
penaltyTypes: [],
defaultReasons: {},
};
return Result.ok(protestDetail);
} catch (error) {
console.error('LeagueProtestReviewPageQuery 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('PROTEST_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
static async execute(input: { leagueId: string; protestId: string }): Promise<Result<any, 'notFound' | 'redirect' | 'PROTEST_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueProtestReviewPageQuery();
return query.execute(input);
}
}

View File

@@ -0,0 +1,59 @@
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

@@ -0,0 +1,21 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
/**
* LeagueRulebookPageQuery
*
* Fetches league rulebook data.
* Currently returns empty data - would need API endpoint.
*/
export class LeagueRulebookPageQuery implements PageQuery<any, string> {
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'RULEBOOK_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
// TODO: Implement when API endpoint is available
// For now, return empty data
return Result.ok({ leagueId, rules: [] });
}
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'RULEBOOK_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueRulebookPageQuery();
return query.execute(leagueId);
}
}

View File

@@ -0,0 +1,63 @@
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

@@ -0,0 +1,52 @@
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';
/**
* LeagueSchedulePageQuery
*
* Fetches league schedule data for the schedule page.
* Returns raw API DTO for now - would need ViewDataBuilder for proper transformation.
*/
export class LeagueSchedulePageQuery implements PageQuery<any, string> {
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'SCHEDULE_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 {
const scheduleDto = await apiClient.getSchedule(leagueId);
if (!scheduleDto) {
return Result.err('notFound');
}
return Result.ok(scheduleDto);
} catch (error) {
console.error('LeagueSchedulePageQuery 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_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'SCHEDULE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueSchedulePageQuery();
return query.execute(leagueId);
}
}

View File

@@ -0,0 +1,55 @@
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';
/**
* LeagueSettingsPageQuery
*
* Fetches league settings data.
*/
export class LeagueSettingsPageQuery implements PageQuery<any, string> {
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'SETTINGS_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 league config
const config = await apiClient.getLeagueConfig(leagueId);
if (!config) {
return Result.err('notFound');
}
return Result.ok({
leagueId,
config,
});
} catch (error) {
console.error('LeagueSettingsPageQuery 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('SETTINGS_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'SETTINGS_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueSettingsPageQuery();
return query.execute(leagueId);
}
}

View File

@@ -0,0 +1,61 @@
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';
/**
* LeagueSponsorshipsPageQuery
*
* Fetches league sponsorships data.
*/
export class LeagueSponsorshipsPageQuery implements PageQuery<any, string> {
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'SPONSORSHIPS_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 first to find active season
const seasons = await apiClient.getSeasons(leagueId);
if (!seasons || seasons.length === 0) {
return Result.err('notFound');
}
// Get sponsorships for the first season (or active season)
const activeSeason = seasons.find(s => s.status === 'active') || seasons[0];
const sponsorshipsData = await apiClient.getSeasonSponsorships(activeSeason.seasonId);
return Result.ok({
leagueId,
seasonId: activeSeason.seasonId,
sponsorships: sponsorshipsData.sponsorships || [],
seasons,
});
} catch (error) {
console.error('LeagueSponsorshipsPageQuery 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('SPONSORSHIPS_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'SPONSORSHIPS_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueSponsorshipsPageQuery();
return query.execute(leagueId);
}
}

View File

@@ -0,0 +1,65 @@
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';
import type { LeagueStandingsViewData } from '@/lib/view-data/LeagueStandingsViewData';
/**
* LeagueStandingsPageQuery
*
* Fetches league standings data for the standings page.
* Returns Result<LeagueStandingsViewData, PresentationError>
*/
export class LeagueStandingsPageQuery implements PageQuery<LeagueStandingsViewData, string> {
async execute(leagueId: string): Promise<Result<LeagueStandingsViewData, 'notFound' | 'redirect' | 'STANDINGS_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 {
// Fetch standings
const standingsDto = await apiClient.getStandings(leagueId);
if (!standingsDto) {
return Result.err('notFound');
}
// For now, return empty data structure
// In a real implementation, this would transform the DTO to ViewData
const viewData: LeagueStandingsViewData = {
standings: [],
drivers: [],
memberships: [],
leagueId,
currentDriverId: null,
isAdmin: false,
};
return Result.ok(viewData);
} catch (error) {
console.error('LeagueStandingsPageQuery 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('STANDINGS_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
static async execute(leagueId: string): Promise<Result<LeagueStandingsViewData, 'notFound' | 'redirect' | 'STANDINGS_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueStandingsPageQuery();
return query.execute(leagueId);
}
}

View File

@@ -0,0 +1,67 @@
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';
/**
* LeagueStewardingPageQuery
*
* Fetches league stewarding data (protests and penalties).
*/
export class LeagueStewardingPageQuery implements PageQuery<any, string> {
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'STEWARDING_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 races for the league
const racesData = await apiClient.getRaces(leagueId);
if (!racesData) {
return Result.err('notFound');
}
// Get memberships for driver lookup
const memberships = await apiClient.getMemberships(leagueId);
// Return data structure for stewarding page
// In real implementation, would need protest/penalty API endpoints
return Result.ok({
leagueId,
races: racesData.races || [],
memberships: memberships || { members: [] },
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
racesWithData: [],
allDrivers: [],
driverMap: {},
});
} catch (error) {
console.error('LeagueStewardingPageQuery 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('STEWARDING_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'STEWARDING_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueStewardingPageQuery();
return query.execute(leagueId);
}
}

View File

@@ -0,0 +1,63 @@
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';
/**
* LeagueWalletPageQuery
*
* Fetches league wallet data.
*/
export class LeagueWalletPageQuery implements PageQuery<any, string> {
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'WALLET_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 league memberships to verify access
const memberships = await apiClient.getMemberships(leagueId);
if (!memberships) {
return Result.err('notFound');
}
// Return wallet data structure
// In real implementation, would need wallet API endpoints
return Result.ok({
leagueId,
balance: 0,
totalRevenue: 0,
totalFees: 0,
pendingPayouts: 0,
transactions: [],
canWithdraw: false,
withdrawalBlockReason: 'Wallet system not yet implemented',
});
} catch (error) {
console.error('LeagueWalletPageQuery 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('WALLET_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
}
}
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'WALLET_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
const query = new LeagueWalletPageQuery();
return query.execute(leagueId);
}
}

View File

@@ -1,132 +1,46 @@
import { ApiClient } from '@/lib/api';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
import { Result } from '@/lib/contracts/Result';
import type { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { SessionGateway } from '@/lib/gateways/SessionGateway';
/**
* Page DTO for Profile Leagues page
* JSON-serializable data structure for server-to-client communication
*/
export 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';
}>;
}
interface MembershipDTO {
driverId: string;
role: string;
status?: 'active' | 'inactive';
}
import { ProfileLeaguesService } from '@/lib/services/leagues/ProfileLeaguesService';
import { ProfileLeaguesViewDataBuilder } from '@/lib/builders/view-data/ProfileLeaguesViewDataBuilder';
import type { ProfileLeaguesViewData } from '@/lib/view-data/ProfileLeaguesViewData';
import { mapToPresentationError, type PresentationError } from '@/lib/contracts/page-queries/PresentationError';
/**
* Profile Leagues Page Query
*
* Server-side composition that:
* 1. Reads session to determine currentDriverId
* 2. Calls API clients directly (manual wiring)
* 3. Assembles Page DTO
* 4. Returns PageQueryResult
* 2. Uses service for orchestration
* 3. Transforms to ViewData using builder
* 4. Returns Result<ViewData, PresentationError>
*/
export class ProfileLeaguesPageQuery {
static async execute(): Promise<PageQueryResult<ProfileLeaguesPageDto>> {
try {
// Get session server-side
const sessionGateway = new SessionGateway();
const session = await sessionGateway.getSession();
if (!session?.user?.primaryDriverId) {
return { status: 'notFound' };
}
const currentDriverId = session.user.primaryDriverId;
// Manual wiring: construct API client explicitly
const apiBaseUrl = getWebsiteApiBaseUrl();
const apiClient = new ApiClient(apiBaseUrl);
// Fetch all leagues
const leaguesDto = await apiClient.leagues.getAllWithCapacity();
if (!leaguesDto?.leagues) {
return { status: 'notFound' };
}
// Fetch memberships for each league and categorize
const owned: ProfileLeaguesPageDto['ownedLeagues'] = [];
const member: ProfileLeaguesPageDto['memberLeagues'] = [];
for (const league of leaguesDto.leagues) {
try {
const membershipsDto = await apiClient.leagues.getMemberships(league.id);
// Handle both possible response structures with proper type checking
let memberships: MembershipDTO[] = [];
if (membershipsDto && typeof membershipsDto === 'object') {
if ('members' in membershipsDto && Array.isArray((membershipsDto as { members?: unknown }).members)) {
memberships = (membershipsDto as { members: MembershipDTO[] }).members;
} else if ('memberships' in membershipsDto && Array.isArray((membershipsDto as { memberships?: unknown }).memberships)) {
memberships = (membershipsDto as { memberships: MembershipDTO[] }).memberships;
}
}
const currentMembership = memberships.find(
(m) => m.driverId === currentDriverId
);
if (currentMembership && currentMembership.status === 'active') {
const leagueData = {
leagueId: league.id,
name: league.name,
description: league.description,
membershipRole: currentMembership.role as 'owner' | 'admin' | 'steward' | 'member',
};
if (currentMembership.role === 'owner') {
owned.push(leagueData);
} else {
member.push(leagueData);
}
}
} catch (error) {
// Skip leagues where membership fetch fails
console.warn(`Failed to fetch memberships for league ${league.id}:`, error);
continue;
}
}
return {
status: 'ok',
dto: {
ownedLeagues: owned,
memberLeagues: member,
},
};
} catch (error) {
console.error('ProfileLeaguesPageQuery failed:', error);
if (error instanceof Error) {
// Check for specific error properties
const errorAny = error as { statusCode?: number };
if (error.message.includes('not found') || errorAny.statusCode === 404) {
return { status: 'notFound' };
}
if (error.message.includes('redirect') || errorAny.statusCode === 302) {
return { status: 'redirect', to: '/' };
}
return { status: 'error', errorId: 'PROFILE_LEAGUES_FETCH_FAILED' };
}
return { status: 'error', errorId: 'UNKNOWN_ERROR' };
export class ProfileLeaguesPageQuery implements PageQuery<ProfileLeaguesViewData, void, PresentationError> {
async execute(): Promise<Result<ProfileLeaguesViewData, PresentationError>> {
// Get session server-side
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 ProfileLeaguesService();
const serviceResult = await service.getProfileLeagues(currentDriverId);
if (serviceResult.isErr()) {
return Result.err(mapToPresentationError(serviceResult.getError()));
}
// Transform to ViewData using builder
const apiDto = serviceResult.unwrap();
const profileView = ProfileLeaguesViewDataBuilder.build(apiDto);
return Result.ok(profileView);
}
}
static async execute(): Promise<Result<ProfileLeaguesViewData, PresentationError>> {
const query = new ProfileLeaguesPageQuery();
return query.execute();
}
}

View File

@@ -1,47 +1,48 @@
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { isProductionEnvironment } from '@/lib/config/env';
import type { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import type { Result as ResultType } from '@/lib/contracts/Result';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
import type { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { SessionGateway } from '@/lib/gateways/SessionGateway';
import { SponsorshipRequestsService } from '@/lib/services/sponsors/SponsorshipRequestsService';
import { SponsorshipRequestsPageViewDataBuilder } from '@/lib/builders/view-data/SponsorshipRequestsPageViewDataBuilder';
import type { SponsorshipRequestsViewData } from '@/lib/view-data/SponsorshipRequestsViewData';
import { mapToPresentationError, type PresentationError } from '@/lib/contracts/page-queries/PresentationError';
export type SponsorshipRequestsPageQueryError =
| 'SPONSORSHIP_REQUESTS_NOT_FOUND';
export class SponsorshipRequestsPageQuery
implements PageQuery<GetPendingSponsorshipRequestsOutputDTO, Record<string, string>, SponsorshipRequestsPageQueryError>
{
private readonly client: SponsorsApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
/**
* Sponsorship Requests Page Query
*
* Server-side composition that:
* 1. Reads session to determine currentDriverId
* 2. Uses service for orchestration
* 3. Transforms to ViewData using builder
* 4. Returns Result<ViewData, PresentationError>
*/
export class SponsorshipRequestsPageQuery implements PageQuery<SponsorshipRequestsViewData, void, PresentationError> {
async execute(): Promise<Result<SponsorshipRequestsViewData, 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 service = new SponsorshipRequestsService();
const serviceResult = await service.getPendingRequests({
entityType: 'driver',
entityId: session.user.primaryDriverId,
});
this.client = new SponsorsApiClient(baseUrl, errorReporter, logger);
if (serviceResult.isErr()) {
return Result.err(mapToPresentationError(serviceResult.getError()));
}
// Transform to ViewData using builder
const apiDto = serviceResult.unwrap();
const sponsorshipView = SponsorshipRequestsPageViewDataBuilder.build(apiDto);
return Result.ok(sponsorshipView);
}
async execute(
params: Record<string, string>,
): Promise<ResultType<GetPendingSponsorshipRequestsOutputDTO, SponsorshipRequestsPageQueryError>> {
try {
// For now, we'll use hardcoded entityType/entityId
// In a real implementation, these would come from the user session
const result = await this.client.getPendingSponsorshipRequests({
entityType: 'driver',
entityId: 'current-user-id', // This would come from session
});
return Result.ok(result);
} catch {
return Result.err('SPONSORSHIP_REQUESTS_NOT_FOUND');
}
static async execute(): Promise<Result<SponsorshipRequestsViewData, PresentationError>> {
const query = new SponsorshipRequestsPageQuery();
return query.execute();
}
}

View File

@@ -1,8 +1,5 @@
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
import { SessionGateway } from '@/lib/gateways/SessionGateway';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { TeamService } from '@/lib/services/teams/TeamService';
import type { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
@@ -65,26 +62,25 @@ export class TeamDetailPageQuery {
const currentDriverId = session.user.primaryDriverId;
// Manual dependency creation
const baseUrl = process.env.API_BASE_URL || 'http://localhost:3101';
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
const service = new TeamService(teamsApiClient);
const service = new TeamService();
// Fetch team details
const teamData = await service.getTeamDetails(teamId, currentDriverId);
const teamResult = await service.getTeamDetails(teamId, currentDriverId);
if (!teamData) {
if (teamResult.isErr()) {
return { status: 'notFound' };
}
const teamData = teamResult.unwrap();
// Fetch team members
const membersData = await service.getTeamMembers(teamId, currentDriverId, teamData.ownerId);
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 = {

View File

@@ -1,7 +1,4 @@
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { TeamService } from '@/lib/services/teams/TeamService';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
@@ -39,19 +36,16 @@ export class TeamsPageQuery {
static async execute(): Promise<PageQueryResult<TeamsPageDto>> {
try {
// Manual dependency creation
const baseUrl = process.env.API_BASE_URL || 'http://localhost:3101';
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
const service = new TeamService(teamsApiClient);
const service = new TeamService();
// Fetch teams
const teams = await service.getAllTeams();
const result = await service.getAllTeams();
if (result.isErr()) {
return { status: 'error', errorId: 'TEAMS_FETCH_FAILED' };
}
const teams = result.unwrap();
if (!teams || teams.length === 0) {
return { status: 'notFound' };

View File

@@ -0,0 +1,41 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { RaceDetailViewData } from '@/lib/view-data/races/RaceDetailViewData';
import { RacesService } from '@/lib/services/races/RacesService';
import { RaceDetailViewDataBuilder } from '@/lib/builders/view-data/RaceDetailViewDataBuilder';
interface RaceDetailPageQueryParams {
raceId: string;
driverId?: string;
}
/**
* Race Detail Page Query
*
* Fetches race detail data for the race detail page.
* Returns Result<RaceDetailViewData, PresentationError>
*/
export class RaceDetailPageQuery implements PageQuery<RaceDetailViewData, RaceDetailPageQueryParams> {
async execute(params: RaceDetailPageQueryParams): Promise<Result<RaceDetailViewData, PresentationError>> {
// Manual wiring: Service creates its own dependencies
const service = new RacesService();
// Get race detail data
const result = await service.getRaceDetail(params.raceId, params.driverId || '');
if (result.isErr()) {
return Result.err(mapToPresentationError(result.getError()));
}
// Transform to ViewData using builder
const viewData = RaceDetailViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
// Static method to avoid object construction in server code
static async execute(params: RaceDetailPageQueryParams): Promise<Result<RaceDetailViewData, PresentationError>> {
const query = new RaceDetailPageQuery();
return await query.execute(params);
}
}

View File

@@ -0,0 +1,41 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { RaceResultsViewData } from '@/lib/view-data/races/RaceResultsViewData';
import { RaceResultsService } from '@/lib/services/races/RaceResultsService';
import { RaceResultsViewDataBuilder } from '@/lib/builders/view-data/RaceResultsViewDataBuilder';
interface RaceResultsPageQueryParams {
raceId: string;
driverId?: string;
}
/**
* Race Results Page Query
*
* Fetches race results data for the race results page.
* Returns Result<RaceResultsViewData, PresentationError>
*/
export class RaceResultsPageQuery implements PageQuery<RaceResultsViewData, RaceResultsPageQueryParams> {
async execute(params: RaceResultsPageQueryParams): Promise<Result<RaceResultsViewData, PresentationError>> {
// Manual wiring: Service creates its own dependencies
const service = new RaceResultsService();
// Get race results data
const result = await service.getRaceResultsDetail(params.raceId);
if (result.isErr()) {
return Result.err(mapToPresentationError(result.getError()));
}
// Transform to ViewData using builder
const viewData = RaceResultsViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
// Static method to avoid object construction in server code
static async execute(params: RaceResultsPageQueryParams): Promise<Result<RaceResultsViewData, PresentationError>> {
const query = new RaceResultsPageQuery();
return await query.execute(params);
}
}

View File

@@ -0,0 +1,41 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { RaceStewardingViewData } from '@/lib/view-data/races/RaceStewardingViewData';
import { RaceStewardingService } from '@/lib/services/races/RaceStewardingService';
import { RaceStewardingViewDataBuilder } from '@/lib/builders/view-data/RaceStewardingViewDataBuilder';
interface RaceStewardingPageQueryParams {
raceId: string;
driverId?: string;
}
/**
* Race Stewarding Page Query
*
* Fetches race stewarding data for the stewarding page.
* Returns Result<RaceStewardingViewData, PresentationError>
*/
export class RaceStewardingPageQuery implements PageQuery<RaceStewardingViewData, RaceStewardingPageQueryParams> {
async execute(params: RaceStewardingPageQueryParams): Promise<Result<RaceStewardingViewData, PresentationError>> {
// Manual wiring: Service creates its own dependencies
const service = new RaceStewardingService();
// Get race stewarding data
const result = await service.getRaceStewarding(params.raceId);
if (result.isErr()) {
return Result.err(mapToPresentationError(result.getError()));
}
// Transform to ViewData using builder
const viewData = RaceStewardingViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
// Static method to avoid object construction in server code
static async execute(params: RaceStewardingPageQueryParams): Promise<Result<RaceStewardingViewData, PresentationError>> {
const query = new RaceStewardingPageQuery();
return await query.execute(params);
}
}

View File

@@ -0,0 +1,36 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { RacesAllViewData } from '@/lib/view-data/races/RacesAllViewData';
import { RacesService } from '@/lib/services/races/RacesService';
import { RacesAllViewDataBuilder } from '@/lib/builders/view-data/RacesAllViewDataBuilder';
/**
* Races All Page Query
*
* Fetches all races data for the all races page.
* Returns Result<RacesAllViewData, PresentationError>
*/
export class RacesAllPageQuery implements PageQuery<RacesAllViewData, void> {
async execute(): Promise<Result<RacesAllViewData, PresentationError>> {
// Manual wiring: Service creates its own dependencies
const service = new RacesService();
// Get all races data
const result = await service.getAllRacesPageData();
if (result.isErr()) {
return Result.err(mapToPresentationError(result.getError()));
}
// Transform to ViewData using builder
const viewData = RacesAllViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
// Static method to avoid object construction in server code
static async execute(): Promise<Result<RacesAllViewData, PresentationError>> {
const query = new RacesAllPageQuery();
return await query.execute();
}
}

View File

@@ -0,0 +1,36 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { RacesViewData } from '@/lib/view-data/races/RacesViewData';
import { RacesService } from '@/lib/services/races/RacesService';
import { RacesViewDataBuilder } from '@/lib/builders/view-data/RacesViewDataBuilder';
/**
* Races Page Query
*
* Fetches races data for the main races page.
* Returns Result<RacesViewData, PresentationError>
*/
export class RacesPageQuery implements PageQuery<RacesViewData, void> {
async execute(): Promise<Result<RacesViewData, PresentationError>> {
// Manual wiring: Service creates its own dependencies
const service = new RacesService();
// Get races data
const result = await service.getRacesPageData();
if (result.isErr()) {
return Result.err(mapToPresentationError(result.getError()));
}
// Transform to ViewData using builder
const viewData = RacesViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
// Static method to avoid object construction in server code
static async execute(): Promise<Result<RacesViewData, PresentationError>> {
const query = new RacesPageQuery();
return await query.execute();
}
}