website refactor

This commit is contained in:
2026-01-17 15:46:55 +01:00
parent 4d5ce9bfd6
commit 72a626ce71
346 changed files with 19308 additions and 8605 deletions

View File

@@ -5,257 +5,181 @@
* NO Intl.*, NO Date.toLocale*, NO dynamic formatting.
*/
// ============================================================================
// COUNTRY FLAG DISPLAY
// ============================================================================
export interface CountryFlagDisplayData {
flag: string;
label: string;
}
export const countryFlagDisplay: Record<string, CountryFlagDisplayData> = {
// Common country codes - add as needed
US: { flag: '🇺🇸', label: 'United States' },
GB: { flag: '🇬🇧', label: 'United Kingdom' },
DE: { flag: '🇩🇪', label: 'Germany' },
FR: { flag: '🇫🇷', label: 'France' },
IT: { flag: '🇮🇹', label: 'Italy' },
ES: { flag: '🇪🇸', label: 'Spain' },
JP: { flag: '🇯🇵', label: 'Japan' },
AU: { flag: '🇦🇺', label: 'Australia' },
CA: { flag: '🇨🇦', label: 'Canada' },
BR: { flag: '🇧🇷', label: 'Brazil' },
// Fallback for unknown codes
DEFAULT: { flag: '🏁', label: 'Unknown' },
} as const;
export function getCountryFlagDisplay(countryCode: string): CountryFlagDisplayData {
const code = countryCode.toUpperCase();
return countryFlagDisplay[code] || countryFlagDisplay.DEFAULT;
}
// ============================================================================
// ACHIEVEMENT RARITY DISPLAY
// ============================================================================
export interface AchievementRarityDisplayData {
text: string;
badgeClasses: string;
borderClasses: string;
}
export const achievementRarityDisplay: Record<string, AchievementRarityDisplayData> = {
common: {
text: 'Common',
badgeClasses: 'bg-gray-400/10 text-gray-400',
borderClasses: 'border-gray-400/30',
},
rare: {
text: 'Rare',
badgeClasses: 'bg-primary-blue/10 text-primary-blue',
borderClasses: 'border-primary-blue/30',
},
epic: {
text: 'Epic',
badgeClasses: 'bg-purple-400/10 text-purple-400',
borderClasses: 'border-purple-400/30',
},
legendary: {
text: 'Legendary',
badgeClasses: 'bg-yellow-400/10 text-yellow-400',
borderClasses: 'border-yellow-400/30',
},
} as const;
export function getAchievementRarityDisplay(rarity: string): AchievementRarityDisplayData {
return achievementRarityDisplay[rarity] || achievementRarityDisplay.common;
}
// ============================================================================
// ACHIEVEMENT ICON DISPLAY
// ============================================================================
export type AchievementIconType = 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
export interface AchievementIconDisplayData {
name: string;
// Icon component will be resolved in UI layer
}
export const achievementIconDisplay: Record<AchievementIconType, AchievementIconDisplayData> = {
trophy: { name: 'Trophy' },
medal: { name: 'Medal' },
star: { name: 'Star' },
crown: { name: 'Crown' },
target: { name: 'Target' },
zap: { name: 'Zap' },
} as const;
export function getAchievementIconDisplay(icon: string): AchievementIconDisplayData {
return achievementIconDisplay[icon as AchievementIconType] || achievementIconDisplay.trophy;
}
// ============================================================================
// SOCIAL PLATFORM DISPLAY
// ============================================================================
export interface SocialPlatformDisplayData {
name: string;
hoverClasses: string;
}
export const socialPlatformDisplay: Record<string, SocialPlatformDisplayData> = {
twitter: {
name: 'Twitter',
hoverClasses: 'hover:text-sky-400 hover:bg-sky-400/10',
},
youtube: {
name: 'YouTube',
hoverClasses: 'hover:text-red-500 hover:bg-red-500/10',
},
twitch: {
name: 'Twitch',
hoverClasses: 'hover:text-purple-400 hover:bg-purple-400/10',
},
discord: {
name: 'Discord',
hoverClasses: 'hover:text-indigo-400 hover:bg-indigo-400/10',
},
} as const;
export function getSocialPlatformDisplay(platform: string): SocialPlatformDisplayData {
return socialPlatformDisplay[platform] || socialPlatformDisplay.discord;
}
// ============================================================================
// DATE FORMATTING (DETERMINISTIC)
// ============================================================================
/**
* Format date string to "Month Year" format
* Input: ISO date string (e.g., "2024-01-15T10:30:00Z")
* Output: "Jan 2024"
*/
export function formatMonthYear(dateString: string): string {
const date = new Date(dateString);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const month = months[date.getUTCMonth()];
const year = date.getUTCFullYear();
return `${month} ${year}`;
}
/**
* Format date string to "Month Day, Year" format
* Input: ISO date string
* Output: "Jan 15, 2024"
*/
export function formatMonthDayYear(dateString: string): string {
const date = new Date(dateString);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const month = months[date.getUTCMonth()];
const day = date.getUTCDate();
const year = date.getUTCFullYear();
return `${month} ${day}, ${year}`;
}
// ============================================================================
// STATISTICS FORMATTING
// ============================================================================
export interface StatDisplayData {
value: string;
label: string;
}
/**
* Format percentage with 1 decimal place
* Input: 0.1234
* Output: "12.3%"
*/
export function formatPercentage(value: number | null): string {
if (value === null || value === undefined) return '0.0%';
return `${(value * 100).toFixed(1)}%`;
}
/**
* Format finish position
* Input: 1
* Output: "P1"
*/
export function formatFinishPosition(position: number | null): string {
if (position === null || position === undefined) return 'P-';
return `P${position}`;
}
/**
* Format average finish with 1 decimal place
* Input: 3.456
* Output: "P3.5"
*/
export function formatAvgFinish(avg: number | null): string {
if (avg === null || avg === undefined) return 'P-';
return `P${avg.toFixed(1)}`;
}
/**
* Format rating (whole number)
* Input: 1234.56
* Output: "1235"
*/
export function formatRating(rating: number | null): string {
if (rating === null || rating === undefined) return '0';
return Math.round(rating).toString();
}
/**
* Format consistency percentage
* Input: 87.5
* Output: "88%"
*/
export function formatConsistency(consistency: number | null): string {
if (consistency === null || consistency === undefined) return '0%';
return `${Math.round(consistency)}%`;
}
/**
* Format percentile
* Input: 15.5
* Output: "Top 16%"
*/
export function formatPercentile(percentile: number | null): string {
if (percentile === null || percentile === undefined) return 'Top -%';
return `Top ${Math.round(percentile)}%`;
}
// ============================================================================
// TEAM ROLE DISPLAY
// ============================================================================
export interface TeamRoleDisplayData {
text: string;
badgeClasses: string;
}
export const teamRoleDisplay: Record<string, TeamRoleDisplayData> = {
owner: {
text: 'Owner',
badgeClasses: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30',
},
admin: {
text: 'Admin',
badgeClasses: 'bg-purple-500/10 text-purple-400 border-purple-500/30',
},
steward: {
text: 'Steward',
badgeClasses: 'bg-blue-500/10 text-blue-400 border-blue-500/30',
},
member: {
text: 'Member',
badgeClasses: 'bg-primary-blue/10 text-primary-blue border-primary-blue/30',
},
} as const;
export class ProfileDisplay {
private static readonly countryFlagDisplay: Record<string, CountryFlagDisplayData> = {
US: { flag: '🇺🇸', label: 'United States' },
GB: { flag: '🇬🇧', label: 'United Kingdom' },
DE: { flag: '🇩🇪', label: 'Germany' },
FR: { flag: '🇫🇷', label: 'France' },
IT: { flag: '🇮🇹', label: 'Italy' },
ES: { flag: '🇪🇸', label: 'Spain' },
JP: { flag: '🇯🇵', label: 'Japan' },
AU: { flag: '🇦🇺', label: 'Australia' },
CA: { flag: '🇨🇦', label: 'Canada' },
BR: { flag: '🇧🇷', label: 'Brazil' },
DEFAULT: { flag: '🏁', label: 'Unknown' },
};
export function getTeamRoleDisplay(role: string): TeamRoleDisplayData {
return teamRoleDisplay[role] || teamRoleDisplay.member;
}
static getCountryFlag(countryCode: string): CountryFlagDisplayData {
const code = countryCode.toUpperCase();
return ProfileDisplay.countryFlagDisplay[code] || ProfileDisplay.countryFlagDisplay.DEFAULT;
}
private static readonly achievementRarityDisplay: Record<string, AchievementRarityDisplayData> = {
common: {
text: 'Common',
badgeClasses: 'bg-gray-400/10 text-gray-400',
borderClasses: 'border-gray-400/30',
},
rare: {
text: 'Rare',
badgeClasses: 'bg-primary-blue/10 text-primary-blue',
borderClasses: 'border-primary-blue/30',
},
epic: {
text: 'Epic',
badgeClasses: 'bg-purple-400/10 text-purple-400',
borderClasses: 'border-purple-400/30',
},
legendary: {
text: 'Legendary',
badgeClasses: 'bg-yellow-400/10 text-yellow-400',
borderClasses: 'border-yellow-400/30',
},
};
static getAchievementRarity(rarity: string): AchievementRarityDisplayData {
return ProfileDisplay.achievementRarityDisplay[rarity] || ProfileDisplay.achievementRarityDisplay.common;
}
private static readonly achievementIconDisplay: Record<string, AchievementIconDisplayData> = {
trophy: { name: 'Trophy' },
medal: { name: 'Medal' },
star: { name: 'Star' },
crown: { name: 'Crown' },
target: { name: 'Target' },
zap: { name: 'Zap' },
};
static getAchievementIcon(icon: string): AchievementIconDisplayData {
return ProfileDisplay.achievementIconDisplay[icon] || ProfileDisplay.achievementIconDisplay.trophy;
}
private static readonly socialPlatformDisplay: Record<string, SocialPlatformDisplayData> = {
twitter: {
name: 'Twitter',
hoverClasses: 'hover:text-sky-400 hover:bg-sky-400/10',
},
youtube: {
name: 'YouTube',
hoverClasses: 'hover:text-red-500 hover:bg-red-500/10',
},
twitch: {
name: 'Twitch',
hoverClasses: 'hover:text-purple-400 hover:bg-purple-400/10',
},
discord: {
name: 'Discord',
hoverClasses: 'hover:text-indigo-400 hover:bg-indigo-400/10',
},
};
static getSocialPlatform(platform: string): SocialPlatformDisplayData {
return ProfileDisplay.socialPlatformDisplay[platform] || ProfileDisplay.socialPlatformDisplay.discord;
}
static formatMonthYear(dateString: string): string {
const date = new Date(dateString);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const month = months[date.getUTCMonth()];
const year = date.getUTCFullYear();
return `${month} ${year}`;
}
static formatMonthDayYear(dateString: string): string {
const date = new Date(dateString);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const month = months[date.getUTCMonth()];
const day = date.getUTCDate();
const year = date.getUTCFullYear();
return `${month} ${day}, ${year}`;
}
static formatPercentage(value: number | null | undefined): string {
if (value === null || value === undefined) return '0.0%';
return `${(value * 100).toFixed(1)}%`;
}
static formatFinishPosition(position: number | null | undefined): string {
if (position === null || position === undefined) return 'P-';
return `P${position}`;
}
static formatAvgFinish(avg: number | null | undefined): string {
if (avg === null || avg === undefined) return 'P-';
return `P${avg.toFixed(1)}`;
}
static formatRating(rating: number | null | undefined): string {
if (rating === null || rating === undefined) return '0';
return Math.round(rating).toString();
}
static formatConsistency(consistency: number | null | undefined): string {
if (consistency === null || consistency === undefined) return '0%';
return `${Math.round(consistency)}%`;
}
static formatPercentile(percentile: number | null | undefined): string {
if (percentile === null || percentile === undefined) return 'Top -%';
return `Top ${Math.round(percentile)}%`;
}
private static readonly teamRoleDisplay: Record<string, TeamRoleDisplayData> = {
owner: {
text: 'Owner',
badgeClasses: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30',
},
admin: {
text: 'Admin',
badgeClasses: 'bg-purple-500/10 text-purple-400 border-purple-500/30',
},
steward: {
text: 'Steward',
badgeClasses: 'bg-blue-500/10 text-blue-400 border-blue-500/30',
},
member: {
text: 'Member',
badgeClasses: 'bg-primary-blue/10 text-primary-blue border-primary-blue/30',
},
};
static getTeamRole(role: string): TeamRoleDisplayData {
return ProfileDisplay.teamRoleDisplay[role] || ProfileDisplay.teamRoleDisplay.member;
}
}

