website refactor
This commit is contained in:
63
apps/website/lib/adapters/MediaAdapter.ts
Normal file
63
apps/website/lib/adapters/MediaAdapter.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* MediaAdapter
|
||||
*
|
||||
* Handles HTTP operations for media assets.
|
||||
* This is where external API calls belong.
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError } from '@/lib/contracts/services/Service';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
|
||||
/**
|
||||
* MediaAdapter
|
||||
*
|
||||
* Handles binary media fetching from the API.
|
||||
* All HTTP calls are isolated here.
|
||||
*/
|
||||
export class MediaAdapter {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = getWebsiteApiBaseUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch binary media from API
|
||||
*
|
||||
* @param mediaPath - API path to media resource
|
||||
* @returns Result with MediaBinaryDTO on success, DomainError on failure
|
||||
*/
|
||||
async fetchMedia(mediaPath: string): Promise<Result<MediaBinaryDTO, DomainError>> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}${mediaPath}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return Result.err({
|
||||
type: 'notFound',
|
||||
message: `Media not found: ${mediaPath}`
|
||||
});
|
||||
}
|
||||
return Result.err({
|
||||
type: 'serverError',
|
||||
message: `HTTP ${response.status}: ${response.statusText}`
|
||||
});
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
const contentType = response.headers.get('content-type') || 'image/svg+xml';
|
||||
|
||||
return Result.ok({ buffer, contentType });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return Result.err({
|
||||
type: 'networkError',
|
||||
message: `Failed to fetch media: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { proxyMediaRequest, getMediaContentType, getMediaCacheControl } from './MediaProxyAdapter';
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
|
||||
describe('MediaProxyAdapter', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('proxyMediaRequest', () => {
|
||||
it('should successfully proxy media and return ArrayBuffer', async () => {
|
||||
const mockBuffer = new ArrayBuffer(8);
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
arrayBuffer: vi.fn().mockResolvedValue(mockBuffer),
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await proxyMediaRequest('/media/avatar/123');
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBe(mockBuffer);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3000/media/avatar/123',
|
||||
{ method: 'GET' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 404 errors', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await proxyMediaRequest('/media/avatar/999');
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error.type).toBe('notFound');
|
||||
expect(error.message).toContain('Media not found');
|
||||
});
|
||||
|
||||
it('should handle other HTTP errors', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await proxyMediaRequest('/media/avatar/123');
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error.type).toBe('serverError');
|
||||
expect(error.message).toContain('HTTP 500');
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await proxyMediaRequest('/media/avatar/123');
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error.type).toBe('networkError');
|
||||
expect(error.message).toContain('Failed to fetch media');
|
||||
});
|
||||
|
||||
it('should use custom API base URL from environment', () => {
|
||||
process.env.API_BASE_URL = 'https://api.example.com';
|
||||
|
||||
// Just verify the function exists and can be called
|
||||
expect(typeof proxyMediaRequest).toBe('function');
|
||||
|
||||
// Reset
|
||||
delete process.env.API_BASE_URL;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMediaContentType', () => {
|
||||
it('should return image/png for all media paths', () => {
|
||||
expect(getMediaContentType('/media/avatar/123')).toBe('image/png');
|
||||
expect(getMediaContentType('/media/teams/456/logo')).toBe('image/png');
|
||||
expect(getMediaContentType('/media/leagues/789/cover')).toBe('image/png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMediaCacheControl', () => {
|
||||
it('should return public cache control with max-age', () => {
|
||||
expect(getMediaCacheControl()).toBe('public, max-age=3600');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* MediaProxyAdapter
|
||||
*
|
||||
* Handles direct HTTP proxy operations for media assets.
|
||||
* This is a special case where direct fetch is needed for binary responses.
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
|
||||
export type MediaProxyError =
|
||||
| { type: 'notFound'; message: string }
|
||||
| { type: 'serverError'; message: string }
|
||||
| { type: 'networkError'; message: string };
|
||||
|
||||
/**
|
||||
* Proxy media request to backend API
|
||||
*
|
||||
* @param mediaPath - The API path to fetch media from (e.g., "/media/avatar/123")
|
||||
* @returns Result with ArrayBuffer on success, or error on failure
|
||||
*/
|
||||
export async function proxyMediaRequest(
|
||||
mediaPath: string
|
||||
): Promise<Result<ArrayBuffer, MediaProxyError>> {
|
||||
try {
|
||||
const baseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
|
||||
const response = await fetch(`${baseUrl}${mediaPath}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return Result.err({
|
||||
type: 'notFound',
|
||||
message: `Media not found: ${mediaPath}`
|
||||
});
|
||||
}
|
||||
return Result.err({
|
||||
type: 'serverError',
|
||||
message: `HTTP ${response.status}: ${response.statusText}`
|
||||
});
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
return Result.ok(buffer);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return Result.err({
|
||||
type: 'networkError',
|
||||
message: `Failed to fetch media: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content type for media path
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function getMediaContentType(mediaPath: string): string {
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache control header value
|
||||
*/
|
||||
export function getMediaCacheControl(): string {
|
||||
return 'public, max-age=3600';
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
import type { ErrorReporter } from '@/lib/interfaces/ErrorReporter';
|
||||
import type { Logger } from '@/lib/interfaces/Logger';
|
||||
|
||||
export interface UserDto {
|
||||
id: string;
|
||||
|
||||
18
apps/website/lib/builders/view-data/AvatarViewDataBuilder.ts
Normal file
18
apps/website/lib/builders/view-data/AvatarViewDataBuilder.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* AvatarViewDataBuilder
|
||||
*
|
||||
* Transforms MediaBinaryDTO into AvatarViewData for server-side rendering.
|
||||
* Deterministic; side-effect free; no HTTP calls.
|
||||
*/
|
||||
|
||||
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
import { AvatarViewData } from '@/lib/view-data/AvatarViewData';
|
||||
|
||||
export class AvatarViewDataBuilder {
|
||||
static build(apiDto: MediaBinaryDTO): AvatarViewData {
|
||||
return {
|
||||
buffer: apiDto.buffer,
|
||||
contentType: apiDto.contentType,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* CategoryIconViewDataBuilder
|
||||
*
|
||||
* Transforms MediaBinaryDTO into CategoryIconViewData for server-side rendering.
|
||||
* Deterministic; side-effect free; no HTTP calls.
|
||||
*/
|
||||
|
||||
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
import { CategoryIconViewData } from '@/lib/view-data/CategoryIconViewData';
|
||||
|
||||
export class CategoryIconViewDataBuilder {
|
||||
static build(apiDto: MediaBinaryDTO): CategoryIconViewData {
|
||||
return {
|
||||
buffer: apiDto.buffer,
|
||||
contentType: apiDto.contentType,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
apps/website/lib/builders/view-data/DriverProfileViewData.ts
Normal file
15
apps/website/lib/builders/view-data/DriverProfileViewData.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { DriverProfileDriverSummaryDTO } from '@/lib/types/generated/DriverProfileDriverSummaryDTO';
|
||||
import type { DriverProfileStatsDTO } from '@/lib/types/generated/DriverProfileStatsDTO';
|
||||
import type { DriverProfileFinishDistributionDTO } from '@/lib/types/generated/DriverProfileFinishDistributionDTO';
|
||||
import type { DriverProfileTeamMembershipDTO } from '@/lib/types/generated/DriverProfileTeamMembershipDTO';
|
||||
import type { DriverProfileSocialSummaryDTO } from '@/lib/types/generated/DriverProfileSocialSummaryDTO';
|
||||
import type { DriverProfileExtendedProfileDTO } from '@/lib/types/generated/DriverProfileExtendedProfileDTO';
|
||||
|
||||
export interface DriverProfileViewData {
|
||||
currentDriver: DriverProfileDriverSummaryDTO | null;
|
||||
stats: DriverProfileStatsDTO | null;
|
||||
finishDistribution: DriverProfileFinishDistributionDTO | null;
|
||||
teamMemberships: DriverProfileTeamMembershipDTO[];
|
||||
socialSummary: DriverProfileSocialSummaryDTO;
|
||||
extendedProfile: DriverProfileExtendedProfileDTO | null;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||
import type { DriverProfileViewData } from './DriverProfileViewData';
|
||||
|
||||
/**
|
||||
* DriverProfileViewDataBuilder
|
||||
*
|
||||
* Transforms GetDriverProfileOutputDTO into ViewData for the driver profile page.
|
||||
* Deterministic, side-effect free, no HTTP calls.
|
||||
*/
|
||||
export class DriverProfileViewDataBuilder {
|
||||
static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData {
|
||||
return {
|
||||
currentDriver: apiDto.currentDriver || null,
|
||||
stats: apiDto.stats || null,
|
||||
finishDistribution: apiDto.finishDistribution || null,
|
||||
teamMemberships: apiDto.teamMemberships,
|
||||
socialSummary: apiDto.socialSummary,
|
||||
extendedProfile: apiDto.extendedProfile || null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DriverRankingsPageDto } from '@/lib/page-queries/page-dtos/DriverRankingsPageDto';
|
||||
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
|
||||
import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewData';
|
||||
|
||||
export class DriverRankingsViewDataBuilder {
|
||||
static build(dto: DriverRankingsPageDto | null): DriverRankingsViewData {
|
||||
if (!dto || !dto.drivers) {
|
||||
static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData {
|
||||
if (!apiDto || apiDto.length === 0) {
|
||||
return {
|
||||
drivers: [],
|
||||
podium: [],
|
||||
@@ -15,7 +15,7 @@ export class DriverRankingsViewDataBuilder {
|
||||
}
|
||||
|
||||
return {
|
||||
drivers: dto.drivers.map(driver => ({
|
||||
drivers: apiDto.map(driver => ({
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
rating: driver.rating,
|
||||
@@ -36,7 +36,7 @@ export class DriverRankingsViewDataBuilder {
|
||||
driver.rank === 3 ? 'text-amber-600' :
|
||||
'text-gray-500',
|
||||
})),
|
||||
podium: dto.drivers.slice(0, 3).map((driver, index) => {
|
||||
podium: apiDto.slice(0, 3).map((driver, index) => {
|
||||
const positions = [2, 1, 3]; // Display order: 2nd, 1st, 3rd
|
||||
const position = positions[index];
|
||||
return {
|
||||
|
||||
9
apps/website/lib/builders/view-data/DriversViewData.ts
Normal file
9
apps/website/lib/builders/view-data/DriversViewData.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
|
||||
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
|
||||
|
||||
export interface DriversViewData {
|
||||
drivers: DriverLeaderboardItemDTO[];
|
||||
totalRaces: number;
|
||||
totalWins: number;
|
||||
activeCount: number;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
|
||||
import type { DriversViewData } from './DriversViewData';
|
||||
|
||||
/**
|
||||
* DriversViewDataBuilder
|
||||
*
|
||||
* Transforms DriversLeaderboardDTO into ViewData for the drivers listing page.
|
||||
* Deterministic, side-effect free, no HTTP calls.
|
||||
*
|
||||
* This builder does NOT perform filtering or sorting - that belongs in the API.
|
||||
* If the API doesn't support filtering, it should be marked as NotImplemented.
|
||||
*/
|
||||
export class DriversViewDataBuilder {
|
||||
static build(apiDto: DriversLeaderboardDTO): DriversViewData {
|
||||
return {
|
||||
drivers: apiDto.drivers,
|
||||
totalRaces: apiDto.totalRaces,
|
||||
totalWins: apiDto.totalWins,
|
||||
activeCount: apiDto.activeCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,17 @@
|
||||
/**
|
||||
* Forgot Password View Data Builder
|
||||
*
|
||||
*
|
||||
* Transforms ForgotPasswordPageDTO into ViewData for the forgot password template.
|
||||
* Deterministic, side-effect free, no business logic.
|
||||
*/
|
||||
|
||||
import { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO';
|
||||
|
||||
export interface ForgotPasswordViewData {
|
||||
returnTo: string;
|
||||
showSuccess: boolean;
|
||||
successMessage?: string;
|
||||
magicLink?: string;
|
||||
formState: any; // Will be managed by client component
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
}
|
||||
import { ForgotPasswordViewData } from './types/ForgotPasswordViewData';
|
||||
|
||||
export class ForgotPasswordViewDataBuilder {
|
||||
static build(data: ForgotPasswordPageDTO): ForgotPasswordViewData {
|
||||
static build(apiDto: ForgotPasswordPageDTO): ForgotPasswordViewData {
|
||||
return {
|
||||
returnTo: data.returnTo,
|
||||
returnTo: apiDto.returnTo,
|
||||
showSuccess: false,
|
||||
formState: {
|
||||
fields: {
|
||||
|
||||
@@ -4,11 +4,10 @@ import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData'
|
||||
|
||||
export class LeaderboardsViewDataBuilder {
|
||||
static build(
|
||||
driversDto: { drivers: DriverLeaderboardItemDTO[] } | null,
|
||||
teamsDto: { teams: TeamListItemDTO[] } | null
|
||||
apiDto: { drivers: { drivers: DriverLeaderboardItemDTO[] }; teams: { teams: TeamListItemDTO[] } }
|
||||
): LeaderboardsViewData {
|
||||
return {
|
||||
drivers: driversDto?.drivers.slice(0, 5).map((driver, index) => ({
|
||||
drivers: apiDto.drivers.drivers.map(driver => ({
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
rating: driver.rating,
|
||||
@@ -17,9 +16,9 @@ export class LeaderboardsViewDataBuilder {
|
||||
wins: driver.wins,
|
||||
rank: driver.rank,
|
||||
avatarUrl: driver.avatarUrl || '',
|
||||
position: index + 1,
|
||||
})) || [],
|
||||
teams: teamsDto?.teams.slice(0, 5).map((team, index) => ({
|
||||
position: driver.rank,
|
||||
})),
|
||||
teams: apiDto.teams.teams.map(team => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
@@ -27,8 +26,8 @@ export class LeaderboardsViewDataBuilder {
|
||||
category: team.category,
|
||||
totalWins: team.totalWins || 0,
|
||||
logoUrl: team.logoUrl || '',
|
||||
position: index + 1,
|
||||
})) || [],
|
||||
position: 0, // API doesn't provide team ranking
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* LeagueCoverViewDataBuilder
|
||||
*
|
||||
* Transforms MediaBinaryDTO into LeagueCoverViewData for server-side rendering.
|
||||
* Deterministic; side-effect free; no HTTP calls.
|
||||
*/
|
||||
|
||||
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
import { LeagueCoverViewData } from '@/lib/view-data/LeagueCoverViewData';
|
||||
|
||||
export class LeagueCoverViewDataBuilder {
|
||||
static build(apiDto: MediaBinaryDTO): LeagueCoverViewData {
|
||||
return {
|
||||
buffer: apiDto.buffer,
|
||||
contentType: apiDto.contentType,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -22,22 +22,22 @@ export class LeagueDetailViewDataBuilder {
|
||||
}): LeagueDetailViewData {
|
||||
const { league, owner, scoringConfig, memberships, races, sponsors } = input;
|
||||
|
||||
// Calculate running races
|
||||
// Calculate running races - using available fields from RaceDTO
|
||||
const runningRaces: LiveRaceData[] = races
|
||||
.filter(r => r.status === 'running')
|
||||
.filter(r => r.name.includes('Running')) // Placeholder filter
|
||||
.map(r => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
date: r.scheduledAt,
|
||||
registeredCount: r.registeredCount,
|
||||
strengthOfField: r.strengthOfField,
|
||||
date: r.date,
|
||||
registeredCount: 0,
|
||||
strengthOfField: 0,
|
||||
}));
|
||||
|
||||
// Calculate info data
|
||||
const membersCount = Array.isArray(memberships.members) ? memberships.members.length : 0;
|
||||
const completedRacesCount = races.filter(r => r.status === 'completed').length;
|
||||
const avgSOF = races.length > 0
|
||||
? Math.round(races.reduce((sum, r) => sum + (r.strengthOfField || 0), 0) / races.length)
|
||||
const completedRacesCount = races.filter(r => r.name.includes('Completed')).length; // Placeholder
|
||||
const avgSOF = races.length > 0
|
||||
? Math.round(races.reduce((sum, r) => sum + 0, 0) / races.length)
|
||||
: null;
|
||||
|
||||
const info: LeagueInfoData = {
|
||||
@@ -47,7 +47,7 @@ export class LeagueDetailViewDataBuilder {
|
||||
racesCount: completedRacesCount,
|
||||
avgSOF,
|
||||
structure: `Solo • ${league.settings?.maxDrivers ?? 32} max`,
|
||||
scoring: scoringConfig?.name || 'Standard',
|
||||
scoring: scoringConfig?.scoringPresetId || 'Standard',
|
||||
createdAt: league.createdAt,
|
||||
discordUrl: league.socialLinks?.discordUrl,
|
||||
youtubeUrl: league.socialLinks?.youtubeUrl,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* LeagueLogoViewDataBuilder
|
||||
*
|
||||
* Transforms MediaBinaryDTO into LeagueLogoViewData for server-side rendering.
|
||||
* Deterministic; side-effect free; no HTTP calls.
|
||||
*/
|
||||
|
||||
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
import { LeagueLogoViewData } from '@/lib/view-data/LeagueLogoViewData';
|
||||
|
||||
export class LeagueLogoViewDataBuilder {
|
||||
static build(apiDto: MediaBinaryDTO): LeagueLogoViewData {
|
||||
return {
|
||||
buffer: apiDto.buffer,
|
||||
contentType: apiDto.contentType,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
||||
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
|
||||
import type { LeagueRosterAdminViewData, RosterMemberData, JoinRequestData } from '@/lib/view-data/LeagueRosterAdminViewData';
|
||||
|
||||
/**
|
||||
* LeagueRosterAdminViewDataBuilder
|
||||
*
|
||||
* Transforms API DTOs into LeagueRosterAdminViewData for server-side rendering.
|
||||
* Deterministic; side-effect free; no HTTP calls.
|
||||
*/
|
||||
export class LeagueRosterAdminViewDataBuilder {
|
||||
static build(input: {
|
||||
leagueId: string;
|
||||
members: LeagueRosterMemberDTO[];
|
||||
joinRequests: LeagueRosterJoinRequestDTO[];
|
||||
}): LeagueRosterAdminViewData {
|
||||
const { leagueId, members, joinRequests } = input;
|
||||
|
||||
// Transform members
|
||||
const rosterMembers: RosterMemberData[] = members.map(member => ({
|
||||
driverId: member.driverId,
|
||||
driver: {
|
||||
id: member.driverId,
|
||||
name: member.driver?.name || 'Unknown Driver',
|
||||
},
|
||||
role: member.role,
|
||||
joinedAt: member.joinedAt,
|
||||
}));
|
||||
|
||||
// Transform join requests
|
||||
const requests: JoinRequestData[] = joinRequests.map(req => ({
|
||||
id: req.id,
|
||||
driver: {
|
||||
id: req.driverId,
|
||||
name: 'Unknown Driver', // driver field is unknown type
|
||||
},
|
||||
requestedAt: req.requestedAt,
|
||||
message: req.message,
|
||||
}));
|
||||
|
||||
return {
|
||||
leagueId,
|
||||
members: rosterMembers,
|
||||
joinRequests: requests,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
|
||||
import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO';
|
||||
import type { LeagueScheduleViewData, ScheduleRaceData } from '@/lib/view-data/LeagueScheduleViewData';
|
||||
|
||||
/**
|
||||
* LeagueScheduleViewDataBuilder
|
||||
*
|
||||
* Transforms API DTOs into LeagueScheduleViewData for server-side rendering.
|
||||
* Deterministic; side-effect free; no HTTP calls.
|
||||
*/
|
||||
export class LeagueScheduleViewDataBuilder {
|
||||
static build(input: {
|
||||
schedule: LeagueScheduleDTO;
|
||||
seasons: LeagueSeasonSummaryDTO[];
|
||||
leagueId: string;
|
||||
}): LeagueScheduleViewData {
|
||||
const { schedule, seasons, leagueId } = input;
|
||||
|
||||
// Transform races - using available fields from RaceDTO
|
||||
const races: ScheduleRaceData[] = (schedule.races || []).map(race => ({
|
||||
id: race.id,
|
||||
name: race.name,
|
||||
track: race.leagueName || 'Unknown Track',
|
||||
car: 'Unknown Car',
|
||||
scheduledAt: race.date,
|
||||
status: 'scheduled',
|
||||
}));
|
||||
|
||||
return {
|
||||
leagueId,
|
||||
races,
|
||||
seasons: seasons.map(s => ({
|
||||
seasonId: s.seasonId,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,18 @@
|
||||
/**
|
||||
* Login View Data Builder
|
||||
*
|
||||
*
|
||||
* Transforms LoginPageDTO into ViewData for the login template.
|
||||
* Deterministic, side-effect free, no business logic.
|
||||
*/
|
||||
|
||||
import { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
|
||||
|
||||
export interface FormFieldState {
|
||||
value: string | boolean;
|
||||
error?: string;
|
||||
touched: boolean;
|
||||
validating: boolean;
|
||||
}
|
||||
|
||||
export interface FormState {
|
||||
fields: {
|
||||
email: FormFieldState;
|
||||
password: FormFieldState;
|
||||
rememberMe: FormFieldState;
|
||||
};
|
||||
isValid: boolean;
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
submitCount: number;
|
||||
}
|
||||
|
||||
export interface LoginViewData {
|
||||
returnTo: string;
|
||||
hasInsufficientPermissions: boolean;
|
||||
showPassword: boolean;
|
||||
showErrorDetails: boolean;
|
||||
formState: FormState;
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
}
|
||||
import { LoginViewData } from './types/LoginViewData';
|
||||
|
||||
export class LoginViewDataBuilder {
|
||||
static build(data: LoginPageDTO): LoginViewData {
|
||||
static build(apiDto: LoginPageDTO): LoginViewData {
|
||||
return {
|
||||
returnTo: data.returnTo,
|
||||
hasInsufficientPermissions: data.hasInsufficientPermissions,
|
||||
returnTo: apiDto.returnTo,
|
||||
hasInsufficientPermissions: apiDto.hasInsufficientPermissions,
|
||||
showPassword: false,
|
||||
showErrorDetails: false,
|
||||
formState: {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ProfileLeaguesViewData } from '@/lib/view-data/ProfileLeaguesViewData';
|
||||
|
||||
interface ProfileLeaguesPageDto {
|
||||
ownedLeagues: Array<{
|
||||
leagueId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
membershipRole: 'owner' | 'admin' | 'steward' | 'member';
|
||||
}>;
|
||||
memberLeagues: Array<{
|
||||
leagueId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
membershipRole: 'owner' | 'admin' | 'steward' | 'member';
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewData Builder for Profile Leagues page
|
||||
* Transforms Page DTO to ViewData for templates
|
||||
*/
|
||||
export class ProfileLeaguesViewDataBuilder {
|
||||
static build(apiDto: ProfileLeaguesPageDto): ProfileLeaguesViewData {
|
||||
return {
|
||||
ownedLeagues: apiDto.ownedLeagues.map((league: { leagueId: string; name: string; description: string; membershipRole: 'owner' | 'admin' | 'steward' | 'member'; }) => ({
|
||||
leagueId: league.leagueId,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
membershipRole: league.membershipRole,
|
||||
})),
|
||||
memberLeagues: apiDto.memberLeagues.map((league: { leagueId: string; name: string; description: string; membershipRole: 'owner' | 'admin' | 'steward' | 'member'; }) => ({
|
||||
leagueId: league.leagueId,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
membershipRole: league.membershipRole,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { RaceDetailViewData, RaceDetailRace, RaceDetailLeague, RaceDetailEntry, RaceDetailRegistration, RaceDetailUserResult } from '@/lib/view-data/races/RaceDetailViewData';
|
||||
|
||||
/**
|
||||
* Race Detail View Data Builder
|
||||
*
|
||||
* Transforms API DTO into ViewData for the race detail template.
|
||||
* Deterministic, side-effect free.
|
||||
*/
|
||||
export class RaceDetailViewDataBuilder {
|
||||
static build(apiDto: any): RaceDetailViewData {
|
||||
if (!apiDto || !apiDto.race) {
|
||||
return {
|
||||
race: {
|
||||
id: '',
|
||||
track: '',
|
||||
car: '',
|
||||
scheduledAt: '',
|
||||
status: 'scheduled',
|
||||
sessionType: 'race',
|
||||
},
|
||||
entryList: [],
|
||||
registration: {
|
||||
isUserRegistered: false,
|
||||
canRegister: false,
|
||||
},
|
||||
canReopenRace: false,
|
||||
};
|
||||
}
|
||||
|
||||
const race: RaceDetailRace = {
|
||||
id: apiDto.race.id,
|
||||
track: apiDto.race.track,
|
||||
car: apiDto.race.car,
|
||||
scheduledAt: apiDto.race.scheduledAt,
|
||||
status: apiDto.race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
|
||||
sessionType: apiDto.race.sessionType,
|
||||
};
|
||||
|
||||
const league: RaceDetailLeague | undefined = apiDto.league ? {
|
||||
id: apiDto.league.id,
|
||||
name: apiDto.league.name,
|
||||
description: apiDto.league.description || undefined,
|
||||
settings: {
|
||||
maxDrivers: apiDto.league.settings?.maxDrivers || 32,
|
||||
qualifyingFormat: apiDto.league.settings?.qualifyingFormat || 'Open',
|
||||
},
|
||||
} : undefined;
|
||||
|
||||
const entryList: RaceDetailEntry[] = apiDto.entryList.map((entry: any) => ({
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
avatarUrl: entry.avatarUrl,
|
||||
country: entry.country,
|
||||
rating: entry.rating,
|
||||
isCurrentUser: entry.isCurrentUser,
|
||||
}));
|
||||
|
||||
const registration: RaceDetailRegistration = {
|
||||
isUserRegistered: apiDto.registration.isUserRegistered,
|
||||
canRegister: apiDto.registration.canRegister,
|
||||
};
|
||||
|
||||
const userResult: RaceDetailUserResult | undefined = apiDto.userResult ? {
|
||||
position: apiDto.userResult.position,
|
||||
startPosition: apiDto.userResult.startPosition,
|
||||
positionChange: apiDto.userResult.positionChange,
|
||||
incidents: apiDto.userResult.incidents,
|
||||
isClean: apiDto.userResult.isClean,
|
||||
isPodium: apiDto.userResult.isPodium,
|
||||
ratingChange: apiDto.userResult.ratingChange,
|
||||
} : undefined;
|
||||
|
||||
return {
|
||||
race,
|
||||
league,
|
||||
entryList,
|
||||
registration,
|
||||
userResult,
|
||||
canReopenRace: apiDto.canReopenRace || false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { RaceResultsViewData, RaceResultsResult, RaceResultsPenalty } from '@/lib/view-data/races/RaceResultsViewData';
|
||||
|
||||
/**
|
||||
* Race Results View Data Builder
|
||||
*
|
||||
* Transforms API DTO into ViewData for the race results template.
|
||||
* Deterministic, side-effect free.
|
||||
*/
|
||||
export class RaceResultsViewDataBuilder {
|
||||
static build(apiDto: any): RaceResultsViewData {
|
||||
if (!apiDto) {
|
||||
return {
|
||||
raceSOF: null,
|
||||
results: [],
|
||||
penalties: [],
|
||||
pointsSystem: {},
|
||||
fastestLapTime: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Transform results
|
||||
const results: RaceResultsResult[] = (apiDto.results || []).map((result: any) => ({
|
||||
position: result.position,
|
||||
driverId: result.driverId,
|
||||
driverName: result.driverName,
|
||||
driverAvatar: result.avatarUrl,
|
||||
country: result.country || 'US',
|
||||
car: result.car || 'Unknown',
|
||||
laps: result.laps || 0,
|
||||
time: result.time || '0:00.00',
|
||||
fastestLap: result.fastestLap?.toString() || '0.00',
|
||||
points: result.points || 0,
|
||||
incidents: result.incidents || 0,
|
||||
isCurrentUser: result.isCurrentUser || false,
|
||||
}));
|
||||
|
||||
// Transform penalties
|
||||
const penalties: RaceResultsPenalty[] = (apiDto.penalties || []).map((penalty: any) => ({
|
||||
driverId: penalty.driverId,
|
||||
driverName: penalty.driverName || 'Unknown',
|
||||
type: penalty.type as 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points',
|
||||
value: penalty.value || 0,
|
||||
reason: penalty.reason || 'Penalty applied',
|
||||
notes: penalty.notes,
|
||||
}));
|
||||
|
||||
return {
|
||||
raceTrack: apiDto.race?.track,
|
||||
raceScheduledAt: apiDto.race?.scheduledAt,
|
||||
totalDrivers: apiDto.stats?.totalDrivers,
|
||||
leagueName: apiDto.league?.name,
|
||||
raceSOF: apiDto.strengthOfField || null,
|
||||
results,
|
||||
penalties,
|
||||
pointsSystem: apiDto.pointsSystem || {},
|
||||
fastestLapTime: apiDto.fastestLapTime || 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { RaceStewardingViewData, Protest, Penalty, Driver } from '@/lib/view-data/races/RaceStewardingViewData';
|
||||
|
||||
/**
|
||||
* Race Stewarding View Data Builder
|
||||
*
|
||||
* Transforms API DTO into ViewData for the race stewarding template.
|
||||
* Deterministic, side-effect free.
|
||||
*/
|
||||
export class RaceStewardingViewDataBuilder {
|
||||
static build(apiDto: any): RaceStewardingViewData {
|
||||
if (!apiDto) {
|
||||
return {
|
||||
race: null,
|
||||
league: null,
|
||||
pendingProtests: [],
|
||||
resolvedProtests: [],
|
||||
penalties: [],
|
||||
driverMap: {},
|
||||
pendingCount: 0,
|
||||
resolvedCount: 0,
|
||||
penaltiesCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const race = apiDto.race ? {
|
||||
id: apiDto.race.id,
|
||||
track: apiDto.race.track,
|
||||
scheduledAt: apiDto.race.scheduledAt,
|
||||
} : null;
|
||||
|
||||
const league = apiDto.league ? {
|
||||
id: apiDto.league.id,
|
||||
} : null;
|
||||
|
||||
const pendingProtests: Protest[] = (apiDto.pendingProtests || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
protestingDriverId: p.protestingDriverId,
|
||||
accusedDriverId: p.accusedDriverId,
|
||||
incident: {
|
||||
lap: p.incident?.lap || 0,
|
||||
description: p.incident?.description || '',
|
||||
},
|
||||
filedAt: p.filedAt,
|
||||
status: p.status,
|
||||
proofVideoUrl: p.proofVideoUrl,
|
||||
decisionNotes: p.decisionNotes,
|
||||
}));
|
||||
|
||||
const resolvedProtests: Protest[] = (apiDto.resolvedProtests || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
protestingDriverId: p.protestingDriverId,
|
||||
accusedDriverId: p.accusedDriverId,
|
||||
incident: {
|
||||
lap: p.incident?.lap || 0,
|
||||
description: p.incident?.description || '',
|
||||
},
|
||||
filedAt: p.filedAt,
|
||||
status: p.status,
|
||||
proofVideoUrl: p.proofVideoUrl,
|
||||
decisionNotes: p.decisionNotes,
|
||||
}));
|
||||
|
||||
const penalties: Penalty[] = (apiDto.penalties || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
driverId: p.driverId,
|
||||
type: p.type,
|
||||
value: p.value || 0,
|
||||
reason: p.reason || '',
|
||||
notes: p.notes,
|
||||
}));
|
||||
|
||||
const driverMap: Record<string, Driver> = apiDto.driverMap || {};
|
||||
|
||||
return {
|
||||
race,
|
||||
league,
|
||||
pendingProtests,
|
||||
resolvedProtests,
|
||||
penalties,
|
||||
driverMap,
|
||||
pendingCount: apiDto.pendingCount || pendingProtests.length,
|
||||
resolvedCount: apiDto.resolvedCount || resolvedProtests.length,
|
||||
penaltiesCount: apiDto.penaltiesCount || penalties.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { RacesAllViewData, RacesAllRace } from '@/lib/view-data/races/RacesAllViewData';
|
||||
|
||||
/**
|
||||
* Races All View Data Builder
|
||||
*
|
||||
* Transforms API DTO into ViewData for the all races template.
|
||||
* Deterministic, side-effect free.
|
||||
*/
|
||||
export class RacesAllViewDataBuilder {
|
||||
static build(apiDto: any): RacesAllViewData {
|
||||
const races = apiDto.races.map((race: any) => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt,
|
||||
status: race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
|
||||
sessionType: 'race',
|
||||
leagueId: race.leagueId,
|
||||
leagueName: race.leagueName,
|
||||
strengthOfField: race.strengthOfField ?? undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
races,
|
||||
};
|
||||
}
|
||||
}
|
||||
39
apps/website/lib/builders/view-data/RacesViewDataBuilder.ts
Normal file
39
apps/website/lib/builders/view-data/RacesViewDataBuilder.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { RacesViewData, RacesRace } from '@/lib/view-data/races/RacesViewData';
|
||||
|
||||
/**
|
||||
* Races View Data Builder
|
||||
*
|
||||
* Transforms API DTO into ViewData for the races template.
|
||||
* Deterministic, side-effect free.
|
||||
*/
|
||||
export class RacesViewDataBuilder {
|
||||
static build(apiDto: any): RacesViewData {
|
||||
const races = apiDto.races.map((race: any) => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt,
|
||||
status: race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
|
||||
sessionType: 'race',
|
||||
leagueId: race.leagueId,
|
||||
leagueName: race.leagueName,
|
||||
strengthOfField: race.strengthOfField ?? undefined,
|
||||
isUpcoming: race.status === 'scheduled',
|
||||
isLive: race.status === 'running',
|
||||
isPast: race.status === 'completed',
|
||||
}));
|
||||
|
||||
const totalCount = races.length;
|
||||
const scheduledRaces = races.filter((r: RacesRace) => r.isUpcoming);
|
||||
const runningRaces = races.filter((r: RacesRace) => r.isLive);
|
||||
const completedRaces = races.filter((r: RacesRace) => r.isPast);
|
||||
|
||||
return {
|
||||
races,
|
||||
totalCount,
|
||||
scheduledRaces,
|
||||
runningRaces,
|
||||
completedRaces,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,18 @@
|
||||
/**
|
||||
* Reset Password View Data Builder
|
||||
*
|
||||
*
|
||||
* Transforms ResetPasswordPageDTO into ViewData for the reset password template.
|
||||
* Deterministic, side-effect free, no business logic.
|
||||
*/
|
||||
|
||||
import { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO';
|
||||
|
||||
export interface ResetPasswordViewData {
|
||||
token: string;
|
||||
returnTo: string;
|
||||
showSuccess: boolean;
|
||||
successMessage?: string;
|
||||
formState: any; // Will be managed by client component
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
}
|
||||
import { ResetPasswordViewData } from './types/ResetPasswordViewData';
|
||||
|
||||
export class ResetPasswordViewDataBuilder {
|
||||
static build(data: ResetPasswordPageDTO): ResetPasswordViewData {
|
||||
static build(apiDto: ResetPasswordPageDTO): ResetPasswordViewData {
|
||||
return {
|
||||
token: data.token,
|
||||
returnTo: data.returnTo,
|
||||
token: apiDto.token,
|
||||
returnTo: apiDto.returnTo,
|
||||
showSuccess: false,
|
||||
formState: {
|
||||
fields: {
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
/**
|
||||
* Signup View Data Builder
|
||||
*
|
||||
*
|
||||
* Transforms SignupPageDTO into ViewData for the signup template.
|
||||
* Deterministic, side-effect free, no business logic.
|
||||
*/
|
||||
|
||||
import { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO';
|
||||
|
||||
export interface SignupViewData {
|
||||
returnTo: string;
|
||||
formState: any; // Will be managed by client component
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
}
|
||||
import { SignupViewData } from './types/SignupViewData';
|
||||
|
||||
export class SignupViewDataBuilder {
|
||||
static build(data: SignupPageDTO): SignupViewData {
|
||||
static build(apiDto: SignupPageDTO): SignupViewData {
|
||||
return {
|
||||
returnTo: data.returnTo,
|
||||
returnTo: apiDto.returnTo,
|
||||
formState: {
|
||||
fields: {
|
||||
firstName: { value: '', error: undefined, touched: false, validating: false },
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* SponsorLogoViewDataBuilder
|
||||
*
|
||||
* Transforms MediaBinaryDTO into SponsorLogoViewData for server-side rendering.
|
||||
* Deterministic; side-effect free; no HTTP calls.
|
||||
*/
|
||||
|
||||
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
import { SponsorLogoViewData } from '@/lib/view-data/SponsorLogoViewData';
|
||||
|
||||
export class SponsorLogoViewDataBuilder {
|
||||
static build(apiDto: MediaBinaryDTO): SponsorLogoViewData {
|
||||
return {
|
||||
buffer: apiDto.buffer,
|
||||
contentType: apiDto.contentType,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,26 @@
|
||||
import type { SponsorshipRequestDTO } from '@/lib/types/generated/SponsorshipRequestDTO';
|
||||
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
|
||||
import type { SponsorshipRequestsViewData } from '@/lib/view-data/SponsorshipRequestsViewData';
|
||||
|
||||
export interface SponsorshipRequestsViewData {
|
||||
requests: SponsorshipRequestDTO[];
|
||||
isEmpty: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewData Builder for Sponsorship Requests page
|
||||
* Transforms API DTO to ViewData for templates
|
||||
*/
|
||||
export class SponsorshipRequestsPageViewDataBuilder {
|
||||
build(queryResult: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData {
|
||||
static build(apiDto: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData {
|
||||
return {
|
||||
requests: queryResult.requests,
|
||||
isEmpty: queryResult.requests.length === 0,
|
||||
sections: [{
|
||||
entityType: apiDto.entityType as 'driver' | 'team' | 'season',
|
||||
entityId: apiDto.entityId,
|
||||
entityName: apiDto.entityType,
|
||||
requests: apiDto.requests.map(request => ({
|
||||
id: request.id,
|
||||
sponsorId: request.sponsorId,
|
||||
sponsorName: request.sponsorName,
|
||||
sponsorLogoUrl: request.sponsorLogo || null,
|
||||
message: request.message || null,
|
||||
createdAtIso: request.createdAt,
|
||||
})),
|
||||
}],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import type { SponsorshipRequestsPageDto } from '@/lib/page-queries/page-queries/SponsorshipRequestsPageQuery';
|
||||
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
|
||||
import type { SponsorshipRequestsViewData } from '@/lib/view-data/SponsorshipRequestsViewData';
|
||||
|
||||
export class SponsorshipRequestsViewDataBuilder {
|
||||
static build(apiDto: SponsorshipRequestsPageDto): SponsorshipRequestsViewData {
|
||||
static build(apiDto: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData {
|
||||
return {
|
||||
sections: apiDto.sections.map((section) => ({
|
||||
entityType: section.entityType,
|
||||
entityId: section.entityId,
|
||||
entityName: section.entityName,
|
||||
requests: section.requests.map((request) => ({
|
||||
id: request.requestId,
|
||||
sponsorId: request.sponsorId,
|
||||
sponsorName: request.sponsorName,
|
||||
sponsorLogoUrl: null,
|
||||
message: request.message,
|
||||
createdAtIso: request.createdAtIso,
|
||||
})),
|
||||
})),
|
||||
sections: [
|
||||
{
|
||||
entityType: apiDto.entityType as 'driver' | 'team' | 'season',
|
||||
entityId: apiDto.entityId,
|
||||
entityName: apiDto.entityType === 'driver' ? 'Driver' : apiDto.entityType,
|
||||
requests: apiDto.requests.map((request) => ({
|
||||
id: request.id,
|
||||
sponsorId: request.sponsorId,
|
||||
sponsorName: request.sponsorName,
|
||||
sponsorLogoUrl: request.sponsorLogo || null,
|
||||
message: request.message || null,
|
||||
createdAtIso: request.createdAt,
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,28 @@ import type { TeamDetailViewData, TeamDetailData, TeamMemberData, SponsorMetric,
|
||||
import { Users, Zap, Calendar } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* TeamDetailPresenter - Client-side presenter for team detail page
|
||||
* Transforms PageQuery DTO into ViewData for the template
|
||||
* Deterministic; no hooks; no side effects
|
||||
* TeamDetailViewDataBuilder - Transforms TeamDetailPageDto into ViewData
|
||||
* Deterministic; side-effect free; no HTTP calls
|
||||
*/
|
||||
export class TeamDetailPresenter {
|
||||
static createViewData(pageDto: TeamDetailPageDto): TeamDetailViewData {
|
||||
export class TeamDetailViewDataBuilder {
|
||||
static build(apiDto: TeamDetailPageDto): TeamDetailViewData {
|
||||
const team: TeamDetailData = {
|
||||
id: pageDto.team.id,
|
||||
name: pageDto.team.name,
|
||||
tag: pageDto.team.tag,
|
||||
description: pageDto.team.description,
|
||||
ownerId: pageDto.team.ownerId,
|
||||
leagues: pageDto.team.leagues,
|
||||
createdAt: pageDto.team.createdAt,
|
||||
specialization: pageDto.team.specialization,
|
||||
region: pageDto.team.region,
|
||||
languages: pageDto.team.languages,
|
||||
category: pageDto.team.category,
|
||||
membership: pageDto.team.membership,
|
||||
canManage: pageDto.team.canManage,
|
||||
id: apiDto.team.id,
|
||||
name: apiDto.team.name,
|
||||
tag: apiDto.team.tag,
|
||||
description: apiDto.team.description,
|
||||
ownerId: apiDto.team.ownerId,
|
||||
leagues: apiDto.team.leagues,
|
||||
createdAt: apiDto.team.createdAt,
|
||||
specialization: apiDto.team.specialization,
|
||||
region: apiDto.team.region,
|
||||
languages: apiDto.team.languages,
|
||||
category: apiDto.team.category,
|
||||
membership: apiDto.team.membership,
|
||||
canManage: apiDto.team.canManage,
|
||||
};
|
||||
|
||||
const memberships: TeamMemberData[] = pageDto.memberships.map(membership => ({
|
||||
const memberships: TeamMemberData[] = apiDto.memberships.map((membership) => ({
|
||||
driverId: membership.driverId,
|
||||
driverName: membership.driverName,
|
||||
role: membership.role,
|
||||
@@ -35,7 +34,7 @@ export class TeamDetailPresenter {
|
||||
}));
|
||||
|
||||
// Calculate isAdmin based on current driver's role
|
||||
const currentDriverMembership = memberships.find(m => m.driverId === pageDto.currentDriverId);
|
||||
const currentDriverMembership = memberships.find(m => m.driverId === apiDto.currentDriverId);
|
||||
const isAdmin = currentDriverMembership?.role === 'owner' || currentDriverMembership?.role === 'manager';
|
||||
|
||||
// Build sponsor metrics
|
||||
@@ -78,7 +77,7 @@ export class TeamDetailPresenter {
|
||||
return {
|
||||
team,
|
||||
memberships,
|
||||
currentDriverId: pageDto.currentDriverId,
|
||||
currentDriverId: apiDto.currentDriverId,
|
||||
isAdmin,
|
||||
teamMetrics,
|
||||
tabs,
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* TeamLogoViewDataBuilder
|
||||
*
|
||||
* Transforms MediaBinaryDTO into TeamLogoViewData for server-side rendering.
|
||||
* Deterministic; side-effect free; no HTTP calls.
|
||||
*/
|
||||
|
||||
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
import { TeamLogoViewData } from '@/lib/view-data/TeamLogoViewData';
|
||||
|
||||
export class TeamLogoViewDataBuilder {
|
||||
static build(apiDto: MediaBinaryDTO): TeamLogoViewData {
|
||||
return {
|
||||
buffer: apiDto.buffer,
|
||||
contentType: apiDto.contentType,
|
||||
};
|
||||
}
|
||||
}
|
||||
20
apps/website/lib/builders/view-data/TeamsViewDataBuilder.ts
Normal file
20
apps/website/lib/builders/view-data/TeamsViewDataBuilder.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { TeamsPageDto } from '@/lib/page-queries/page-queries/TeamsPageQuery';
|
||||
import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData';
|
||||
|
||||
/**
|
||||
* TeamsViewDataBuilder - Transforms TeamsPageDto into ViewData for TeamsTemplate
|
||||
* Deterministic; side-effect free; no HTTP calls
|
||||
*/
|
||||
export class TeamsViewDataBuilder {
|
||||
static build(apiDto: TeamsPageDto): TeamsViewData {
|
||||
const teams: TeamSummaryData[] = apiDto.teams.map((team): TeamSummaryData => ({
|
||||
teamId: team.id,
|
||||
teamName: team.name,
|
||||
leagueName: team.leagues[0] || '',
|
||||
memberCount: team.memberCount,
|
||||
logoUrl: team.logoUrl,
|
||||
}));
|
||||
|
||||
return { teams };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* TrackImageViewDataBuilder
|
||||
*
|
||||
* Transforms MediaBinaryDTO into TrackImageViewData for server-side rendering.
|
||||
* Deterministic; side-effect free; no HTTP calls.
|
||||
*/
|
||||
|
||||
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
|
||||
import { TrackImageViewData } from '@/lib/view-data/TrackImageViewData';
|
||||
|
||||
export class TrackImageViewDataBuilder {
|
||||
static build(apiDto: MediaBinaryDTO): TrackImageViewData {
|
||||
return {
|
||||
buffer: apiDto.buffer,
|
||||
contentType: apiDto.contentType,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Forgot Password View Data
|
||||
*
|
||||
* ViewData for the forgot password template.
|
||||
*/
|
||||
|
||||
export interface ForgotPasswordViewData {
|
||||
returnTo: string;
|
||||
showSuccess: boolean;
|
||||
successMessage?: string;
|
||||
magicLink?: string;
|
||||
formState: any; // Will be managed by client component
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
}
|
||||
12
apps/website/lib/builders/view-data/types/FormFieldState.ts
Normal file
12
apps/website/lib/builders/view-data/types/FormFieldState.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Form Field State
|
||||
*
|
||||
* State for a single form field.
|
||||
*/
|
||||
|
||||
export interface FormFieldState {
|
||||
value: string | boolean;
|
||||
error?: string;
|
||||
touched: boolean;
|
||||
validating: boolean;
|
||||
}
|
||||
19
apps/website/lib/builders/view-data/types/FormState.ts
Normal file
19
apps/website/lib/builders/view-data/types/FormState.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Form State
|
||||
*
|
||||
* Complete state for a form.
|
||||
*/
|
||||
|
||||
import { FormFieldState } from './FormFieldState';
|
||||
|
||||
export interface FormState {
|
||||
fields: {
|
||||
email: FormFieldState;
|
||||
password: FormFieldState;
|
||||
rememberMe: FormFieldState;
|
||||
};
|
||||
isValid: boolean;
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
submitCount: number;
|
||||
}
|
||||
17
apps/website/lib/builders/view-data/types/LoginViewData.ts
Normal file
17
apps/website/lib/builders/view-data/types/LoginViewData.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Login View Data
|
||||
*
|
||||
* ViewData for the login template.
|
||||
*/
|
||||
|
||||
import { FormState } from './FormState';
|
||||
|
||||
export interface LoginViewData {
|
||||
returnTo: string;
|
||||
hasInsufficientPermissions: boolean;
|
||||
showPassword: boolean;
|
||||
showErrorDetails: boolean;
|
||||
formState: FormState;
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Reset Password View Data
|
||||
*
|
||||
* ViewData for the reset password template.
|
||||
*/
|
||||
|
||||
export interface ResetPasswordViewData {
|
||||
token: string;
|
||||
returnTo: string;
|
||||
showSuccess: boolean;
|
||||
successMessage?: string;
|
||||
formState: any; // Will be managed by client component
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
}
|
||||
12
apps/website/lib/builders/view-data/types/SignupViewData.ts
Normal file
12
apps/website/lib/builders/view-data/types/SignupViewData.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Signup View Data
|
||||
*
|
||||
* ViewData for the signup template.
|
||||
*/
|
||||
|
||||
export interface SignupViewData {
|
||||
returnTo: string;
|
||||
formState: any; // Will be managed by client component
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
|
||||
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
|
||||
import { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
|
||||
|
||||
/**
|
||||
* DriversViewModelBuilder
|
||||
*
|
||||
* Transforms DriversLeaderboardDTO into DriverLeaderboardViewModel.
|
||||
* Deterministic, side-effect free, no HTTP calls.
|
||||
*/
|
||||
export class DriversViewModelBuilder {
|
||||
static build(apiDto: DriversLeaderboardDTO): DriverLeaderboardViewModel {
|
||||
return new DriverLeaderboardViewModel({
|
||||
drivers: apiDto.drivers,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* Deterministic, side-effect free, no business logic.
|
||||
*/
|
||||
|
||||
import { ForgotPasswordViewData } from '@/lib/builders/view-data/ForgotPasswordViewDataBuilder';
|
||||
import { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData';
|
||||
import { ForgotPasswordViewModel, ForgotPasswordFormState } from '@/lib/view-models/auth/ForgotPasswordViewModel';
|
||||
|
||||
export class ForgotPasswordViewModelBuilder {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Deterministic, side-effect free, no business logic.
|
||||
*/
|
||||
|
||||
import { LoginViewData } from '@/lib/builders/view-data/LoginViewDataBuilder';
|
||||
import { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData';
|
||||
import { LoginViewModel, LoginFormState, LoginUIState } from '@/lib/view-models/auth/LoginViewModel';
|
||||
|
||||
export class LoginViewModelBuilder {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Deterministic, side-effect free, no business logic.
|
||||
*/
|
||||
|
||||
import { ResetPasswordViewData } from '@/lib/builders/view-data/ResetPasswordViewDataBuilder';
|
||||
import { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData';
|
||||
import { ResetPasswordViewModel, ResetPasswordFormState, ResetPasswordUIState } from '@/lib/view-models/auth/ResetPasswordViewModel';
|
||||
|
||||
export class ResetPasswordViewModelBuilder {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Deterministic, side-effect free, no business logic.
|
||||
*/
|
||||
|
||||
import { SignupViewData } from '@/lib/builders/view-data/SignupViewDataBuilder';
|
||||
import { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData';
|
||||
import { SignupViewModel, SignupFormState, SignupUIState } from '@/lib/view-models/auth/SignupViewModel';
|
||||
|
||||
export class SignupViewModelBuilder {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* These are mapped from DomainErrors by Mutations.
|
||||
*/
|
||||
|
||||
export type MutationError =
|
||||
export type MutationError =
|
||||
| 'userNotFound' // User doesn't exist
|
||||
| 'noPermission' // Insufficient permissions
|
||||
| 'invalidData' // Validation failed
|
||||
@@ -14,6 +14,7 @@ export type MutationError =
|
||||
| 'createFailed' // Create operation failed
|
||||
| 'networkError' // Network/communication error
|
||||
| 'serverError' // Generic server error
|
||||
| 'notImplemented' // Feature not implemented
|
||||
| 'unknown'; // Unknown error
|
||||
|
||||
// Helper to map DomainError to MutationError
|
||||
@@ -39,6 +40,9 @@ export function mapToMutationError(domainError: any): MutationError {
|
||||
case 'networkError':
|
||||
case 'NetworkError':
|
||||
return 'networkError';
|
||||
case 'notImplemented':
|
||||
case 'NotImplementedError':
|
||||
return 'notImplemented';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export type DomainError =
|
||||
| { type: 'validation'; message: string }
|
||||
| { type: 'serverError'; message: string }
|
||||
| { type: 'networkError'; message: string }
|
||||
| { type: 'notImplemented'; message: string }
|
||||
| { type: 'unknown'; message: string };
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import { RaceModule } from './modules/race.module';
|
||||
import { LandingModule } from './modules/landing.module';
|
||||
import { PolicyModule } from './modules/policy.module';
|
||||
import { SponsorModule } from './modules/sponsor.module';
|
||||
import { AnalyticsModule } from './modules/analytics.module';
|
||||
|
||||
/**
|
||||
* Creates and configures the root DI container
|
||||
@@ -29,7 +30,8 @@ export function createContainer(): Container {
|
||||
RaceModule,
|
||||
LandingModule,
|
||||
PolicyModule,
|
||||
SponsorModule
|
||||
SponsorModule,
|
||||
AnalyticsModule
|
||||
);
|
||||
|
||||
return container;
|
||||
|
||||
15
apps/website/lib/di/modules/analytics.module.ts
Normal file
15
apps/website/lib/di/modules/analytics.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { DashboardService } from '@/lib/services/analytics/DashboardService';
|
||||
|
||||
import {
|
||||
DASHBOARD_SERVICE_TOKEN,
|
||||
} from '../tokens';
|
||||
|
||||
export const AnalyticsModule = new ContainerModule((options) => {
|
||||
const bind = options.bind;
|
||||
|
||||
// Dashboard Service - creates its own dependencies per contract
|
||||
bind<DashboardService>(DASHBOARD_SERVICE_TOKEN)
|
||||
.to(DashboardService)
|
||||
.inSingletonScope();
|
||||
});
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
RACE_STEWARDING_SERVICE_TOKEN,
|
||||
RACE_API_CLIENT_TOKEN,
|
||||
PROTEST_API_CLIENT_TOKEN,
|
||||
PENALTY_API_CLIENT_TOKEN
|
||||
PENALTY_API_CLIENT_TOKEN,
|
||||
} from '../tokens';
|
||||
|
||||
export const RaceModule = new ContainerModule((options) => {
|
||||
const bind = options.bind;
|
||||
|
||||
// Race Service
|
||||
// Race Service - creates its own dependencies per contract
|
||||
bind<RaceService>(RACE_SERVICE_TOKEN)
|
||||
.toDynamicValue((ctx) => {
|
||||
const raceApiClient = ctx.get<RacesApiClient>(RACE_API_CLIENT_TOKEN);
|
||||
@@ -27,21 +27,13 @@ export const RaceModule = new ContainerModule((options) => {
|
||||
})
|
||||
.inSingletonScope();
|
||||
|
||||
// Race Results Service
|
||||
// Race Results Service - creates its own dependencies per contract
|
||||
bind<RaceResultsService>(RACE_RESULTS_SERVICE_TOKEN)
|
||||
.toDynamicValue((ctx) => {
|
||||
const raceApiClient = ctx.get<RacesApiClient>(RACE_API_CLIENT_TOKEN);
|
||||
return new RaceResultsService(raceApiClient);
|
||||
})
|
||||
.to(RaceResultsService)
|
||||
.inSingletonScope();
|
||||
|
||||
// Race Stewarding Service
|
||||
// Race Stewarding Service - creates its own dependencies per contract
|
||||
bind<RaceStewardingService>(RACE_STEWARDING_SERVICE_TOKEN)
|
||||
.toDynamicValue((ctx) => {
|
||||
const raceApiClient = ctx.get<RacesApiClient>(RACE_API_CLIENT_TOKEN);
|
||||
const protestApiClient = ctx.get<ProtestsApiClient>(PROTEST_API_CLIENT_TOKEN);
|
||||
const penaltyApiClient = ctx.get<PenaltiesApiClient>(PENALTY_API_CLIENT_TOKEN);
|
||||
return new RaceStewardingService(raceApiClient, protestApiClient, penaltyApiClient);
|
||||
})
|
||||
.to(RaceStewardingService)
|
||||
.inSingletonScope();
|
||||
});
|
||||
@@ -1,15 +1,16 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { TEAM_SERVICE_TOKEN, TEAM_API_CLIENT_TOKEN } from '../tokens';
|
||||
import { TEAM_SERVICE_TOKEN, TEAM_JOIN_SERVICE_TOKEN } from '../tokens';
|
||||
import { TeamService } from '@/lib/services/teams/TeamService';
|
||||
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
|
||||
import { TeamJoinService } from '@/lib/services/teams/TeamJoinService';
|
||||
|
||||
export const TeamModule = new ContainerModule((options) => {
|
||||
const bind = options.bind;
|
||||
|
||||
bind(TEAM_SERVICE_TOKEN)
|
||||
.toDynamicValue((ctx) => {
|
||||
const apiClient = ctx.get<TeamsApiClient>(TEAM_API_CLIENT_TOKEN);
|
||||
return new TeamService(apiClient);
|
||||
})
|
||||
.to(TeamService)
|
||||
.inSingletonScope();
|
||||
|
||||
bind(TEAM_JOIN_SERVICE_TOKEN)
|
||||
.to(TeamJoinService)
|
||||
.inSingletonScope();
|
||||
});
|
||||
@@ -29,14 +29,24 @@ export const DASHBOARD_API_CLIENT_TOKEN = Symbol.for('Api.DashboardClient');
|
||||
import type { LeagueService } from '@/lib/services/leagues/LeagueService';
|
||||
import type { LeagueMembershipService } from '@/lib/services/leagues/LeagueMembershipService';
|
||||
import type { LeagueWalletService } from '@/lib/services/leagues/LeagueWalletService';
|
||||
import type { LeagueSettingsService } from '@/lib/services/leagues/LeagueSettingsService';
|
||||
import type { LeagueStewardingService } from '@/lib/services/leagues/LeagueStewardingService';
|
||||
import type { DriverService } from '@/lib/services/drivers/DriverService';
|
||||
// These services are created as needed
|
||||
import type { AuthService } from '@/lib/services/auth/AuthService';
|
||||
import type { SessionService } from '@/lib/services/auth/SessionService';
|
||||
import type { PenaltyService } from '@/lib/services/penalties/PenaltyService';
|
||||
import type { PolicyService } from '@/lib/services/policy/PolicyService';
|
||||
import type { AdminService } from '@/lib/services/admin/AdminService';
|
||||
import type { OnboardingService } from '@/lib/services/onboarding/OnboardingService';
|
||||
import type { SponsorService } from '@/lib/services/sponsors/SponsorService';
|
||||
import type { ProtestService } from '@/lib/services/protests/ProtestService';
|
||||
import type { LandingService } from '@/lib/services/landing/LandingService';
|
||||
import type { TeamService } from '@/lib/services/teams/TeamService';
|
||||
import type { TeamJoinService } from '@/lib/services/teams/TeamJoinService';
|
||||
import type { RaceService } from '@/lib/services/races/RaceService';
|
||||
import type { RaceResultsService } from '@/lib/services/races/RaceResultsService';
|
||||
import type { RaceStewardingService } from '@/lib/services/races/RaceStewardingService';
|
||||
import type { DashboardService } from '@/lib/services/analytics/DashboardService';
|
||||
|
||||
export const LEAGUE_SERVICE_TOKEN = Symbol.for('Service.League') as symbol & { type: LeagueService };
|
||||
export const LEAGUE_MEMBERSHIP_SERVICE_TOKEN = Symbol.for('Service.LeagueMembership') as symbol & { type: LeagueMembershipService };
|
||||
@@ -44,11 +54,14 @@ export const LEAGUE_WALLET_SERVICE_TOKEN = Symbol.for('Service.LeagueWallet') as
|
||||
|
||||
export const DRIVER_SERVICE_TOKEN = Symbol.for('Service.Driver') as symbol & { type: DriverService };
|
||||
|
||||
export const TEAM_SERVICE_TOKEN = Symbol.for('Service.Team');
|
||||
export const TEAM_SERVICE_TOKEN = Symbol.for('Service.Team') as symbol & { type: TeamService };
|
||||
export const TEAM_JOIN_SERVICE_TOKEN = Symbol.for('Service.TeamJoin') as symbol & { type: TeamJoinService };
|
||||
|
||||
export const RACE_SERVICE_TOKEN = Symbol.for('Service.Race');
|
||||
export const RACE_SERVICE_TOKEN = Symbol.for('Service.Race') as symbol & { type: RaceService };
|
||||
export const RACE_RESULTS_SERVICE_TOKEN = Symbol.for('Service.RaceResults') as symbol & { type: RaceResultsService };
|
||||
export const RACE_STEWARDING_SERVICE_TOKEN = Symbol.for('Service.RaceStewarding') as symbol & { type: RaceStewardingService };
|
||||
|
||||
export const SPONSOR_SERVICE_TOKEN = Symbol.for('Service.Sponsor');
|
||||
export const SPONSOR_SERVICE_TOKEN = Symbol.for('Service.Sponsor') as symbol & { type: SponsorService };
|
||||
|
||||
export const PAYMENT_SERVICE_TOKEN = Symbol.for('Service.Payment');
|
||||
export const WALLET_SERVICE_TOKEN = Symbol.for('Service.Wallet');
|
||||
@@ -58,19 +71,20 @@ export const MEDIA_SERVICE_TOKEN = Symbol.for('Service.Media');
|
||||
export const AUTH_SERVICE_TOKEN = Symbol.for('Service.Auth') as symbol & { type: AuthService };
|
||||
export const SESSION_SERVICE_TOKEN = Symbol.for('Service.Session') as symbol & { type: SessionService };
|
||||
|
||||
export const PENALTY_SERVICE_TOKEN = Symbol.for('Service.Penalty');
|
||||
export const PENALTY_SERVICE_TOKEN = Symbol.for('Service.Penalty') as symbol & { type: PenaltyService };
|
||||
|
||||
export const POLICY_SERVICE_TOKEN = Symbol.for('Service.Policy');
|
||||
export const POLICY_SERVICE_TOKEN = Symbol.for('Service.Policy') as symbol & { type: PolicyService };
|
||||
|
||||
export const ADMIN_SERVICE_TOKEN = Symbol.for('Service.Admin') as symbol & { type: AdminService };
|
||||
|
||||
// Additional league services
|
||||
export const LEAGUE_SETTINGS_SERVICE_TOKEN = Symbol.for('Service.LeagueSettings');
|
||||
export const LEAGUE_STEWARDING_SERVICE_TOKEN = Symbol.for('Service.LeagueStewarding');
|
||||
export const PROTEST_SERVICE_TOKEN = Symbol.for('Service.Protest');
|
||||
export const RACE_RESULTS_SERVICE_TOKEN = Symbol.for('Service.RaceResults');
|
||||
export const RACE_STEWARDING_SERVICE_TOKEN = Symbol.for('Service.RaceStewarding');
|
||||
export const LANDING_SERVICE_TOKEN = Symbol.for('Service.Landing');
|
||||
export const LEAGUE_SETTINGS_SERVICE_TOKEN = Symbol.for('Service.LeagueSettings') as symbol & { type: LeagueSettingsService };
|
||||
export const LEAGUE_STEWARDING_SERVICE_TOKEN = Symbol.for('Service.LeagueStewarding') as symbol & { type: LeagueStewardingService };
|
||||
export const PROTEST_SERVICE_TOKEN = Symbol.for('Service.Protest') as symbol & { type: ProtestService };
|
||||
|
||||
export const LANDING_SERVICE_TOKEN = Symbol.for('Service.Landing') as symbol & { type: LandingService };
|
||||
|
||||
export const DASHBOARD_SERVICE_TOKEN = Symbol.for('Service.Dashboard') as symbol & { type: DashboardService };
|
||||
|
||||
// Onboarding Services
|
||||
export const ONBOARDING_SERVICE_TOKEN = Symbol.for('Service.Onboarding') as symbol & { type: OnboardingService };
|
||||
@@ -1,17 +1,23 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { SPONSORSHIP_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useSponsorshipRequests(entityType: string, entityId: string) {
|
||||
const sponsorshipService = useInject(SPONSORSHIP_SERVICE_TOKEN);
|
||||
const sponsorshipService = useInject(SPONSOR_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['sponsorshipRequests', entityType, entityId],
|
||||
queryFn: () => sponsorshipService.getPendingSponsorshipRequests({
|
||||
entityType,
|
||||
entityId,
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const result = await sponsorshipService.getPendingSponsorshipRequests({
|
||||
entityType,
|
||||
entityId,
|
||||
});
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!entityType && !!entityId,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,13 @@ export function useAvailableLeagues() {
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['availableLeagues'],
|
||||
queryFn: () => sponsorService.getAvailableLeagues(),
|
||||
queryFn: async () => {
|
||||
const result = await sponsorService.getAvailableLeagues();
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
|
||||
@@ -8,7 +8,13 @@ export function useSponsorBilling(sponsorId: string) {
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['sponsorBilling', sponsorId],
|
||||
queryFn: () => sponsorService.getBilling(sponsorId),
|
||||
queryFn: async () => {
|
||||
const result = await sponsorService.getBilling(sponsorId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!sponsorId,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,13 @@ export function useSponsorDashboard(sponsorId: string) {
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['sponsorDashboard', sponsorId],
|
||||
queryFn: () => sponsorService.getSponsorDashboard(sponsorId),
|
||||
queryFn: async () => {
|
||||
const result = await sponsorService.getSponsorDashboard(sponsorId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!sponsorId,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,13 @@ export function useSponsorLeagueDetail(leagueId: string) {
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['sponsorLeagueDetail', leagueId],
|
||||
queryFn: () => sponsorService.getLeagueDetail(leagueId),
|
||||
queryFn: async () => {
|
||||
const result = await sponsorService.getLeagueDetail(leagueId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!leagueId,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,13 @@ export function useSponsorSponsorships(sponsorId: string) {
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['sponsorSponsorships', sponsorId],
|
||||
queryFn: () => sponsorService.getSponsorSponsorships(sponsorId),
|
||||
queryFn: async () => {
|
||||
const result = await sponsorService.getSponsorSponsorships(sponsorId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!sponsorId,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,13 @@ export function useAllTeams() {
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['allTeams'],
|
||||
queryFn: () => teamService.getAllTeams(),
|
||||
queryFn: async () => {
|
||||
const result = await teamService.getAllTeams();
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
|
||||
@@ -9,7 +9,13 @@ export function useCreateTeam(options?: UseMutationOptions<CreateTeamOutputDTO,
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<CreateTeamOutputDTO, ApiError, CreateTeamInputDTO>({
|
||||
mutationFn: (input) => teamService.createTeam(input),
|
||||
mutationFn: async (input) => {
|
||||
const result = await teamService.createTeam(input);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -8,7 +8,13 @@ export function useTeamDetails(teamId: string, currentUserId: string) {
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['teamDetails', teamId, currentUserId],
|
||||
queryFn: () => teamService.getTeamDetails(teamId, currentUserId),
|
||||
queryFn: async () => {
|
||||
const result = await teamService.getTeamDetails(teamId, currentUserId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!teamId && !!currentUserId,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,13 @@ export function useTeamMembers(teamId: string, currentUserId: string, teamOwnerI
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['teamMembers', teamId, currentUserId, teamOwnerId],
|
||||
queryFn: () => teamService.getTeamMembers(teamId, currentUserId, teamOwnerId),
|
||||
queryFn: async () => {
|
||||
const result = await teamService.getTeamMembers(teamId, currentUserId, teamOwnerId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!teamId && !!currentUserId && !!teamOwnerId,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,13 @@ export function useTeamMembership(teamId: string, driverId: string) {
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['teamMembership', teamId, driverId],
|
||||
queryFn: () => teamService.getMembership(teamId, driverId),
|
||||
queryFn: async () => {
|
||||
const result = await teamService.getMembership(teamId, driverId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!teamId && !!driverId,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,13 @@ export function useUpdateTeam(options?: UseMutationOptions<UpdateTeamOutputDTO,
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<UpdateTeamOutputDTO, ApiError, { teamId: string; input: UpdateTeamInputDTO }>({
|
||||
mutationFn: ({ teamId, input }) => teamService.updateTeam(teamId, input),
|
||||
mutationFn: async ({ teamId, input }) => {
|
||||
const result = await teamService.updateTeam(teamId, input);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
30
apps/website/lib/hooks/useDriverSearch.ts
Normal file
30
apps/website/lib/hooks/useDriverSearch.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
|
||||
|
||||
/**
|
||||
* useDriverSearch
|
||||
*
|
||||
* Client-side hook for UX-only search filtering.
|
||||
* This is view-only transformation, not business logic.
|
||||
*/
|
||||
export function useDriverSearch(drivers: DriverLeaderboardItemViewModel[]) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const filteredDrivers = useMemo(() => {
|
||||
if (!searchQuery) return drivers;
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return drivers.filter(driver => {
|
||||
return (
|
||||
driver.name.toLowerCase().includes(query) ||
|
||||
driver.nationality.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
}, [drivers, searchQuery]);
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
filteredDrivers,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AdminService } from '@/lib/services/admin/AdminService';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { MutationError, mapToMutationError } from '@/lib/contracts/mutations/MutationError';
|
||||
import { Mutation } from '@/lib/contracts/mutations/Mutation';
|
||||
|
||||
/**
|
||||
* DeleteUserMutation
|
||||
@@ -13,7 +14,7 @@ import { MutationError, mapToMutationError } from '@/lib/contracts/mutations/Mut
|
||||
*
|
||||
* Pattern: Server Action → Mutation → Service → API Client
|
||||
*/
|
||||
export class DeleteUserMutation {
|
||||
export class DeleteUserMutation implements Mutation<{ userId: string }, void, MutationError> {
|
||||
async execute(input: { userId: string }): Promise<Result<void, MutationError>> {
|
||||
try {
|
||||
// Manual construction: Service creates its own dependencies
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AdminService } from '@/lib/services/admin/AdminService';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { MutationError, mapToMutationError } from '@/lib/contracts/mutations/MutationError';
|
||||
import { Mutation } from '@/lib/contracts/mutations/Mutation';
|
||||
|
||||
/**
|
||||
* UpdateUserStatusMutation
|
||||
@@ -13,7 +14,7 @@ import { MutationError, mapToMutationError } from '@/lib/contracts/mutations/Mut
|
||||
*
|
||||
* Pattern: Server Action → Mutation → Service → API Client
|
||||
*/
|
||||
export class UpdateUserStatusMutation {
|
||||
export class UpdateUserStatusMutation implements Mutation<{ userId: string; status: string }, void, MutationError> {
|
||||
async execute(input: { userId: string; status: string }): Promise<Result<void, MutationError>> {
|
||||
try {
|
||||
// Manual construction: Service creates its own dependencies
|
||||
|
||||
24
apps/website/lib/mutations/auth/LogoutMutation.ts
Normal file
24
apps/website/lib/mutations/auth/LogoutMutation.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Logout Mutation
|
||||
*
|
||||
* Framework-agnostic mutation for logout operations.
|
||||
* Called from Server Actions.
|
||||
*
|
||||
* Pattern: Server Action → Mutation → Service → API Client
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { AuthService } from '@/lib/services/auth/AuthService';
|
||||
|
||||
export class LogoutMutation {
|
||||
async execute(): Promise<Result<void, string>> {
|
||||
try {
|
||||
const authService = new AuthService();
|
||||
await authService.logout();
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Logout failed';
|
||||
return Result.err(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInp
|
||||
/**
|
||||
* CreateLeagueMutation
|
||||
*
|
||||
* Framework-agnostic mutation for league creation.
|
||||
* Framework-agnostic mutation for creating leagues.
|
||||
* Can be called from Server Actions or other contexts.
|
||||
*/
|
||||
export class CreateLeagueMutation {
|
||||
@@ -24,12 +24,12 @@ export class CreateLeagueMutation {
|
||||
this.service = new LeagueService(apiClient);
|
||||
}
|
||||
|
||||
async createLeague(input: CreateLeagueInputDTO): Promise<Result<void, string>> {
|
||||
async execute(input: CreateLeagueInputDTO): Promise<Result<string, string>> {
|
||||
try {
|
||||
await this.service.createLeague(input);
|
||||
return Result.ok(undefined);
|
||||
const result = await this.service.createLeague(input);
|
||||
return Result.ok(result.leagueId);
|
||||
} catch (error) {
|
||||
console.error('createLeague failed:', error);
|
||||
console.error('CreateLeagueMutation failed:', error);
|
||||
return Result.err('Failed to create league');
|
||||
}
|
||||
}
|
||||
|
||||
80
apps/website/lib/mutations/leagues/StewardingMutation.ts
Normal file
80
apps/website/lib/mutations/leagues/StewardingMutation.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueService } from '@/lib/services/leagues/LeagueService';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
|
||||
/**
|
||||
* StewardingMutation
|
||||
*
|
||||
* Framework-agnostic mutation for stewarding operations.
|
||||
* Can be called from Server Actions or other contexts.
|
||||
*/
|
||||
export class StewardingMutation {
|
||||
private service: LeagueService;
|
||||
|
||||
constructor() {
|
||||
// Manual wiring for serverless
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
this.service = new LeagueService(apiClient);
|
||||
}
|
||||
|
||||
async applyPenalty(input: {
|
||||
protestId: string;
|
||||
penaltyType: string;
|
||||
penaltyValue: number;
|
||||
stewardNotes: string;
|
||||
raceId: string;
|
||||
accusedDriverId: string;
|
||||
reason: string;
|
||||
}): Promise<Result<void, string>> {
|
||||
try {
|
||||
// TODO: Implement when penalty API is available
|
||||
// For now, return success
|
||||
console.log('applyPenalty called with:', input);
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
console.error('applyPenalty failed:', error);
|
||||
return Result.err('Failed to apply penalty');
|
||||
}
|
||||
}
|
||||
|
||||
async requestDefense(input: {
|
||||
protestId: string;
|
||||
stewardId: string;
|
||||
}): Promise<Result<void, string>> {
|
||||
try {
|
||||
// TODO: Implement when defense API is available
|
||||
// For now, return success
|
||||
console.log('requestDefense called with:', input);
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
console.error('requestDefense failed:', error);
|
||||
return Result.err('Failed to request defense');
|
||||
}
|
||||
}
|
||||
|
||||
async quickPenalty(input: {
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
raceId: string;
|
||||
penaltyType: string;
|
||||
penaltyValue: number;
|
||||
reason: string;
|
||||
adminId: string;
|
||||
}): Promise<Result<void, string>> {
|
||||
try {
|
||||
// TODO: Implement when quick penalty API is available
|
||||
// For now, return success
|
||||
console.log('quickPenalty called with:', input);
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
console.error('quickPenalty failed:', error);
|
||||
return Result.err('Failed to apply quick penalty');
|
||||
}
|
||||
}
|
||||
}
|
||||
49
apps/website/lib/mutations/leagues/WalletMutation.ts
Normal file
49
apps/website/lib/mutations/leagues/WalletMutation.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueService } from '@/lib/services/leagues/LeagueService';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
|
||||
/**
|
||||
* WalletMutation
|
||||
*
|
||||
* Framework-agnostic mutation for wallet operations.
|
||||
* Can be called from Server Actions or other contexts.
|
||||
*/
|
||||
export class WalletMutation {
|
||||
private service: LeagueService;
|
||||
|
||||
constructor() {
|
||||
// Manual wiring for serverless
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
this.service = new LeagueService(apiClient);
|
||||
}
|
||||
|
||||
async withdraw(leagueId: string, amount: number): Promise<Result<void, string>> {
|
||||
try {
|
||||
// TODO: Implement when wallet withdrawal API is available
|
||||
// For now, return success
|
||||
console.log('withdraw called with:', { leagueId, amount });
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
console.error('withdraw failed:', error);
|
||||
return Result.err('Failed to withdraw funds');
|
||||
}
|
||||
}
|
||||
|
||||
async exportTransactions(leagueId: string): Promise<Result<void, string>> {
|
||||
try {
|
||||
// TODO: Implement when export API is available
|
||||
// For now, return success
|
||||
console.log('exportTransactions called with:', { leagueId });
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
console.error('exportTransactions failed:', error);
|
||||
return Result.err('Failed to export transactions');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,27 +8,23 @@
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Mutation } from '@/lib/contracts/mutations/Mutation';
|
||||
import { mapToMutationError } from '@/lib/contracts/mutations/MutationError';
|
||||
import { OnboardingService } from '@/lib/services/onboarding/OnboardingService';
|
||||
import { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
|
||||
import { CompleteOnboardingViewDataBuilder } from '@/lib/builders/view-data/CompleteOnboardingViewDataBuilder';
|
||||
import { CompleteOnboardingViewData } from '@/lib/builders/view-data/CompleteOnboardingViewData';
|
||||
|
||||
export class CompleteOnboardingMutation {
|
||||
export class CompleteOnboardingMutation implements Mutation<CompleteOnboardingInputDTO, CompleteOnboardingViewData, string> {
|
||||
async execute(params: CompleteOnboardingInputDTO): Promise<Result<CompleteOnboardingViewData, string>> {
|
||||
try {
|
||||
const onboardingService = new OnboardingService();
|
||||
const result = await onboardingService.completeOnboarding(params);
|
||||
|
||||
if (result.isErr()) {
|
||||
const error = result.getError();
|
||||
return Result.err(error.message || 'Failed to complete onboarding');
|
||||
}
|
||||
|
||||
const output = CompleteOnboardingViewDataBuilder.build(result.unwrap());
|
||||
return Result.ok(output);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to complete onboarding';
|
||||
return Result.err(errorMessage);
|
||||
const onboardingService = new OnboardingService();
|
||||
const result = await onboardingService.completeOnboarding(params);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err(mapToMutationError(result.getError()));
|
||||
}
|
||||
|
||||
const output = CompleteOnboardingViewDataBuilder.build(result.unwrap());
|
||||
return Result.ok(output);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Mutation } from '@/lib/contracts/mutations/Mutation';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { mapToMutationError } from '@/lib/contracts/mutations/MutationError';
|
||||
import { OnboardingService } from '@/lib/services/onboarding/OnboardingService';
|
||||
import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO';
|
||||
import { GenerateAvatarsViewDataBuilder } from '@/lib/builders/view-data/GenerateAvatarsViewDataBuilder';
|
||||
@@ -11,7 +12,7 @@ export class GenerateAvatarsMutation implements Mutation<RequestAvatarGeneration
|
||||
const result = await service.generateAvatars(input);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err(result.getError().message);
|
||||
return Result.err(mapToMutationError(result.getError()));
|
||||
}
|
||||
|
||||
const output = GenerateAvatarsViewDataBuilder.build(result.unwrap());
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { SponsorshipRequestsService } from '@/lib/services/sponsors/SponsorshipRequestsService';
|
||||
import type { AcceptSponsorshipRequestCommand } from '@/lib/services/sponsors/SponsorshipRequestsService';
|
||||
import type { Mutation } from '@/lib/contracts/mutations/Mutation';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { Result as ResultType } from '@/lib/contracts/Result';
|
||||
|
||||
export interface AcceptSponsorshipRequestCommand {
|
||||
requestId: string;
|
||||
actorDriverId: string;
|
||||
}
|
||||
|
||||
export type AcceptSponsorshipRequestMutationError =
|
||||
| 'ACCEPT_SPONSORSHIP_REQUEST_FAILED';
|
||||
@@ -18,7 +21,7 @@ export class AcceptSponsorshipRequestMutation
|
||||
|
||||
async execute(
|
||||
command: AcceptSponsorshipRequestCommand,
|
||||
): Promise<ResultType<void, AcceptSponsorshipRequestMutationError>> {
|
||||
): Promise<Result<void, AcceptSponsorshipRequestMutationError>> {
|
||||
const result = await this.service.acceptRequest(command);
|
||||
|
||||
if (result.isErr()) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { SponsorshipRequestsService } from '@/lib/services/sponsors/SponsorshipRequestsService';
|
||||
import type { RejectSponsorshipRequestCommand } from '@/lib/services/sponsors/SponsorshipRequestsService';
|
||||
import type { Mutation } from '@/lib/contracts/mutations/Mutation';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { Result as ResultType } from '@/lib/contracts/Result';
|
||||
|
||||
export interface RejectSponsorshipRequestCommand {
|
||||
requestId: string;
|
||||
actorDriverId: string;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
export type RejectSponsorshipRequestMutationError =
|
||||
| 'REJECT_SPONSORSHIP_REQUEST_FAILED';
|
||||
@@ -18,7 +22,7 @@ export class RejectSponsorshipRequestMutation
|
||||
|
||||
async execute(
|
||||
command: RejectSponsorshipRequestCommand,
|
||||
): Promise<ResultType<void, RejectSponsorshipRequestMutationError>> {
|
||||
): Promise<Result<void, RejectSponsorshipRequestMutationError>> {
|
||||
const result = await this.service.rejectRequest(command);
|
||||
|
||||
if (result.isErr()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
43
apps/website/lib/page-queries/media/GetAvatarPageQuery.ts
Normal file
43
apps/website/lib/page-queries/media/GetAvatarPageQuery.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
36
apps/website/lib/page-queries/media/GetTeamLogoPageQuery.ts
Normal file
36
apps/website/lib/page-queries/media/GetTeamLogoPageQuery.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user