View File

@@ -70,7 +70,7 @@ describe('SessionGateway', () => {
// Assert
expect(result).toEqual(mockSession);
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3101/auth/session', {
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/auth/session'), {
headers: { cookie: 'gp_session=valid-token; other=value' },
cache: 'no-store',
credentials: 'include',

View File

@@ -0,0 +1,36 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { TeamService } from '@/lib/services/teams/TeamService';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
export interface TeamLeaderboardPageData {
teams: TeamSummaryViewModel[];
}
export class TeamLeaderboardPageQuery implements PageQuery<TeamLeaderboardPageData, void> {
async execute(): Promise<Result<TeamLeaderboardPageData, PresentationError>> {
try {
const service = new TeamService();
const result = await service.getAllTeams();
if (result.isErr()) {
return Result.err(mapToPresentationError(result.getError()));
}
const teams = result.unwrap().map(t => ({
id: t.id,
name: t.name,
logoUrl: t.logoUrl,
memberCount: t.memberCount,
totalWins: t.totalWins,
totalRaces: t.totalRaces,
rating: 1450, // Mocked as in original
} as TeamSummaryViewModel));
return Result.ok({ teams });
} catch (error) {
return Result.err('unknown');
}
}
}

View File

@@ -0,0 +1,36 @@
import { Result } from '@/lib/contracts/Result';
export interface ActionItem {
id: string;
type: 'USER_UPDATE' | 'ONBOARDING' | 'AVATAR_GEN' | 'PROFILE_UPDATE' | 'LEAGUE_SCHEDULE' | 'SPONSORSHIP';
status: 'PENDING' | 'COMPLETED' | 'FAILED' | 'IN_PROGRESS';
timestamp: string;
initiator: string;
details: string;
}
export class ActionsPageQuery {
async execute(): Promise<Result<{ actions: ActionItem[] }, string>> {
// Mock data for now
return Result.ok({
actions: [
{
id: '1',
type: 'USER_UPDATE',
status: 'COMPLETED',
timestamp: new Date().toISOString(),
initiator: 'Admin',
details: 'Updated status for user 123'
},
{
id: '2',
type: 'AVATAR_GEN',
status: 'IN_PROGRESS',
timestamp: new Date().toISOString(),
initiator: 'User 456',
details: 'Generating AI avatars'
}
]
});
}
}

View File

@@ -10,6 +10,7 @@ import type { SignupParamsDTO } from '@/lib/types/generated/SignupParamsDTO';
import type { ForgotPasswordDTO } from '@/lib/types/generated/ForgotPasswordDTO';
import type { ResetPasswordDTO } from '@/lib/types/generated/ResetPasswordDTO';
import { isProductionEnvironment } from '@/lib/config/env';
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
/**
* Auth Service
@@ -20,41 +21,45 @@ import { isProductionEnvironment } from '@/lib/config/env';
export class AuthService implements Service {
private apiClient: AuthApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new AuthApiClient(baseUrl, errorReporter, logger);
constructor(apiClient?: AuthApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new AuthApiClient(baseUrl, errorReporter, logger);
}
}
async login(params: LoginParamsDTO): Promise<Result<AuthSessionDTO, DomainError>> {
async login(params: LoginParamsDTO): Promise<any> {
try {
const dto = await this.apiClient.login(params);
return Result.ok(dto);
return new SessionViewModel(dto.user);
} catch (error: unknown) {
return Result.err({ type: 'unauthorized', message: (error as Error).message || 'Login failed' });
throw error;
}
}
async signup(params: SignupParamsDTO): Promise<Result<AuthSessionDTO, DomainError>> {
async signup(params: SignupParamsDTO): Promise<any> {
try {
const dto = await this.apiClient.signup(params);
return Result.ok(dto);
return new SessionViewModel(dto.user);
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Signup failed' });
throw error;
}
}
async logout(): Promise<Result<void, DomainError>> {
async logout(): Promise<any> {
try {
await this.apiClient.logout();
return Result.ok(undefined);
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Logout failed' });
throw error;
}
}
@@ -76,12 +81,12 @@ export class AuthService implements Service {
}
}
async getSession(): Promise<Result<AuthSessionDTO | null, DomainError>> {
async getSession(): Promise<any> {
try {
const dto = await this.apiClient.getSession();
return Result.ok(dto);
return dto;
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch session' });
throw error;
}
}
}

View File

@@ -2,6 +2,7 @@ import { Result } from '@/lib/contracts/Result';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { AuthService } from './AuthService';
import type { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
/**
* Session Service
@@ -12,14 +13,22 @@ import type { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
export class SessionService implements Service {
private authService: AuthService;
constructor() {
this.authService = new AuthService();
constructor(apiClient?: any) {
this.authService = new AuthService(apiClient);
}
/**
* Get current user session
*/
async getSession(): Promise<Result<AuthSessionDTO | null, DomainError>> {
return this.authService.getSession();
async getSession(): Promise<any> {
try {
const res = await this.authService.getSession();
if (!res) return null;
const data = (res as any).value || res;
if (!data || !data.user) return null;
return new SessionViewModel(data.user);
} catch (error: unknown) {
throw error;
}
}
}

View File

@@ -9,6 +9,9 @@ import { DomainError, Service } from '@/lib/contracts/services/Service';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { CompleteOnboardingViewModel } from '@/lib/view-models/CompleteOnboardingViewModel';
/**
* Driver Service - DTO Only
@@ -19,46 +22,59 @@ import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporte
export class DriverService implements Service {
private readonly apiClient: DriversApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger);
this.apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
constructor(apiClient?: DriversApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger);
this.apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
}
}
async getDriver(id: string): Promise<any> {
return this.apiClient.getDriver(id);
}
/**
* Get driver leaderboard (returns DTO)
*/
async getDriverLeaderboard(): Promise<Result<unknown, DomainError>> {
async getDriverLeaderboard(): Promise<any> {
try {
const data = await this.apiClient.getLeaderboard();
return Result.ok(data);
const res = await this.apiClient.getLeaderboard();
const data = (res as any).value || res;
return new DriverLeaderboardViewModel(data);
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get leaderboard' });
throw error;
}
}
/**
* Complete driver onboarding (returns DTO)
*/
async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise<Result<CompleteOnboardingOutputDTO, DomainError>> {
async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise<any> {
try {
const data = await this.apiClient.completeOnboarding(input);
return Result.ok(data);
const res = await this.apiClient.completeOnboarding(input);
const data = (res as any).value || res;
return new CompleteOnboardingViewModel(data);
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to complete onboarding' });
throw error;
}
}
/**
* Get current driver (returns DTO)
*/
async getCurrentDriver(): Promise<Result<DriverDTO | null, DomainError>> {
async getCurrentDriver(): Promise<any> {
try {
const data = await this.apiClient.getCurrent();
return Result.ok(data);
const res = await this.apiClient.getCurrent();
if (!res) return null;
const data = (res as any).value || res;
if (!data) return null;
return new DriverViewModel(data);
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get current driver' });
throw error;
}
}

View File

@@ -75,7 +75,7 @@ export class HomeService implements Service {
async shouldRedirectToDashboard(): Promise<boolean> {
const sessionService = new SessionService();
const sessionResult = await sessionService.getSession();
return sessionResult.isOk() && !!sessionResult.unwrap();
const session = await sessionService.getSession();
return !!session;
}
}

View File

@@ -8,6 +8,7 @@ import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { isProductionEnvironment } from '@/lib/config/env';
import { HomeDiscoveryViewModel } from '@/lib/view-models/HomeDiscoveryViewModel';
/**
* Landing Service - DTO Only
@@ -21,7 +22,7 @@ export class LandingService implements Service {
private teamsApi: TeamsApiClient;
private authApi: AuthApiClient;
constructor() {
constructor(racesApi?: RacesApiClient, leaguesApi?: LeaguesApiClient, teamsApi?: TeamsApiClient) {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
@@ -30,12 +31,34 @@ export class LandingService implements Service {
reportToExternal: isProductionEnvironment(),
});
this.racesApi = new RacesApiClient(baseUrl, errorReporter, logger);
this.leaguesApi = new LeaguesApiClient(baseUrl, errorReporter, logger);
this.teamsApi = new TeamsApiClient(baseUrl, errorReporter, logger);
this.racesApi = racesApi || new RacesApiClient(baseUrl, errorReporter, logger);
this.leaguesApi = leaguesApi || new LeaguesApiClient(baseUrl, errorReporter, logger);
this.teamsApi = teamsApi || new TeamsApiClient(baseUrl, errorReporter, logger);
this.authApi = new AuthApiClient(baseUrl, errorReporter, logger);
}
async getHomeDiscovery(): Promise<any> {
try {
const [racesRes, leaguesRes, teamsRes] = await Promise.all([
this.racesApi.getPageData(),
this.leaguesApi.getAllWithCapacity(),
this.teamsApi.getAll(),
]);
const racesData = (racesRes as any).value || racesRes;
const leaguesData = (leaguesRes as any).value || leaguesRes;
const teamsData = (teamsRes as any).value || teamsRes;
return new HomeDiscoveryViewModel({
topLeagues: leaguesData.leagues.slice(0, 4),
teams: teamsData.teams.slice(0, 4),
upcomingRaces: racesData.races.slice(0, 4),
} as any);
} catch (error: unknown) {
throw error;
}
}
async getLandingData(): Promise<Result<{ featuredLeagues: unknown[]; stats: Record<string, unknown> }, DomainError>> {
return Result.ok({ featuredLeagues: [], stats: {} });
}

View File

@@ -5,9 +5,9 @@ import { Service, type DomainError } from '@/lib/contracts/services/Service';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { isProductionEnvironment } from '@/lib/config/env';
import { LeagueMemberViewModel } from '@/lib/view-models/LeagueMemberViewModel';
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
import type { LeagueMembershipDTO } from '@/lib/types/generated/LeagueMembershipDTO';
export interface LeagueRosterAdminData {
leagueId: string;
@@ -20,15 +20,40 @@ export class LeagueMembershipService implements Service {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private static cachedMemberships = new Map<string, any[]>();
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
constructor(apiClient?: LeaguesApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
}
}
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<any[]> {
const res = await this.apiClient.getMemberships(leagueId);
const members = (res as any).members || res;
return members.map((m: any) => new LeagueMemberViewModel({ ...m, currentUserId }, currentUserId as any));
}
async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<any> {
const res = await this.apiClient.removeMember(leagueId, performerDriverId, targetDriverId);
return (res as any).value || res;
}
async removeRosterMember(leagueId: string, targetDriverId: string): Promise<Result<{ success: boolean }, DomainError>> {
try {
const res = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
const dto = (res as any).value || res;
return Result.ok({ success: dto.success });
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to remove member' });
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -101,18 +126,10 @@ export class LeagueMembershipService implements Service {
}
}
async removeMember(leagueId: string, targetDriverId: string): Promise<Result<{ success: boolean }, DomainError>> {
try {
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
return Result.ok({ success: dto.success });
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to remove member' });
}
}
async approveJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
try {
const dto = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId);
const res = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId);
const dto = (res as any).value || res;
return Result.ok({ success: dto.success });
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to approve join request' });
@@ -121,7 +138,8 @@ export class LeagueMembershipService implements Service {
async rejectJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
try {
const dto = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId);
const res = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId);
const dto = (res as any).value || res;
return Result.ok({ success: dto.success });
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to reject join request' });

View File

@@ -56,25 +56,82 @@ export class LeagueService implements Service {
private sponsorsApiClient?: SponsorsApiClient;
private racesApiClient?: RacesApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
constructor(apiClient?: LeaguesApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
}
// Optional clients can be initialized if needed
}
async getAllLeagues(): Promise<Result<AllLeaguesWithCapacityAndScoringDTO, DomainError>> {
async getLeagueStandings(leagueId: string): Promise<any> {
try {
const data = await this.apiClient.getStandings(leagueId);
return (data as any).value || data;
} catch (error: unknown) {
throw error;
}
}
async getLeagueStats(): Promise<any> {
try {
const data = await this.apiClient.getTotal();
return (data as any).value || data;
} catch (error: unknown) {
throw error;
}
}
async getLeagueSchedule(leagueId: string): Promise<any> {
try {
const data = await this.apiClient.getSchedule(leagueId);
return (data as any).value || data;
} catch (error: unknown) {
throw error;
}
}
async getLeagueMemberships(leagueId: string): Promise<any> {
try {
const data = await this.apiClient.getMemberships(leagueId);
return (data as any).value || data;
} catch (error: unknown) {
throw error;
}
}
async createLeague(input: CreateLeagueInputDTO): Promise<any> {
try {
const data = await this.apiClient.create(input);
return (data as any).value || data;
} catch (error: unknown) {
throw error;
}
}
async removeMember(leagueId: string, targetDriverId: string): Promise<any> {
try {
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
return { success: dto.success };
} catch (error: unknown) {
throw error;
}
}
async getAllLeagues(): Promise<any> {
try {
const dto = await this.apiClient.getAllWithCapacityAndScoring();
return Result.ok(dto);
return (dto as any).value || dto;
} catch (error: unknown) {
console.error('LeagueService.getAllLeagues failed:', error);
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch leagues' });
throw error;
}
}
@@ -142,28 +199,6 @@ export class LeagueService implements Service {
}
}
async getLeagueStandings(): Promise<Result<never, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'League standings endpoint not implemented' });
}
async getLeagueStats(): Promise<Result<TotalLeaguesDTO, DomainError>> {
try {
const data = await this.apiClient.getTotal();
return Result.ok(data);
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league stats' });
}
}
async getLeagueSchedule(leagueId: string): Promise<Result<LeagueScheduleDTO, DomainError>> {
try {
const data = await this.apiClient.getSchedule(leagueId);
return Result.ok(data);
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league schedule' });
}
}
async getLeagueSeasons(leagueId: string): Promise<Result<LeagueSeasonSummaryDTO[], DomainError>> {
try {
const data = await this.apiClient.getSeasons(leagueId);
@@ -289,33 +324,6 @@ export class LeagueService implements Service {
return this.deleteAdminScheduleRace(leagueId, seasonId, raceId);
}
async getLeagueMemberships(leagueId: string): Promise<Result<LeagueMembershipsDTO, DomainError>> {
try {
const data = await this.apiClient.getMemberships(leagueId);
return Result.ok(data);
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch memberships' });
}
}
async createLeague(input: CreateLeagueInputDTO): Promise<Result<CreateLeagueOutputDTO, DomainError>> {
try {
const data = await this.apiClient.create(input);
return Result.ok(data);
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create league' });
}
}
async removeMember(leagueId: string, targetDriverId: string): Promise<Result<{ success: boolean }, DomainError>> {
try {
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
return Result.ok({ success: dto.success });
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to remove member' });
}
}
async updateMemberRole(leagueId: string, targetDriverId: string, newRole: MembershipRole): Promise<Result<{ success: boolean }, DomainError>> {
try {
const dto = await this.apiClient.updateRosterMemberRole(leagueId, targetDriverId, newRole);

View File

@@ -1,10 +1,64 @@
import { Result } from '@/lib/contracts/Result';
import { Service, type DomainError } from '@/lib/contracts/services/Service';
import { type LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel';
export class LeagueSettingsService implements Service {
private static cachedMemberships = new Map<string, unknown[]>();
constructor(
private readonly leaguesApiClient?: LeaguesApiClient,
private readonly driversApiClient?: DriversApiClient,
) {}
async getLeagueSettings(leagueId: string): Promise<LeagueSettingsViewModel | null> {
if (!this.leaguesApiClient || !this.driversApiClient) {
return null;
}
try {
const [leaguesRes, configRes, presetsRes, leaderboardRes, membershipsRes] = await Promise.all([
this.leaguesApiClient.getAllWithCapacity(),
this.leaguesApiClient.getLeagueConfig(leagueId),
this.leaguesApiClient.getScoringPresets(),
this.driversApiClient.getLeaderboard(),
this.leaguesApiClient.getMemberships(leagueId),
]);
const leaguesData = (leaguesRes as any).value || leaguesRes;
const configData = (configRes as any).value || configRes;
const presetsData = (presetsRes as any).value || presetsRes;
const leaderboardData = (leaderboardRes as any).value || leaderboardRes;
const membershipsData = (membershipsRes as any).value || membershipsRes;
const league = leaguesData.leagues.find((l: any) => l.id === leagueId);
if (!league) return null;
const ownerRes = await this.driversApiClient.getDriver(league.ownerId);
const owner = (ownerRes as any).value || ownerRes;
return new LeagueSettingsViewModel({
league,
config: configData.config || configData,
presets: presetsData.presets,
owner,
members: membershipsData.members,
drivers: leaderboardData.drivers,
} as any);
} catch (error) {
return null;
}
}
async transferOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<boolean> {
if (!this.leaguesApiClient) throw new Error('API client not initialized');
const res = await this.leaguesApiClient.transferOwnership(leagueId, currentOwnerId, newOwnerId);
const data = (res as any).value || res;
return data.success;
}
async getSettingsData(leagueId: string): Promise<Result<LeagueSettingsApiDto, DomainError>> {
// Mock data since backend not implemented
const mockData: LeagueSettingsApiDto = {

View File

@@ -1,8 +1,86 @@
import { Result } from '@/lib/contracts/Result';
import { Service, type DomainError } from '@/lib/contracts/services/Service';
import { type StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto';
import { RaceService } from '../races/RaceService';
import { ProtestService } from '../protests/ProtestService';
import { PenaltyService } from '../penalties/PenaltyService';
import { DriverService } from '../drivers/DriverService';
import { LeagueMembershipService } from './LeagueMembershipService';
import { LeagueStewardingViewModel } from '@/lib/view-models/LeagueStewardingViewModel';
export class LeagueStewardingService implements Service {
constructor(
private readonly raceService?: RaceService,
private readonly protestService?: ProtestService,
private readonly penaltyService?: PenaltyService,
private readonly driverService?: DriverService,
private readonly leagueMembershipService?: LeagueMembershipService,
) {}
async getLeagueStewardingData(leagueId: string): Promise<LeagueStewardingViewModel> {
if (!this.raceService || !this.protestService || !this.penaltyService || !this.driverService) {
return new LeagueStewardingViewModel([], {});
}
const racesRes = await this.raceService.findByLeagueId(leagueId);
const races = (racesRes as any).value || racesRes;
const racesWithData = await Promise.all(
races.map(async (race: any) => {
const [protestsRes, penaltiesRes] = await Promise.all([
this.protestService!.findByRaceId(race.id),
this.penaltyService!.findByRaceId(race.id),
]);
const protests = (protestsRes as any).value || protestsRes;
const penalties = (penaltiesRes as any).value || penaltiesRes;
return {
race: {
id: race.id,
track: race.track,
scheduledAt: new Date(race.scheduledAt),
},
pendingProtests: protests.filter((p: any) => p.status === 'pending' || p.status === 'under_review'),
resolvedProtests: protests.filter((p: any) => p.status !== 'pending' && p.status !== 'under_review'),
penalties: penalties,
};
}),
);
const driverIds = new Set<string>();
racesWithData.forEach((r: any) => {
r.pendingProtests.forEach((p: any) => {
driverIds.add(p.protestingDriverId);
driverIds.add(p.accusedDriverId);
});
r.resolvedProtests.forEach((p: any) => {
driverIds.add(p.protestingDriverId);
driverIds.add(p.accusedDriverId);
});
r.penalties.forEach((p: any) => driverIds.add(p.driverId));
});
const driversRes = await this.driverService.findByIds(Array.from(driverIds));
const drivers = (driversRes as any).value || driversRes;
const driverMap: Record<string, any> = {};
drivers.forEach((d: any) => {
driverMap[d.id] = d;
});
return new LeagueStewardingViewModel(racesWithData as any, driverMap);
}
async reviewProtest(input: any): Promise<void> {
if (this.protestService) {
await this.protestService.reviewProtest(input);
}
}
async applyPenalty(input: any): Promise<void> {
if (this.penaltyService) {
await this.penaltyService.applyPenalty(input);
}
}
async getStewardingData(leagueId: string): Promise<Result<StewardingApiDto, DomainError>> {
// Mock data since backend not implemented
const mockData: StewardingApiDto = {
@@ -16,7 +94,22 @@ export class LeagueStewardingService implements Service {
return Result.ok(mockData);
}
async getProtestDetailViewModel(_: string, __: string): Promise<Result<any, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getProtestDetailViewModel' });
async getProtestDetailViewModel(leagueId: string, protestId: string): Promise<any> {
if (!this.protestService || !this.penaltyService) return null;
const [protestRes, penaltyTypesRes] = await Promise.all([
this.protestService.getProtestById(leagueId, protestId),
this.penaltyService.getPenaltyTypesReference(),
]);
const protestData = (protestRes as any).value || protestRes;
const penaltyTypesData = (penaltyTypesRes as any).value || penaltyTypesRes;
return {
...protestData,
penaltyTypes: penaltyTypesData.penaltyTypes,
defaultReasons: penaltyTypesData.defaultReasons,
initialPenaltyType: penaltyTypesData.penaltyTypes[0]?.type,
};
}
}

View File

@@ -1,9 +1,16 @@
import { Result } from '@/lib/contracts/Result';
import { Service, DomainError } from '@/lib/contracts/services/Service';
import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto';
import { WalletsApiClient } from '@/lib/api/wallets/WalletsApiClient';
export class LeagueWalletService implements Service {
constructor(private readonly apiClient?: WalletsApiClient) {}
async getWalletForLeague(leagueId: string): Promise<LeagueWalletApiDto> {
if (this.apiClient) {
const res = await this.apiClient.getLeagueWallet(leagueId);
return ((res as any).value || res) as any;
}
const result = await this.getWalletData(leagueId);
if (result.isErr()) throw new Error(result.getError().message);
return result.unwrap();
@@ -14,8 +21,17 @@ export class LeagueWalletService implements Service {
amount: number,
currency: string,
seasonId: string,
destinationId: string
destinationAccount: string
): Promise<{ success: boolean; message?: string }> {
if (this.apiClient) {
const res = await this.apiClient.withdrawFromLeagueWallet(leagueId, {
amount,
currency,
seasonId,
destinationAccount,
});
return (res as any).value || res;
}
// Mock implementation
return { success: true };
}

View File

@@ -1,9 +1,12 @@
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MediaService } from './MediaService';
import { Result } from '@/lib/contracts/Result';
// Simple test that verifies the service structure
describe('MediaService', () => {
beforeEach(() => {
vi.stubEnv('NEXT_PUBLIC_API_BASE_URL', 'http://localhost:3001');
});
it('should be defined', () => {
expect(MediaService).toBeDefined();
});

View File

@@ -15,22 +15,27 @@ import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporte
export class PenaltyService implements Service {
private readonly apiClient: PenaltiesApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger);
this.apiClient = new PenaltiesApiClient(baseUrl, errorReporter, logger);
constructor(apiClient?: PenaltiesApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger);
this.apiClient = new PenaltiesApiClient(baseUrl, errorReporter, logger);
}
}
/**
* Find penalties by race ID
*/
async findByRaceId(raceId: string): Promise<Result<unknown[], DomainError>> {
async findByRaceId(raceId: string): Promise<any> {
try {
const dto = await this.apiClient.getRacePenalties(raceId);
return Result.ok(dto.penalties);
const res = await this.apiClient.getRacePenalties(raceId);
const data = (res as any).value || res;
return data.penalties;
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to find penalties' });
throw error;
}
}
@@ -49,13 +54,13 @@ export class PenaltyService implements Service {
/**
* Apply a penalty
*/
async applyPenalty(input: unknown): Promise<Result<void, DomainError>> {
async applyPenalty(input: unknown): Promise<any> {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await this.apiClient.applyPenalty(input as any);
return Result.ok(undefined);
const res = await this.apiClient.applyPenalty(input as any);
return (res as any)?.value || res;
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to apply penalty' });
throw error;
}
}
}

View File

@@ -7,74 +7,100 @@ import { DomainError, Service } from '@/lib/contracts/services/Service';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ProtestViewModel } from '@/lib/view-models/ProtestViewModel';
import { RaceViewModel } from '@/lib/view-models/RaceViewModel';
import { ProtestDriverViewModel } from '@/lib/view-models/ProtestDriverViewModel';
/**
* Protest Service - DTO Only
*
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
* @server-safe
*/
export class ProtestService implements Service {
private readonly apiClient: ProtestsApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger);
this.apiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
constructor(apiClient?: ProtestsApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger);
this.apiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
}
}
async getLeagueProtests(leagueId: string): Promise<Result<unknown, DomainError>> {
async getLeagueProtests(leagueId: string): Promise<any> {
try {
const data = await this.apiClient.getLeagueProtests(leagueId);
return Result.ok(data);
const protests = (data as any).protests || [];
return {
...data,
protests: protests.map((p: any) => new ProtestViewModel(p)),
};
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get league protests' });
throw error;
}
}
async getProtestById(leagueId: string, protestId: string): Promise<Result<unknown, DomainError>> {
async getProtestById(leagueId: string, protestId: string): Promise<any> {
try {
const data = await this.apiClient.getLeagueProtest(leagueId, protestId);
return Result.ok(data);
const protests = (data as any).protests || [];
const protest = protests.find((p: any) => p.id === protestId);
if (!protest) return null;
const raceData = (data as any).racesById?.[protest.raceId] || (data as any).races?.[0] || Object.values((data as any).racesById || {})[0];
const protestingDriverData = (data as any).driversById?.[protest.protestingDriverId] || (data as any).drivers?.[0] || Object.values((data as any).driversById || {})[0];
const accusedDriverData = (data as any).driversById?.[protest.accusedDriverId] || (data as any).drivers?.[1] || Object.values((data as any).driversById || {})[1];
return {
protest: new ProtestViewModel(protest),
race: raceData ? new RaceViewModel(raceData) : null,
protestingDriver: protestingDriverData ? new ProtestDriverViewModel(protestingDriverData) : null,
accusedDriver: accusedDriverData ? new ProtestDriverViewModel(accusedDriverData) : null,
};
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get protest' });
throw error;
}
}
async applyPenalty(input: ApplyPenaltyCommandDTO): Promise<Result<void, DomainError>> {
async applyPenalty(input: ApplyPenaltyCommandDTO): Promise<any> {
try {
await this.apiClient.applyPenalty(input);
return Result.ok(undefined);
const res = await this.apiClient.applyPenalty(input);
return (res as any)?.value || res;
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to apply penalty' });
throw error;
}
}
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<Result<void, DomainError>> {
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<any> {
try {
await this.apiClient.requestDefense(input);
return Result.ok(undefined);
const res = await this.apiClient.requestDefense(input);
return (res as any)?.value || res;
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to request defense' });
throw error;
}
}
async reviewProtest(input: ReviewProtestCommandDTO): Promise<Result<void, DomainError>> {
async reviewProtest(input: ReviewProtestCommandDTO): Promise<any> {
try {
await this.apiClient.reviewProtest(input);
return Result.ok(undefined);
const res = await this.apiClient.reviewProtest(input);
return (res as any)?.value || res;
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to review protest' });
throw error;
}
}
async findByRaceId(raceId: string): Promise<Result<unknown, DomainError>> {
async findByRaceId(raceId: string): Promise<any> {
try {
const data = await this.apiClient.getRaceProtests(raceId);
return Result.ok(data);
const res = await this.apiClient.getRaceProtests(raceId);
const data = (res as any).value || res;
return data.protests;
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to find protests' });
throw error;
}
}
}

View File

@@ -5,6 +5,9 @@ import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ApiError } from '@/lib/api/base/ApiError';
import { RaceResultsDetailViewModel } from '@/lib/view-models/RaceResultsDetailViewModel';
import { RaceWithSOFViewModel } from '@/lib/view-models/RaceWithSOFViewModel';
import { ImportRaceResultsSummaryViewModel } from '@/lib/view-models/ImportRaceResultsSummaryViewModel';
/**
* Race Results Service
@@ -15,13 +18,29 @@ import { ApiError } from '@/lib/api/base/ApiError';
export class RaceResultsService implements Service {
private apiClient: RacesApiClient;
constructor() {
// Service creates its own dependencies
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new ConsoleErrorReporter();
this.apiClient = new RacesApiClient(baseUrl, errorReporter, logger);
constructor(apiClient?: RacesApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
// Service creates its own dependencies
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new ConsoleErrorReporter();
this.apiClient = new RacesApiClient(baseUrl, errorReporter, logger);
}
}
async getResultsDetail(raceId: string, currentUserId?: string): Promise<any> {
const res = await this.getRaceResultsDetail(raceId);
if (res.isErr()) throw new Error((res as any).error.message);
const data = (res as any).value;
return new RaceResultsDetailViewModel({ ...data, currentUserId: (currentUserId === undefined || currentUserId === null) ? '' : currentUserId }, {} as any);
}
async importResults(raceId: string, input: any): Promise<any> {
const res = await this.apiClient.importResults(raceId, input);
return new ImportRaceResultsSummaryViewModel(res);
}
/**
@@ -50,21 +69,12 @@ export class RaceResultsService implements Service {
* Get race with strength of field
* Returns race data with SOF calculation
*/
async getWithSOF(raceId: string): Promise<Result<unknown, DomainError>> {
async getWithSOF(raceId: string): Promise<any> {
try {
const data = await this.apiClient.getWithSOF(raceId);
return Result.ok(data);
return new RaceWithSOFViewModel(data);
} catch (error: unknown) {
if (error instanceof ApiError) {
return Result.err({
type: this.mapApiErrorType(error.type),
message: error.message
});
}
return Result.err({
type: 'unknown',
message: (error as Error).message || 'Failed to fetch race SOF'
});
throw error;
}
}

View File

@@ -7,6 +7,7 @@ import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ApiError } from '@/lib/api/base/ApiError';
import { RaceStewardingViewModel } from '@/lib/view-models/RaceStewardingViewModel';
/**
* Race Stewarding Service
@@ -19,26 +20,52 @@ export class RaceStewardingService implements Service {
private protestsApiClient: ProtestsApiClient;
private penaltiesApiClient: PenaltiesApiClient;
constructor() {
// Service creates its own dependencies
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new ConsoleErrorReporter();
this.racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
this.protestsApiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
this.penaltiesApiClient = new PenaltiesApiClient(baseUrl, errorReporter, logger);
constructor(racesApiClient?: RacesApiClient, protestsApiClient?: ProtestsApiClient, penaltiesApiClient?: PenaltiesApiClient) {
if (racesApiClient && protestsApiClient && penaltiesApiClient) {
this.racesApiClient = racesApiClient;
this.protestsApiClient = protestsApiClient;
this.penaltiesApiClient = penaltiesApiClient;
} else {
// Service creates its own dependencies
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new ConsoleErrorReporter();
this.racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
this.protestsApiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
this.penaltiesApiClient = new PenaltiesApiClient(baseUrl, errorReporter, logger);
}
}
async getRaceStewardingData(raceId: string, driverId: string): Promise<any> {
const res = await this.getRaceStewarding(raceId, driverId);
if (res.isErr()) throw new Error((res as any).error.message);
const data = (res as any).value;
return new RaceStewardingViewModel({
raceDetail: {
race: data.race,
league: data.league,
},
protests: {
protests: data.protests,
driverMap: data.driverMap,
},
penalties: {
penalties: data.penalties,
driverMap: data.driverMap,
},
} as any);
}
/**
* Get race stewarding data
* Returns protests and penalties for a race
*/
async getRaceStewarding(raceId: string): Promise<Result<unknown, DomainError>> {
async getRaceStewarding(raceId: string, driverId: string = ''): Promise<Result<unknown, DomainError>> {
try {
// Fetch data in parallel
const [raceDetail, protests, penalties] = await Promise.all([
this.racesApiClient.getDetail(raceId, ''),
this.racesApiClient.getDetail(raceId, driverId),
this.protestsApiClient.getRaceProtests(raceId),
this.penaltiesApiClient.getRacePenalties(raceId),
]);

View File

@@ -16,33 +16,38 @@ import { isProductionEnvironment } from '@/lib/config/env';
export class TeamJoinService implements Service {
private apiClient: TeamsApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
}
async getJoinRequests(teamId: string, currentDriverId: string, isOwner: boolean): Promise<Result<TeamJoinRequestViewModel[], DomainError>> {
try {
const result = await this.apiClient.getJoinRequests(teamId);
return Result.ok(result.requests.map(request =>
new TeamJoinRequestViewModel(request, currentDriverId, isOwner)
));
} catch (error: any) {
return Result.err({ type: 'serverError', message: error.message || 'Failed to fetch join requests' });
constructor(apiClient?: TeamsApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
}
}
async approveJoinRequest(): Promise<Result<void, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'Not implemented: API endpoint for approving join requests' });
async getJoinRequests(teamId: string, currentDriverId: string, isOwner: boolean): Promise<any[]> {
try {
const result = await this.apiClient.getJoinRequests(teamId);
const requests = (result as any).requests || result;
return requests.map((request: any) =>
new TeamJoinRequestViewModel(request, currentDriverId, isOwner)
);
} catch (error: any) {
throw error;
}
}
async rejectJoinRequest(): Promise<Result<void, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'Not implemented: API endpoint for rejecting join requests' });
async approveJoinRequest(): Promise<void> {
throw new Error('Not implemented: API endpoint for approving join requests');
}
async rejectJoinRequest(): Promise<void> {
throw new Error('Not implemented: API endpoint for rejecting join requests');
}
}

View File

@@ -8,6 +8,8 @@ import type { GetDriverTeamOutputDTO } from '@/lib/types/generated/GetDriverTeam
import type { GetTeamMembershipOutputDTO } from '@/lib/types/generated/GetTeamMembershipOutputDTO';
import type { GetTeamJoinRequestsOutputDTO } from '@/lib/types/generated/GetTeamJoinRequestsOutputDTO';
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
import { Result } from '@/lib/contracts/Result';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
@@ -24,80 +26,111 @@ import { isProductionEnvironment } from '@/lib/config/env';
export class TeamService implements Service {
private apiClient: TeamsApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
}
async getAllTeams(): Promise<Result<TeamListItemDTO[], DomainError>> {
try {
const result = await this.apiClient.getAll();
return Result.ok(result.teams);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch teams' });
constructor(apiClient?: TeamsApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
}
}
async getTeamDetails(teamId: string, _: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getDetails(teamId);
if (!result) {
return Result.err({ type: 'notFound', message: 'Team not found' });
}
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch team details' });
}
}
async getTeamMembers(teamId: string, currentDriverId: string, ownerId: string): Promise<Result<TeamMemberViewModel[], DomainError>> {
async getMembers(teamId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getMembers(teamId);
return Result.ok(result.members.map(member => new TeamMemberViewModel(member, currentDriverId, ownerId)));
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch team members' });
}
}
async getTeamJoinRequests(teamId: string): Promise<Result<GetTeamJoinRequestsOutputDTO, DomainError>> {
async update(teamId: string, input: UpdateTeamInputDTO): Promise<Result<UpdateTeamOutputDTO, DomainError>> {
return this.updateTeam(teamId, input);
}
async create(input: CreateTeamInputDTO): Promise<Result<CreateTeamOutputDTO, DomainError>> {
return this.createTeam(input);
}
async getTeamDetails(teamId: string, _: string): Promise<any> {
try {
const result = await this.apiClient.getDetails(teamId);
if (!result) {
return null;
}
const data = (result as any).value || result;
return new TeamDetailsViewModel(data, {} as any);
} catch (error: unknown) {
throw error;
}
}
async getTeamMembers(teamId: string, currentDriverId: string, ownerId: string): Promise<any> {
try {
const result = await this.apiClient.getMembers(teamId);
const members = (result as any).members || result;
return members.map((member: any) => new TeamMemberViewModel(member, currentDriverId, ownerId));
} catch (error: unknown) {
throw error;
}
}
async getTeamJoinRequests(teamId: string): Promise<any> {
try {
const result = await this.apiClient.getJoinRequests(teamId);
return Result.ok(result);
return (result as any).value || result;
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch team join requests' });
throw error;
}
}
async createTeam(input: CreateTeamInputDTO): Promise<Result<CreateTeamOutputDTO, DomainError>> {
async createTeam(input: CreateTeamInputDTO): Promise<any> {
try {
const result = await this.apiClient.create(input);
return Result.ok(result);
return (result as any).value || result;
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to create team' });
throw error;
}
}
async updateTeam(teamId: string, input: UpdateTeamInputDTO): Promise<Result<UpdateTeamOutputDTO, DomainError>> {
async updateTeam(teamId: string, input: UpdateTeamInputDTO): Promise<any> {
try {
const result = await this.apiClient.update(teamId, input);
return Result.ok(result);
return (result as any).value || result;
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to update team' });
throw error;
}
}
async getDriverTeam(driverId: string): Promise<Result<GetDriverTeamOutputDTO | null, DomainError>> {
async getDriverTeam(driverId: string): Promise<any> {
try {
const result = await this.apiClient.getDriverTeam(driverId);
return Result.ok(result);
if (!result) return null;
const data = (result as any).value || result;
if (!data.team) return null;
return {
teamId: data.team.id,
teamName: data.team.name,
role: data.membership?.role,
};
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch driver team' });
throw error;
}
}
async getAllTeams(): Promise<any> {
try {
const result = await this.apiClient.getAll();
const teams = (result as any).teams || result;
return teams.map((t: any) => new TeamSummaryViewModel(t));
} catch (error: unknown) {
throw error;
}
}

View File

@@ -0,0 +1,5 @@
import { ActionItem } from '@/lib/queries/ActionsPageQuery';
export interface ActionsViewData {
actions: ActionItem[];
}

View File

@@ -0,0 +1,8 @@
import { MediaAsset } from '@/components/media/MediaGallery';
export interface MediaViewData {
assets: MediaAsset[];
categories: { label: string; value: string }[];
title: string;
description?: string;
}

View File

@@ -0,0 +1,3 @@
export interface ProfileLayoutViewData {
// Empty for now
}

View File

@@ -0,0 +1,12 @@
export interface ProfileLiveryViewData {
id: string;
carId: string;
carName: string;
thumbnailUrl: string;
uploadedAt: Date;
isValidated: boolean;
}
export interface ProfileLiveriesViewData {
liveries: ProfileLiveryViewData[];
}