website refactor
This commit is contained in:
@@ -3,8 +3,6 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { ApiError } from './ApiError';
|
||||
import { CircuitBreakerRegistry } from './RetryHandler';
|
||||
|
||||
export type ConnectionStatus = 'connected' | 'disconnected' | 'degraded' | 'checking';
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ export class BaseApiClient {
|
||||
/**
|
||||
* Get developer-friendly hint for troubleshooting
|
||||
*/
|
||||
private getDeveloperHint(error: Error, path: string, method: string): string {
|
||||
private getDeveloperHint(error: Error, _path: string, _method: string): string {
|
||||
if (error.message.includes('fetch failed') || error.message.includes('Failed to fetch')) {
|
||||
return 'Check if API server is running and CORS is configured correctly';
|
||||
}
|
||||
@@ -183,7 +183,7 @@ export class BaseApiClient {
|
||||
/**
|
||||
* Get troubleshooting context for network errors
|
||||
*/
|
||||
private getTroubleshootingContext(error: Error, path: string): string {
|
||||
private getTroubleshootingContext(error: Error, _path: string): string {
|
||||
if (typeof window !== 'undefined') {
|
||||
const baseUrl = this.baseUrl;
|
||||
const currentOrigin = window.location.origin;
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface CacheEntry<T> {
|
||||
* Simple in-memory cache for API responses
|
||||
*/
|
||||
class ResponseCache {
|
||||
private cache = new Map<string, CacheEntry<any>>();
|
||||
private cache = new Map<string, CacheEntry<unknown>>();
|
||||
|
||||
/**
|
||||
* Get cached data if not expired
|
||||
@@ -46,20 +46,20 @@ class ResponseCache {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return null;
|
||||
|
||||
if (new Date() > entry.expiry) {
|
||||
if (new globalThis.Date() > entry.expiry) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data;
|
||||
return entry.data as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached data with expiry
|
||||
*/
|
||||
set<T>(key: string, data: T, ttlMs: number = 300000): void {
|
||||
const now = new Date();
|
||||
const expiry = new Date(now.getTime() + ttlMs);
|
||||
const now = new globalThis.Date();
|
||||
const expiry = new globalThis.Date(now.getTime() + ttlMs);
|
||||
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
@@ -133,7 +133,7 @@ export async function withGracefulDegradation<T>(
|
||||
'API unavailable and no fallback provided',
|
||||
'NETWORK_ERROR',
|
||||
{
|
||||
timestamp: new Date().toISOString(),
|
||||
timestamp: new globalThis.Date().toISOString(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Retry logic and circuit breaker for API requests
|
||||
*/
|
||||
|
||||
import { ApiError, ApiErrorType } from './ApiError';
|
||||
import { ApiError } from './ApiError';
|
||||
|
||||
export interface RetryConfig {
|
||||
maxRetries: number;
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { LeaguesApiClient } from './leagues/LeaguesApiClient';
|
||||
import { RacesApiClient } from './races/RacesApiClient';
|
||||
import { DriversApiClient } from './drivers/DriversApiClient';
|
||||
import { TeamsApiClient } from './teams/TeamsApiClient';
|
||||
import { SponsorsApiClient } from './sponsors/SponsorsApiClient';
|
||||
import { MediaApiClient } from './media/MediaApiClient';
|
||||
import { AnalyticsApiClient } from './analytics/AnalyticsApiClient';
|
||||
import { AuthApiClient } from './auth/AuthApiClient';
|
||||
import { PaymentsApiClient } from './payments/PaymentsApiClient';
|
||||
import { DashboardApiClient } from './dashboard/DashboardApiClient';
|
||||
import { PenaltiesApiClient } from './penalties/PenaltiesApiClient';
|
||||
import { ProtestsApiClient } from './protests/ProtestsApiClient';
|
||||
import { AdminApiClient } from './admin/AdminApiClient';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
|
||||
/**
|
||||
* Main API Client
|
||||
*
|
||||
* Orchestrates all domain-specific API clients with consistent configuration.
|
||||
*/
|
||||
export class ApiClient {
|
||||
public readonly leagues: LeaguesApiClient;
|
||||
public readonly races: RacesApiClient;
|
||||
public readonly drivers: DriversApiClient;
|
||||
public readonly teams: TeamsApiClient;
|
||||
public readonly sponsors: SponsorsApiClient;
|
||||
public readonly media: MediaApiClient;
|
||||
public readonly analytics: AnalyticsApiClient;
|
||||
public readonly auth: AuthApiClient;
|
||||
public readonly payments: PaymentsApiClient;
|
||||
public readonly dashboard: DashboardApiClient;
|
||||
public readonly penalties: PenaltiesApiClient;
|
||||
public readonly protests: ProtestsApiClient;
|
||||
public readonly admin: AdminApiClient;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
|
||||
this.leagues = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
this.races = new RacesApiClient(baseUrl, errorReporter, logger);
|
||||
this.drivers = new DriversApiClient(baseUrl, errorReporter, logger);
|
||||
this.teams = new TeamsApiClient(baseUrl, errorReporter, logger);
|
||||
this.sponsors = new SponsorsApiClient(baseUrl, errorReporter, logger);
|
||||
this.media = new MediaApiClient(baseUrl, errorReporter, logger);
|
||||
this.analytics = new AnalyticsApiClient(baseUrl, errorReporter, logger);
|
||||
this.auth = new AuthApiClient(baseUrl, errorReporter, logger);
|
||||
this.payments = new PaymentsApiClient(baseUrl, errorReporter, logger);
|
||||
this.dashboard = new DashboardApiClient(baseUrl, errorReporter, logger);
|
||||
this.penalties = new PenaltiesApiClient(baseUrl, errorReporter, logger);
|
||||
this.protests = new ProtestsApiClient(baseUrl, errorReporter, logger);
|
||||
this.admin = new AdminApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Singleton Instance
|
||||
// ============================================================================
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
|
||||
export const api = new ApiClient(API_BASE_URL);
|
||||
@@ -134,7 +134,7 @@ export class LeaguesApiClient extends BaseApiClient {
|
||||
seasonId: string,
|
||||
input: CreateLeagueScheduleRaceInputDTO,
|
||||
): Promise<CreateLeagueScheduleRaceOutputDTO> {
|
||||
const { example: _example, ...payload } = input;
|
||||
const { example: _, ...payload } = input;
|
||||
return this.post<CreateLeagueScheduleRaceOutputDTO>(`/leagues/${leagueId}/seasons/${seasonId}/schedule/races`, payload);
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ export class LeaguesApiClient extends BaseApiClient {
|
||||
raceId: string,
|
||||
input: UpdateLeagueScheduleRaceInputDTO,
|
||||
): Promise<LeagueScheduleRaceMutationSuccessDTO> {
|
||||
const { example: _example, ...payload } = input;
|
||||
const { example: _, ...payload } = input;
|
||||
return this.patch<LeagueScheduleRaceMutationSuccessDTO>(`/leagues/${leagueId}/seasons/${seasonId}/schedule/races/${raceId}`, payload);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ type UpdateMemberPaymentInputDto = {
|
||||
paidAt?: Date | string;
|
||||
};
|
||||
type UpdateMemberPaymentOutputDto = { payment: MemberPaymentDTO };
|
||||
type GetWalletTransactionsOutputDto = { transactions: TransactionDTO[] };
|
||||
type UpdatePaymentStatusOutputDto = { payment: PaymentDTO };
|
||||
type UpsertMembershipFeeInputDto = {
|
||||
leagueId: string;
|
||||
@@ -70,7 +69,6 @@ type AwardPrizeInputDto = {
|
||||
driverId: string;
|
||||
};
|
||||
type AwardPrizeOutputDto = { prize: PrizeDTO };
|
||||
type DeletePrizeInputDto = { prizeId: string };
|
||||
type DeletePrizeOutputDto = { success: boolean };
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,8 +12,6 @@ import type { RaceDetailEntryDTO } from '../../types/generated/RaceDetailEntryDT
|
||||
import type { RaceDetailRegistrationDTO } from '../../types/generated/RaceDetailRegistrationDTO';
|
||||
import type { RaceDetailUserResultDTO } from '../../types/generated/RaceDetailUserResultDTO';
|
||||
import type { FileProtestCommandDTO } from '../../types/generated/FileProtestCommandDTO';
|
||||
import type { AllRacesPageDTO } from '../../types/generated/AllRacesPageDTO';
|
||||
import type { FilteredRacesPageDataDTO } from '../../types/tbd/FilteredRacesPageDataDTO';
|
||||
|
||||
// Define missing types
|
||||
export type RacesPageDataDTO = { races: RacesPageDataRaceDTO[] };
|
||||
|
||||
@@ -66,22 +66,29 @@ export class SponsorsApiClient extends BaseApiClient {
|
||||
|
||||
/** Get sponsor billing information */
|
||||
getBilling(sponsorId: string): Promise<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
paymentMethods: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
invoices: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
stats: any;
|
||||
}> {
|
||||
return this.get(`/sponsors/billing/${sponsorId}`);
|
||||
}
|
||||
|
||||
/** Get available leagues for sponsorship */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getAvailableLeagues(): Promise<any[]> {
|
||||
return this.get('/sponsors/leagues/available');
|
||||
}
|
||||
|
||||
/** Get detailed league information */
|
||||
getLeagueDetail(leagueId: string): Promise<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
league: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
drivers: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
races: any[];
|
||||
}> {
|
||||
return this.get(`/sponsors/leagues/${leagueId}/detail`);
|
||||
@@ -89,14 +96,18 @@ export class SponsorsApiClient extends BaseApiClient {
|
||||
|
||||
/** Get sponsor settings */
|
||||
getSettings(sponsorId: string): Promise<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
profile: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
notifications: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
privacy: any;
|
||||
}> {
|
||||
return this.get(`/sponsors/settings/${sponsorId}`);
|
||||
}
|
||||
|
||||
/** Update sponsor settings */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateSettings(sponsorId: string, input: any): Promise<void> {
|
||||
return this.put(`/sponsors/settings/${sponsorId}`, input);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||
import type { GetAllTeamsOutputDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO';
|
||||
import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO';
|
||||
import type { GetTeamMembersOutputDTO } from '@/lib/types/generated/GetTeamMembersOutputDTO';
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
import { useCurrentSession } from "@/lib/hooks/auth/useCurrentSession";
|
||||
import { useLogout } from "@/lib/hooks/auth/useLogout";
|
||||
|
||||
export type AuthContextValue = {
|
||||
session: SessionViewModel | null;
|
||||
loading: boolean;
|
||||
login: (returnTo?: string) => void;
|
||||
logout: () => Promise<void>;
|
||||
refreshSession: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||
|
||||
interface AuthProviderProps {
|
||||
initialSession?: SessionViewModel | null;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AuthProvider({ initialSession = null, children }: AuthProviderProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// Use React-Query hooks for session management
|
||||
const { data: session, isLoading, refetch: refreshSession } = useCurrentSession({
|
||||
initialData: initialSession,
|
||||
});
|
||||
|
||||
// Use mutation hooks for logout
|
||||
const logoutMutation = useLogout();
|
||||
|
||||
const login = useCallback(
|
||||
(returnTo?: string) => {
|
||||
const search = new URLSearchParams();
|
||||
if (returnTo) {
|
||||
search.set('returnTo', returnTo);
|
||||
}
|
||||
|
||||
const target = search.toString()
|
||||
? `/auth/login?${search.toString()}`
|
||||
: '/auth/login';
|
||||
|
||||
router.push(target);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await logoutMutation.mutateAsync();
|
||||
router.push('/');
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
router.push('/');
|
||||
}
|
||||
}, [logoutMutation, router]);
|
||||
|
||||
const handleRefreshSession = useCallback(async () => {
|
||||
await refreshSession();
|
||||
}, [refreshSession]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
session: session ?? null,
|
||||
loading: isLoading,
|
||||
login,
|
||||
logout,
|
||||
refreshSession: handleRefreshSession,
|
||||
}),
|
||||
[session, isLoading, login, logout, handleRefreshSession],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @file index.ts
|
||||
* Blockers exports
|
||||
*/
|
||||
|
||||
export { Blocker } from './Blocker';
|
||||
export { SubmitBlocker } from './SubmitBlocker';
|
||||
export { ThrottleBlocker } from './ThrottleBlocker';
|
||||
@@ -18,9 +18,9 @@ export class AdminUsersViewDataBuilder {
|
||||
roles: user.roles,
|
||||
status: user.status,
|
||||
isSystemAdmin: user.isSystemAdmin,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
updatedAt: user.updatedAt.toISOString(),
|
||||
lastLoginAt: user.lastLoginAt?.toISOString(),
|
||||
createdAt: typeof user.createdAt === 'string' ? user.createdAt : (user.createdAt as unknown as Date).toISOString(),
|
||||
updatedAt: typeof user.updatedAt === 'string' ? user.updatedAt : (user.updatedAt as unknown as Date).toISOString(),
|
||||
lastLoginAt: user.lastLoginAt ? (typeof user.lastLoginAt === 'string' ? user.lastLoginAt : (user.lastLoginAt as unknown as Date).toISOString()) : undefined,
|
||||
primaryDriverId: user.primaryDriverId,
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||
import type { DriverProfileViewData } from './DriverProfileViewData';
|
||||
import type { DriverProfileViewData } from '@/lib/types/view-data/DriverProfileViewData';
|
||||
|
||||
/**
|
||||
* DriverProfileViewDataBuilder
|
||||
@@ -10,12 +10,82 @@ import type { DriverProfileViewData } from './DriverProfileViewData';
|
||||
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,
|
||||
currentDriver: apiDto.currentDriver ? {
|
||||
id: apiDto.currentDriver.id,
|
||||
name: apiDto.currentDriver.name,
|
||||
country: apiDto.currentDriver.country,
|
||||
avatarUrl: apiDto.currentDriver.avatarUrl || '',
|
||||
iracingId: typeof apiDto.currentDriver.iracingId === 'string' ? parseInt(apiDto.currentDriver.iracingId, 10) : (apiDto.currentDriver.iracingId ?? null),
|
||||
joinedAt: apiDto.currentDriver.joinedAt,
|
||||
rating: apiDto.currentDriver.rating ?? null,
|
||||
globalRank: apiDto.currentDriver.globalRank ?? null,
|
||||
consistency: apiDto.currentDriver.consistency ?? null,
|
||||
bio: apiDto.currentDriver.bio ?? null,
|
||||
totalDrivers: apiDto.currentDriver.totalDrivers ?? null,
|
||||
} : null,
|
||||
stats: apiDto.stats ? {
|
||||
totalRaces: apiDto.stats.totalRaces,
|
||||
wins: apiDto.stats.wins,
|
||||
podiums: apiDto.stats.podiums,
|
||||
dnfs: apiDto.stats.dnfs,
|
||||
avgFinish: apiDto.stats.avgFinish ?? null,
|
||||
bestFinish: apiDto.stats.bestFinish ?? null,
|
||||
worstFinish: apiDto.stats.worstFinish ?? null,
|
||||
finishRate: apiDto.stats.finishRate ?? null,
|
||||
winRate: apiDto.stats.winRate ?? null,
|
||||
podiumRate: apiDto.stats.podiumRate ?? null,
|
||||
percentile: apiDto.stats.percentile ?? null,
|
||||
rating: apiDto.stats.rating ?? null,
|
||||
consistency: apiDto.stats.consistency ?? null,
|
||||
overallRank: apiDto.stats.overallRank ?? null,
|
||||
} : null,
|
||||
finishDistribution: apiDto.finishDistribution ? {
|
||||
totalRaces: apiDto.finishDistribution.totalRaces,
|
||||
wins: apiDto.finishDistribution.wins,
|
||||
podiums: apiDto.finishDistribution.podiums,
|
||||
topTen: apiDto.finishDistribution.topTen,
|
||||
dnfs: apiDto.finishDistribution.dnfs,
|
||||
other: apiDto.finishDistribution.other,
|
||||
} : null,
|
||||
teamMemberships: apiDto.teamMemberships.map(m => ({
|
||||
teamId: m.teamId,
|
||||
teamName: m.teamName,
|
||||
teamTag: m.teamTag ?? null,
|
||||
role: m.role,
|
||||
joinedAt: m.joinedAt,
|
||||
isCurrent: m.isCurrent,
|
||||
})),
|
||||
socialSummary: {
|
||||
friendsCount: apiDto.socialSummary.friendsCount,
|
||||
friends: apiDto.socialSummary.friends.map(f => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
country: f.country,
|
||||
avatarUrl: f.avatarUrl || '',
|
||||
})),
|
||||
},
|
||||
extendedProfile: apiDto.extendedProfile ? {
|
||||
socialHandles: apiDto.extendedProfile.socialHandles.map(h => ({
|
||||
platform: h.platform,
|
||||
handle: h.handle,
|
||||
url: h.url,
|
||||
})),
|
||||
achievements: apiDto.extendedProfile.achievements.map(a => ({
|
||||
id: a.id,
|
||||
title: a.title,
|
||||
description: a.description,
|
||||
icon: a.icon,
|
||||
rarity: a.rarity,
|
||||
earnedAt: a.earnedAt,
|
||||
})),
|
||||
racingStyle: apiDto.extendedProfile.racingStyle,
|
||||
favoriteTrack: apiDto.extendedProfile.favoriteTrack,
|
||||
favoriteCar: apiDto.extendedProfile.favoriteCar,
|
||||
timezone: apiDto.extendedProfile.timezone,
|
||||
availableHours: apiDto.extendedProfile.availableHours,
|
||||
lookingForTeam: apiDto.extendedProfile.lookingForTeam,
|
||||
openToRequests: apiDto.extendedProfile.openToRequests,
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export class LeagueDetailViewDataBuilder {
|
||||
const membersCount = Array.isArray(memberships.members) ? memberships.members.length : 0;
|
||||
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)
|
||||
? Math.round(races.reduce((sum, _r) => sum + 0, 0) / races.length)
|
||||
: null;
|
||||
|
||||
const info: LeagueInfoData = {
|
||||
|
||||
@@ -5,9 +5,11 @@ export class LeagueSponsorshipsViewDataBuilder {
|
||||
static build(apiDto: LeagueSponsorshipsApiDto): LeagueSponsorshipsViewData {
|
||||
return {
|
||||
leagueId: apiDto.leagueId,
|
||||
activeTab: 'overview',
|
||||
onTabChange: () => {},
|
||||
league: apiDto.league,
|
||||
sponsorshipSlots: apiDto.sponsorshipSlots,
|
||||
sponsorshipRequests: apiDto.sponsorshipRequests,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
import { LeagueWalletViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
|
||||
import { LeagueWalletViewData, LeagueWalletTransactionViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
|
||||
import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto';
|
||||
|
||||
export class LeagueWalletViewDataBuilder {
|
||||
static build(apiDto: LeagueWalletApiDto): LeagueWalletViewData {
|
||||
const transactions: LeagueWalletTransactionViewData[] = apiDto.transactions.map(t => ({
|
||||
...t,
|
||||
formattedAmount: `${t.amount} ${apiDto.currency}`,
|
||||
amountColor: t.amount >= 0 ? 'green' : 'red',
|
||||
formattedDate: new Date(t.createdAt).toLocaleDateString(),
|
||||
statusColor: t.status === 'completed' ? 'green' : t.status === 'pending' ? 'yellow' : 'red',
|
||||
typeColor: 'blue',
|
||||
}));
|
||||
|
||||
return {
|
||||
leagueId: apiDto.leagueId,
|
||||
balance: apiDto.balance,
|
||||
formattedBalance: `${apiDto.balance} ${apiDto.currency}`,
|
||||
totalRevenue: apiDto.balance, // Mock
|
||||
formattedTotalRevenue: `${apiDto.balance} ${apiDto.currency}`,
|
||||
totalFees: 0, // Mock
|
||||
formattedTotalFees: `0 ${apiDto.currency}`,
|
||||
pendingPayouts: 0, // Mock
|
||||
formattedPendingPayouts: `0 ${apiDto.currency}`,
|
||||
currency: apiDto.currency,
|
||||
transactions: apiDto.transactions,
|
||||
transactions,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { RaceResultsViewData, RaceResultsResult, RaceResultsPenalty } from '@/li
|
||||
* Deterministic, side-effect free.
|
||||
*/
|
||||
export class RaceResultsViewDataBuilder {
|
||||
static build(apiDto: any): RaceResultsViewData {
|
||||
static build(apiDto: unknown): RaceResultsViewData {
|
||||
if (!apiDto) {
|
||||
return {
|
||||
raceSOF: null,
|
||||
@@ -18,8 +18,11 @@ export class RaceResultsViewDataBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const dto = apiDto as any;
|
||||
|
||||
// Transform results
|
||||
const results: RaceResultsResult[] = (apiDto.results || []).map((result: any) => ({
|
||||
const results: RaceResultsResult[] = (dto.results || []).map((result: any) => ({
|
||||
position: result.position,
|
||||
driverId: result.driverId,
|
||||
driverName: result.driverName,
|
||||
@@ -35,7 +38,7 @@ export class RaceResultsViewDataBuilder {
|
||||
}));
|
||||
|
||||
// Transform penalties
|
||||
const penalties: RaceResultsPenalty[] = (apiDto.penalties || []).map((penalty: any) => ({
|
||||
const penalties: RaceResultsPenalty[] = (dto.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',
|
||||
@@ -45,15 +48,15 @@ export class RaceResultsViewDataBuilder {
|
||||
}));
|
||||
|
||||
return {
|
||||
raceTrack: apiDto.race?.track,
|
||||
raceScheduledAt: apiDto.race?.scheduledAt,
|
||||
totalDrivers: apiDto.stats?.totalDrivers,
|
||||
leagueName: apiDto.league?.name,
|
||||
raceSOF: apiDto.strengthOfField || null,
|
||||
raceTrack: dto.race?.track,
|
||||
raceScheduledAt: dto.race?.scheduledAt,
|
||||
totalDrivers: dto.stats?.totalDrivers,
|
||||
leagueName: dto.league?.name,
|
||||
raceSOF: dto.strengthOfField || null,
|
||||
results,
|
||||
penalties,
|
||||
pointsSystem: apiDto.pointsSystem || {},
|
||||
fastestLapTime: apiDto.fastestLapTime || 0,
|
||||
pointsSystem: dto.pointsSystem || {},
|
||||
fastestLapTime: dto.fastestLapTime || 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { RaceStewardingViewData, Protest, Penalty, Driver } from '@/lib/view-dat
|
||||
* Deterministic, side-effect free.
|
||||
*/
|
||||
export class RaceStewardingViewDataBuilder {
|
||||
static build(apiDto: any): RaceStewardingViewData {
|
||||
static build(apiDto: unknown): RaceStewardingViewData {
|
||||
if (!apiDto) {
|
||||
return {
|
||||
race: null,
|
||||
@@ -22,17 +22,20 @@ export class RaceStewardingViewDataBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
const race = apiDto.race ? {
|
||||
id: apiDto.race.id,
|
||||
track: apiDto.race.track,
|
||||
scheduledAt: apiDto.race.scheduledAt,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const dto = apiDto as any;
|
||||
|
||||
const race = dto.race ? {
|
||||
id: dto.race.id,
|
||||
track: dto.race.track,
|
||||
scheduledAt: dto.race.scheduledAt,
|
||||
} : null;
|
||||
|
||||
const league = apiDto.league ? {
|
||||
id: apiDto.league.id,
|
||||
const league = dto.league ? {
|
||||
id: dto.league.id,
|
||||
} : null;
|
||||
|
||||
const pendingProtests: Protest[] = (apiDto.pendingProtests || []).map((p: any) => ({
|
||||
const pendingProtests: Protest[] = (dto.pendingProtests || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
protestingDriverId: p.protestingDriverId,
|
||||
accusedDriverId: p.accusedDriverId,
|
||||
@@ -46,7 +49,7 @@ export class RaceStewardingViewDataBuilder {
|
||||
decisionNotes: p.decisionNotes,
|
||||
}));
|
||||
|
||||
const resolvedProtests: Protest[] = (apiDto.resolvedProtests || []).map((p: any) => ({
|
||||
const resolvedProtests: Protest[] = (dto.resolvedProtests || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
protestingDriverId: p.protestingDriverId,
|
||||
accusedDriverId: p.accusedDriverId,
|
||||
@@ -60,7 +63,7 @@ export class RaceStewardingViewDataBuilder {
|
||||
decisionNotes: p.decisionNotes,
|
||||
}));
|
||||
|
||||
const penalties: Penalty[] = (apiDto.penalties || []).map((p: any) => ({
|
||||
const penalties: Penalty[] = (dto.penalties || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
driverId: p.driverId,
|
||||
type: p.type,
|
||||
@@ -69,7 +72,7 @@ export class RaceStewardingViewDataBuilder {
|
||||
notes: p.notes,
|
||||
}));
|
||||
|
||||
const driverMap: Record<string, Driver> = apiDto.driverMap || {};
|
||||
const driverMap: Record<string, Driver> = dto.driverMap || {};
|
||||
|
||||
return {
|
||||
race,
|
||||
@@ -78,9 +81,9 @@ export class RaceStewardingViewDataBuilder {
|
||||
resolvedProtests,
|
||||
penalties,
|
||||
driverMap,
|
||||
pendingCount: apiDto.pendingCount || pendingProtests.length,
|
||||
resolvedCount: apiDto.resolvedCount || resolvedProtests.length,
|
||||
penaltiesCount: apiDto.penaltiesCount || penalties.length,
|
||||
pendingCount: dto.pendingCount || pendingProtests.length,
|
||||
resolvedCount: dto.resolvedCount || resolvedProtests.length,
|
||||
penaltiesCount: dto.penaltiesCount || penalties.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RacesAllViewData, RacesAllRace } from '@/lib/view-data/races/RacesAllViewData';
|
||||
import { RacesAllViewData } from '@/lib/view-data/races/RacesAllViewData';
|
||||
|
||||
/**
|
||||
* Races All View Data Builder
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TeamDetailPageDto } from '@/lib/page-queries/page-queries/TeamDetailPageQuery';
|
||||
import type { TeamDetailPageDto } from '@/lib/page-queries/TeamDetailPageQuery';
|
||||
import type { TeamDetailViewData, TeamDetailData, TeamMemberData, SponsorMetric, TeamTab } from '@/lib/view-data/TeamDetailViewData';
|
||||
import { Users, Zap, Calendar } from 'lucide-react';
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TeamsPageDto } from '@/lib/page-queries/page-queries/TeamsPageQuery';
|
||||
import type { TeamsPageDto } from '@/lib/page-queries/TeamsPageQuery';
|
||||
import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData';
|
||||
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
|
||||
|
||||
/**
|
||||
* TeamsViewDataBuilder - Transforms TeamsPageDto into ViewData for TeamsTemplate
|
||||
@@ -7,7 +8,7 @@ import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewDa
|
||||
*/
|
||||
export class TeamsViewDataBuilder {
|
||||
static build(apiDto: TeamsPageDto): TeamsViewData {
|
||||
const teams: TeamSummaryData[] = apiDto.teams.map((team): TeamSummaryData => ({
|
||||
const teams: TeamSummaryData[] = apiDto.teams.map((team: TeamListItemDTO): TeamSummaryData => ({
|
||||
teamId: team.id,
|
||||
teamName: team.name,
|
||||
leagueName: team.leagues[0] || '',
|
||||
@@ -17,4 +18,4 @@ export class TeamsViewDataBuilder {
|
||||
|
||||
return { teams };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ export class DriverProfileViewModelBuilder {
|
||||
private static transformExtendedProfile(dto: DriverProfileExtendedProfileDTO): DriverProfileExtendedProfileViewModel {
|
||||
return {
|
||||
socialHandles: dto.socialHandles.map(h => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
platform: h.platform as any, // Type assertion - assuming valid platform
|
||||
handle: h.handle,
|
||||
url: h.url,
|
||||
@@ -119,7 +120,9 @@ export class DriverProfileViewModelBuilder {
|
||||
id: a.id,
|
||||
title: a.title,
|
||||
description: a.description,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
icon: a.icon as any, // Type assertion - assuming valid icon
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
rarity: a.rarity as any, // Type assertion - assuming valid rarity
|
||||
earnedAt: a.earnedAt,
|
||||
})),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
|
||||
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
|
||||
import { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,9 +18,9 @@ export interface ResultError<E> {
|
||||
isOk(): false;
|
||||
isErr(): true;
|
||||
unwrap(): never;
|
||||
unwrapOr(defaultValue: any): any;
|
||||
unwrapOr<T>(defaultValue: T): T;
|
||||
getError(): E;
|
||||
map<U>(fn: (value: any) => U): ResultError<E>;
|
||||
map<U>(fn: (value: unknown) => U): ResultError<E>;
|
||||
}
|
||||
|
||||
export class Ok<T> implements ResultOk<T> {
|
||||
@@ -29,7 +29,7 @@ export class Ok<T> implements ResultOk<T> {
|
||||
isOk(): true { return true; }
|
||||
isErr(): false { return false; }
|
||||
unwrap(): T { return this.value; }
|
||||
unwrapOr(_defaultValue: T): T { return this.value; }
|
||||
unwrapOr(_: T): T { return this.value; }
|
||||
getError(): never {
|
||||
throw new Error('Cannot get error from Ok result');
|
||||
}
|
||||
@@ -46,10 +46,10 @@ export class Err<E> implements ResultError<E> {
|
||||
unwrap(): never {
|
||||
throw new Error(`Called unwrap on error: ${this.error}`);
|
||||
}
|
||||
unwrapOr(defaultValue: any): any { return defaultValue; }
|
||||
unwrapOr<T>(defaultValue: T): T { return defaultValue; }
|
||||
getError(): E { return this.error; }
|
||||
map<U>(_fn: (value: any) => U): ResultError<E> {
|
||||
return this as any;
|
||||
map<U>(_: (value: unknown) => U): ResultError<E> {
|
||||
return this as unknown as ResultError<E>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ export function createTestContainer(overrides: Map<symbol, any> = new Map()): Co
|
||||
const container = createContainer();
|
||||
|
||||
// Apply mock overrides using rebind
|
||||
const promises = Array.from(overrides.entries()).map(([token, mockInstance]) => {
|
||||
return container.rebind(token).then(bind => bind.toConstantValue(mockInstance));
|
||||
Array.from(overrides.entries()).forEach(([token, mockInstance]) => {
|
||||
container.rebind(token).then(bind => bind.toConstantValue(mockInstance));
|
||||
});
|
||||
|
||||
// Return container immediately, mocks will be available after promises resolve
|
||||
|
||||
@@ -74,11 +74,10 @@ export function useInjectAll<T>(serviceIdentifier: string | symbol): T[] {
|
||||
export function useInjectOptional<T>(token: symbol): T | null {
|
||||
const container = useContext(ContainerContext);
|
||||
|
||||
if (!container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return useMemo(() => {
|
||||
if (!container) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return container.get<T>(token);
|
||||
} catch {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Must be first import - enables decorator metadata
|
||||
import 'reflect-metadata';
|
||||
|
||||
// Core exports
|
||||
export * from './container';
|
||||
export * from './tokens';
|
||||
|
||||
// React integration
|
||||
export * from './hooks/useInject';
|
||||
export * from './hooks/useReactQueryWithApiError';
|
||||
export * from './providers/ContainerProvider';
|
||||
|
||||
// Modules
|
||||
export * from './modules/api.module';
|
||||
export * from './modules/auth.module';
|
||||
export * from './modules/core.module';
|
||||
export * from './modules/driver.module';
|
||||
export * from './modules/league.module';
|
||||
export * from './modules/race.module';
|
||||
export * from './modules/team.module';
|
||||
export * from './modules/landing.module';
|
||||
export * from './modules/policy.module';
|
||||
export * from './modules/sponsor.module';
|
||||
@@ -1,12 +1,10 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { AuthService } from '../../services/auth/AuthService';
|
||||
import { SessionService } from '../../services/auth/SessionService';
|
||||
import { AuthApiClient } from '../../api/auth/AuthApiClient';
|
||||
|
||||
import {
|
||||
AUTH_SERVICE_TOKEN,
|
||||
SESSION_SERVICE_TOKEN,
|
||||
AUTH_API_CLIENT_TOKEN
|
||||
} from '../tokens';
|
||||
|
||||
export const AuthModule = new ContainerModule((options) => {
|
||||
@@ -14,10 +12,7 @@ export const AuthModule = new ContainerModule((options) => {
|
||||
|
||||
// Session Service
|
||||
bind<SessionService>(SESSION_SERVICE_TOKEN)
|
||||
.toDynamicValue((ctx) => {
|
||||
const authApiClient = ctx.get<AuthApiClient>(AUTH_API_CLIENT_TOKEN);
|
||||
return new SessionService(authApiClient);
|
||||
})
|
||||
.to(SessionService)
|
||||
.inSingletonScope();
|
||||
|
||||
// Auth Service - now creates its own dependencies
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { DRIVER_SERVICE_TOKEN, DRIVER_API_CLIENT_TOKEN, ONBOARDING_SERVICE_TOKEN } from '../tokens';
|
||||
import { DRIVER_SERVICE_TOKEN, ONBOARDING_SERVICE_TOKEN } from '../tokens';
|
||||
import { DriverService } from '@/lib/services/drivers/DriverService';
|
||||
import { OnboardingService } from '@/lib/services/onboarding/OnboardingService';
|
||||
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
|
||||
|
||||
export const DriverModule = new ContainerModule((options) => {
|
||||
const bind = options.bind;
|
||||
|
||||
bind(DRIVER_SERVICE_TOKEN)
|
||||
.toDynamicValue((ctx) => {
|
||||
const apiClient = ctx.get<DriversApiClient>(DRIVER_API_CLIENT_TOKEN);
|
||||
return new DriverService(apiClient);
|
||||
})
|
||||
.to(DriverService)
|
||||
.inSingletonScope();
|
||||
|
||||
bind(ONBOARDING_SERVICE_TOKEN)
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { LANDING_SERVICE_TOKEN, RACE_API_CLIENT_TOKEN, LEAGUE_API_CLIENT_TOKEN, TEAM_API_CLIENT_TOKEN, AUTH_API_CLIENT_TOKEN } from '../tokens';
|
||||
import { LANDING_SERVICE_TOKEN } from '../tokens';
|
||||
import { LandingService } from '@/lib/services/landing/LandingService';
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
|
||||
export const LandingModule = new ContainerModule((options) => {
|
||||
const bind = options.bind;
|
||||
|
||||
// Landing Service
|
||||
bind<LandingService>(LANDING_SERVICE_TOKEN)
|
||||
.toDynamicValue((ctx) => {
|
||||
const racesApi = ctx.get<RacesApiClient>(RACE_API_CLIENT_TOKEN);
|
||||
const leaguesApi = ctx.get<LeaguesApiClient>(LEAGUE_API_CLIENT_TOKEN);
|
||||
const teamsApi = ctx.get<TeamsApiClient>(TEAM_API_CLIENT_TOKEN);
|
||||
const authApi = ctx.get<AuthApiClient>(AUTH_API_CLIENT_TOKEN);
|
||||
|
||||
return new LandingService(racesApi, leaguesApi, teamsApi, authApi);
|
||||
})
|
||||
.to(LandingService)
|
||||
.inSingletonScope();
|
||||
});
|
||||
@@ -18,36 +18,26 @@ export const LeagueModule = new ContainerModule((options) => {
|
||||
|
||||
// League Service
|
||||
bind<LeagueService>(LEAGUE_SERVICE_TOKEN)
|
||||
.toDynamicValue(() => {
|
||||
return new LeagueService();
|
||||
})
|
||||
.to(LeagueService)
|
||||
.inSingletonScope();
|
||||
|
||||
// League Settings Service
|
||||
bind<LeagueSettingsService>(LEAGUE_SETTINGS_SERVICE_TOKEN)
|
||||
.toDynamicValue(() => {
|
||||
return new LeagueSettingsService();
|
||||
})
|
||||
.to(LeagueSettingsService)
|
||||
.inSingletonScope();
|
||||
|
||||
// League Stewarding Service
|
||||
bind<LeagueStewardingService>(LEAGUE_STEWARDING_SERVICE_TOKEN)
|
||||
.toDynamicValue(() => {
|
||||
return new LeagueStewardingService();
|
||||
})
|
||||
.to(LeagueStewardingService)
|
||||
.inSingletonScope();
|
||||
|
||||
// League Wallet Service
|
||||
bind<LeagueWalletService>(LEAGUE_WALLET_SERVICE_TOKEN)
|
||||
.toDynamicValue(() => {
|
||||
return new LeagueWalletService();
|
||||
})
|
||||
.to(LeagueWalletService)
|
||||
.inSingletonScope();
|
||||
|
||||
// League Membership Service
|
||||
bind<LeagueMembershipService>(LEAGUE_MEMBERSHIP_SERVICE_TOKEN)
|
||||
.toDynamicValue(() => {
|
||||
return new LeagueMembershipService();
|
||||
})
|
||||
.to(LeagueMembershipService)
|
||||
.inSingletonScope();
|
||||
});
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { POLICY_SERVICE_TOKEN, POLICY_API_CLIENT_TOKEN } from '../tokens';
|
||||
import { POLICY_SERVICE_TOKEN } from '../tokens';
|
||||
import { PolicyService } from '@/lib/services/policy/PolicyService';
|
||||
import { PolicyApiClient } from '@/lib/api/policy/PolicyApiClient';
|
||||
|
||||
export const PolicyModule = new ContainerModule((options) => {
|
||||
const bind = options.bind;
|
||||
|
||||
// Policy Service
|
||||
bind<PolicyService>(POLICY_SERVICE_TOKEN)
|
||||
.toDynamicValue((ctx) => {
|
||||
const apiClient = ctx.get<PolicyApiClient>(POLICY_API_CLIENT_TOKEN);
|
||||
return new PolicyService(apiClient);
|
||||
})
|
||||
.to(PolicyService)
|
||||
.inSingletonScope();
|
||||
});
|
||||
@@ -3,17 +3,10 @@ import { RaceService } from '@/lib/services/races/RaceService';
|
||||
import { RaceResultsService } from '@/lib/services/races/RaceResultsService';
|
||||
import { RaceStewardingService } from '@/lib/services/races/RaceStewardingService';
|
||||
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
import { ProtestsApiClient } from '@/lib/api/protests/ProtestsApiClient';
|
||||
import { PenaltiesApiClient } from '@/lib/api/penalties/PenaltiesApiClient';
|
||||
|
||||
import {
|
||||
RACE_SERVICE_TOKEN,
|
||||
RACE_RESULTS_SERVICE_TOKEN,
|
||||
RACE_STEWARDING_SERVICE_TOKEN,
|
||||
RACE_API_CLIENT_TOKEN,
|
||||
PROTEST_API_CLIENT_TOKEN,
|
||||
PENALTY_API_CLIENT_TOKEN,
|
||||
} from '../tokens';
|
||||
|
||||
export const RaceModule = new ContainerModule((options) => {
|
||||
@@ -21,10 +14,7 @@ export const RaceModule = new ContainerModule((options) => {
|
||||
|
||||
// Race Service - creates its own dependencies per contract
|
||||
bind<RaceService>(RACE_SERVICE_TOKEN)
|
||||
.toDynamicValue((ctx) => {
|
||||
const raceApiClient = ctx.get<RacesApiClient>(RACE_API_CLIENT_TOKEN);
|
||||
return new RaceService(raceApiClient);
|
||||
})
|
||||
.to(RaceService)
|
||||
.inSingletonScope();
|
||||
|
||||
// Race Results Service - creates its own dependencies per contract
|
||||
|
||||
@@ -8,11 +8,10 @@ import { ApiError } from '../api/base/ApiError';
|
||||
import { connectionMonitor } from '../api/base/ApiConnectionMonitor';
|
||||
|
||||
// Import notification system (will be used if available)
|
||||
let notificationSystem: any = null;
|
||||
try {
|
||||
// Dynamically import to avoid circular dependencies
|
||||
import('@/components/notifications/NotificationProvider').then(module => {
|
||||
notificationSystem = module;
|
||||
import('@/components/notifications/NotificationProvider').then(_module => {
|
||||
// Notification system available
|
||||
}).catch(() => {
|
||||
// Notification system not available yet
|
||||
});
|
||||
|
||||
@@ -48,7 +48,6 @@ export class ErrorReplaySystem {
|
||||
* Capture current state for replay
|
||||
*/
|
||||
captureReplay(error: Error | ApiError, additionalContext: Record<string, unknown> = {}): ReplayContext {
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
const replayId = `replay_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
@@ -230,7 +230,7 @@ export class GlobalErrorHandler {
|
||||
private setupReactErrorHandling(): void {
|
||||
// This will be used by React Error Boundaries
|
||||
// We'll provide a global registry for React errors
|
||||
(window as any).__GRIDPILOT_REACT_ERRORS__ = [];
|
||||
(window as { __GRIDPILOT_REACT_ERRORS__?: unknown[] }).__GRIDPILOT_REACT_ERRORS__ = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,14 +254,14 @@ export class GlobalErrorHandler {
|
||||
width: window.screen.width,
|
||||
height: window.screen.height,
|
||||
},
|
||||
memory: (performance as any).memory ? {
|
||||
usedJSHeapSize: (performance as any).memory.usedJSHeapSize,
|
||||
totalJSHeapSize: (performance as any).memory.totalJSHeapSize,
|
||||
memory: (performance as unknown as { memory?: { usedJSHeapSize: number; totalJSHeapSize: number } }).memory ? {
|
||||
usedJSHeapSize: (performance as unknown as { memory: { usedJSHeapSize: number; totalJSHeapSize: number } }).memory.usedJSHeapSize,
|
||||
totalJSHeapSize: (performance as unknown as { memory: { usedJSHeapSize: number; totalJSHeapSize: number } }).memory.totalJSHeapSize,
|
||||
} : null,
|
||||
connection: (navigator as any).connection ? {
|
||||
effectiveType: (navigator as any).connection.effectiveType,
|
||||
downlink: (navigator as any).connection.downlink,
|
||||
rtt: (navigator as any).connection.rtt,
|
||||
connection: (navigator as unknown as { connection?: { effectiveType: string; downlink: number; rtt: number } }).connection ? {
|
||||
effectiveType: (navigator as unknown as { connection: { effectiveType: string; downlink: number; rtt: number } }).connection.effectiveType,
|
||||
downlink: (navigator as unknown as { connection: { effectiveType: string; downlink: number; rtt: number } }).connection.downlink,
|
||||
rtt: (navigator as unknown as { connection: { effectiveType: string; downlink: number; rtt: number } }).connection.rtt,
|
||||
} : null,
|
||||
...additionalContext,
|
||||
enhancedStack: this.options.captureEnhancedStacks ? this.enhanceStackTrace(stack) : undefined,
|
||||
@@ -467,8 +467,8 @@ export class GlobalErrorHandler {
|
||||
window.removeEventListener('unhandledrejection', this.handleUnhandledRejection);
|
||||
|
||||
// Restore original console.error
|
||||
if ((console as any)._originalError) {
|
||||
console.error = (console as any)._originalError;
|
||||
if ((console as unknown as { _originalError?: typeof console.error })._originalError) {
|
||||
console.error = (console as unknown as { _originalError: typeof console.error })._originalError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export class ConsoleLogger implements Logger {
|
||||
|
||||
if (supportsGrouping) {
|
||||
// Safe to call - we've verified both functions exist
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(console as any).groupCollapsed(`%c${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`, `color: ${color}; font-weight: bold;`);
|
||||
} else {
|
||||
// Simple format for edge runtime
|
||||
@@ -73,6 +74,7 @@ export class ConsoleLogger implements Logger {
|
||||
|
||||
if (supportsGrouping) {
|
||||
// Safe to call - we've verified the function exists
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(console as any).groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Mutation } from '@/lib/contracts/mutations/Mutation';
|
||||
* Pattern: Server Action → Mutation → Service → API Client
|
||||
*/
|
||||
export class DeleteUserMutation implements Mutation<{ userId: string }, void, MutationError> {
|
||||
async execute(input: { userId: string }): Promise<Result<void, MutationError>> {
|
||||
async execute(_input: { userId: string }): Promise<Result<void, MutationError>> {
|
||||
try {
|
||||
// Manual construction: Service creates its own dependencies
|
||||
const service = new AdminService();
|
||||
|
||||
@@ -17,8 +17,11 @@ export class ForgotPasswordMutation {
|
||||
try {
|
||||
const authService = new AuthService();
|
||||
const result = await authService.forgotPassword(params);
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
if (result.isErr()) {
|
||||
return Result.err(result.getError().message);
|
||||
}
|
||||
return Result.ok(result.unwrap());
|
||||
} catch (error: any) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to send reset link';
|
||||
return Result.err(errorMessage);
|
||||
}
|
||||
|
||||
@@ -16,9 +16,12 @@ export class LoginMutation {
|
||||
async execute(params: LoginParamsDTO): Promise<Result<SessionViewModel, string>> {
|
||||
try {
|
||||
const authService = new AuthService();
|
||||
const session = await authService.login(params);
|
||||
return Result.ok(session);
|
||||
} catch (error) {
|
||||
const result = await authService.login(params);
|
||||
if (result.isErr()) {
|
||||
return Result.err(result.getError().message);
|
||||
}
|
||||
return Result.ok(new SessionViewModel(result.unwrap().user));
|
||||
} catch (error: any) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Login failed';
|
||||
return Result.err(errorMessage);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@ export class ResetPasswordMutation {
|
||||
try {
|
||||
const authService = new AuthService();
|
||||
const result = await authService.resetPassword(params);
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
if (result.isErr()) {
|
||||
return Result.err(result.getError().message);
|
||||
}
|
||||
return Result.ok(result.unwrap());
|
||||
} catch (error: any) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to reset password';
|
||||
return Result.err(errorMessage);
|
||||
}
|
||||
|
||||
@@ -16,9 +16,12 @@ export class SignupMutation {
|
||||
async execute(params: SignupParamsDTO): Promise<Result<SessionViewModel, string>> {
|
||||
try {
|
||||
const authService = new AuthService();
|
||||
const session = await authService.signup(params);
|
||||
return Result.ok(session);
|
||||
} catch (error) {
|
||||
const result = await authService.signup(params);
|
||||
if (result.isErr()) {
|
||||
return Result.err(result.getError().message);
|
||||
}
|
||||
return Result.ok(new SessionViewModel(result.unwrap().user));
|
||||
} catch (error: any) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Signup failed';
|
||||
return Result.err(errorMessage);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface UpdateDriverProfileCommand {
|
||||
|
||||
type UpdateDriverProfileMutationError = 'DRIVER_PROFILE_UPDATE_FAILED';
|
||||
|
||||
const mapToMutationError = (_error: DomainError): UpdateDriverProfileMutationError => {
|
||||
const mapToMutationError = (_: DomainError): UpdateDriverProfileMutationError => {
|
||||
return 'DRIVER_PROFILE_UPDATE_FAILED';
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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';
|
||||
import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
|
||||
import { DomainError } from '@/lib/contracts/services/Service';
|
||||
|
||||
/**
|
||||
* CreateLeagueMutation
|
||||
@@ -15,22 +13,19 @@ export class CreateLeagueMutation {
|
||||
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();
|
||||
}
|
||||
|
||||
async execute(input: CreateLeagueInputDTO): Promise<Result<string, string>> {
|
||||
async execute(input: CreateLeagueInputDTO): Promise<Result<string, DomainError>> {
|
||||
try {
|
||||
const result = await this.service.createLeague(input);
|
||||
return Result.ok(result.leagueId);
|
||||
} catch (error) {
|
||||
if (result.isErr()) {
|
||||
return Result.err(result.getError());
|
||||
}
|
||||
return Result.ok(result.unwrap().leagueId);
|
||||
} catch (error: any) {
|
||||
console.error('CreateLeagueMutation failed:', error);
|
||||
return Result.err('Failed to create league');
|
||||
return Result.err({ type: 'serverError', message: error.message || 'Failed to create league' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { ProtestService } from '@/lib/services/protests/ProtestService';
|
||||
import { ProtestsApiClient } from '@/lib/api/protests/ProtestsApiClient';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import type { ApplyPenaltyCommandDTO } from '@/lib/types/generated/ApplyPenaltyCommandDTO';
|
||||
import type { RequestProtestDefenseCommandDTO } from '@/lib/types/generated/RequestProtestDefenseCommandDTO';
|
||||
import { DomainError } from '@/lib/contracts/services/Service';
|
||||
|
||||
/**
|
||||
* ProtestReviewMutation
|
||||
@@ -16,42 +14,34 @@ export class ProtestReviewMutation {
|
||||
private service: ProtestService;
|
||||
|
||||
constructor() {
|
||||
// Manual wiring for serverless
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const apiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
this.service = new ProtestService(apiClient);
|
||||
this.service = new ProtestService();
|
||||
}
|
||||
|
||||
async applyPenalty(input: ApplyPenaltyCommandDTO): Promise<Result<void, string>> {
|
||||
async applyPenalty(input: ApplyPenaltyCommandDTO): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
await this.service.applyPenalty(input);
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
return await this.service.applyPenalty(input);
|
||||
} catch (error: unknown) {
|
||||
console.error('applyPenalty failed:', error);
|
||||
return Result.err('Failed to apply penalty');
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to apply penalty' });
|
||||
}
|
||||
}
|
||||
|
||||
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<Result<void, string>> {
|
||||
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
await this.service.requestDefense(input);
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
return await this.service.requestDefense(input);
|
||||
} catch (error: unknown) {
|
||||
console.error('requestDefense failed:', error);
|
||||
return Result.err('Failed to request defense');
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to request defense' });
|
||||
}
|
||||
}
|
||||
|
||||
async reviewProtest(input: { protestId: string; stewardId: string; decision: string; decisionNotes: string }): Promise<Result<void, string>> {
|
||||
async reviewProtest(input: { protestId: string; stewardId: string; decision: string; decisionNotes: string }): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
await this.service.reviewProtest(input as any);
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await this.service.reviewProtest(input as any);
|
||||
} catch (error: unknown) {
|
||||
console.error('reviewProtest failed:', error);
|
||||
return Result.err('Failed to review protest');
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to review protest' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class RosterAdminMutation {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
this.service = new LeagueService();
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ 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';
|
||||
import type { CreateLeagueScheduleRaceInputDTO } from '@/lib/types/generated/CreateLeagueScheduleRaceInputDTO';
|
||||
import type { UpdateLeagueScheduleRaceInputDTO } from '@/lib/types/generated/UpdateLeagueScheduleRaceInputDTO';
|
||||
|
||||
/**
|
||||
* ScheduleAdminMutation
|
||||
@@ -20,7 +18,7 @@ export class ScheduleAdminMutation {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
this.service = new LeagueService();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class StewardingMutation {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
this.service = new LeagueService();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class WalletMutation {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
this.service = new LeagueService();
|
||||
}
|
||||
|
||||
@@ -13,32 +13,22 @@ import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-
|
||||
*
|
||||
* Follows Clean Architecture: Uses builders for transformation.
|
||||
*/
|
||||
export class AdminDashboardPageQuery implements PageQuery<AdminDashboardViewData, void> {
|
||||
export class AdminDashboardPageQuery implements PageQuery<AdminDashboardViewData, void, PresentationError> {
|
||||
async execute(): Promise<Result<AdminDashboardViewData, PresentationError>> {
|
||||
try {
|
||||
// Manual construction: Service creates its own dependencies
|
||||
const adminService = new AdminService();
|
||||
// Manual construction: Service creates its own dependencies
|
||||
const adminService = new AdminService();
|
||||
|
||||
// Fetch dashboard stats
|
||||
const apiDtoResult = await adminService.getDashboardStats();
|
||||
// Fetch dashboard stats
|
||||
const apiDtoResult = await adminService.getDashboardStats();
|
||||
|
||||
if (apiDtoResult.isErr()) {
|
||||
return Result.err(mapToPresentationError(apiDtoResult.getError()));
|
||||
}
|
||||
|
||||
// Transform to ViewData using builder
|
||||
const output = AdminDashboardViewDataBuilder.build(apiDtoResult.unwrap());
|
||||
|
||||
return Result.ok(output);
|
||||
} catch (err) {
|
||||
console.error('AdminDashboardPageQuery failed:', err);
|
||||
|
||||
if (err instanceof Error && (err.message.includes('403') || err.message.includes('401'))) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
return Result.err('serverError');
|
||||
if (apiDtoResult.isErr()) {
|
||||
return Result.err(mapToPresentationError(apiDtoResult.getError()));
|
||||
}
|
||||
|
||||
// Transform to ViewData using builder
|
||||
const output = AdminDashboardViewDataBuilder.build(apiDtoResult.unwrap());
|
||||
|
||||
return Result.ok(output);
|
||||
}
|
||||
|
||||
// Static method to avoid object construction in server code
|
||||
@@ -46,4 +36,4 @@ export class AdminDashboardPageQuery implements PageQuery<AdminDashboardViewData
|
||||
const query = new AdminDashboardPageQuery();
|
||||
return query.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
*
|
||||
* Fetches data needed for the create league page.
|
||||
*/
|
||||
export class CreateLeaguePageQuery implements PageQuery<any, void> {
|
||||
async execute(): Promise<Result<any, 'notFound' | 'redirect' | 'CREATE_LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
export class CreateLeaguePageQuery implements PageQuery<unknown, void> {
|
||||
async execute(): Promise<Result<unknown, 'notFound' | 'redirect' | 'CREATE_LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
// Manual wiring: create API client
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
@@ -43,7 +43,7 @@ export class CreateLeaguePageQuery implements PageQuery<any, void> {
|
||||
}
|
||||
}
|
||||
|
||||
static async execute(): Promise<Result<any, 'notFound' | 'redirect' | 'CREATE_LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
static async execute(): Promise<Result<unknown, 'notFound' | 'redirect' | 'CREATE_LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
const query = new CreateLeaguePageQuery();
|
||||
return query.execute();
|
||||
}
|
||||
28
apps/website/lib/page-queries/LeagueDetailPageQuery.ts
Normal file
28
apps/website/lib/page-queries/LeagueDetailPageQuery.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueService } from '@/lib/services/leagues/LeagueService';
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
/**
|
||||
* LeagueDetail page query
|
||||
* Returns the raw API DTO for the league detail page
|
||||
* No DI container usage - constructs dependencies explicitly
|
||||
*/
|
||||
export class LeagueDetailPageQuery implements PageQuery<unknown, string, PresentationError> {
|
||||
async execute(leagueId: string): Promise<Result<unknown, PresentationError>> {
|
||||
const service = new LeagueService();
|
||||
const result = await service.getLeagueDetailData(leagueId);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
return Result.ok(result.unwrap());
|
||||
}
|
||||
|
||||
// Static method to avoid object construction in server code
|
||||
static async execute(leagueId: string): Promise<Result<unknown, PresentationError>> {
|
||||
const query = new LeagueDetailPageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
|
||||
import { ProtestDetailService } from '@/lib/services/leagues/ProtestDetailService';
|
||||
import { ProtestDetailViewDataBuilder } from '@/lib/builders/view-data/ProtestDetailViewDataBuilder';
|
||||
import { ProtestDetailViewData } from '@/lib/view-data/leagues/ProtestDetailViewData';
|
||||
|
||||
interface PresentationError {
|
||||
type: 'notFound' | 'forbidden' | 'serverError';
|
||||
message: string;
|
||||
}
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
export class LeagueProtestDetailPageQuery implements PageQuery<ProtestDetailViewData, { leagueId: string; protestId: string }, PresentationError> {
|
||||
async execute(params: { leagueId: string; protestId: string }): Promise<Result<ProtestDetailViewData, PresentationError>> {
|
||||
@@ -15,7 +11,7 @@ export class LeagueProtestDetailPageQuery implements PageQuery<ProtestDetailView
|
||||
const result = await service.getProtestDetail(params.leagueId, params.protestId);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err({ type: 'serverError', message: 'Failed to load protest details' });
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const viewData = ProtestDetailViewDataBuilder.build(result.unwrap());
|
||||
@@ -10,14 +10,14 @@ import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
*
|
||||
* Fetches protest detail data for review.
|
||||
*/
|
||||
export class LeagueProtestReviewPageQuery implements PageQuery<any, { leagueId: string; protestId: string }> {
|
||||
async execute(input: { leagueId: string; protestId: string }): Promise<Result<any, 'notFound' | 'redirect' | 'PROTEST_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
export class LeagueProtestReviewPageQuery implements PageQuery<unknown, { leagueId: string; protestId: string }> {
|
||||
async execute(input: { leagueId: string; protestId: string }): Promise<Result<unknown, 'notFound' | 'redirect' | 'PROTEST_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
// Manual wiring: create API clients
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const leaguesApiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
const protestsApiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
|
||||
new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
new ProtestsApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
try {
|
||||
// Get protest details
|
||||
@@ -70,7 +70,7 @@ export class LeagueProtestReviewPageQuery implements PageQuery<any, { leagueId:
|
||||
}
|
||||
}
|
||||
|
||||
static async execute(input: { leagueId: string; protestId: string }): Promise<Result<any, 'notFound' | 'redirect' | 'PROTEST_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
static async execute(input: { leagueId: string; protestId: string }): Promise<Result<unknown, 'notFound' | 'redirect' | 'PROTEST_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
const query = new LeagueProtestReviewPageQuery();
|
||||
return query.execute(input);
|
||||
}
|
||||
29
apps/website/lib/page-queries/LeagueRosterAdminPageQuery.ts
Normal file
29
apps/website/lib/page-queries/LeagueRosterAdminPageQuery.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueService } from '@/lib/services/leagues/LeagueService';
|
||||
import { LeagueRosterAdminViewDataBuilder } from '@/lib/builders/view-data/LeagueRosterAdminViewDataBuilder';
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
/**
|
||||
* LeagueRosterAdminPageQuery
|
||||
*
|
||||
* Fetches league roster admin data (members and join requests).
|
||||
*/
|
||||
export class LeagueRosterAdminPageQuery implements PageQuery<unknown, string, PresentationError> {
|
||||
async execute(leagueId: string): Promise<Result<unknown, PresentationError>> {
|
||||
const service = new LeagueService();
|
||||
const result = await service.getRosterAdminData(leagueId);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const viewData = LeagueRosterAdminViewDataBuilder.build(result.unwrap());
|
||||
return Result.ok(viewData);
|
||||
}
|
||||
|
||||
static async execute(leagueId: string): Promise<Result<unknown, PresentationError>> {
|
||||
const query = new LeagueRosterAdminPageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueRulebookService } from '@/lib/services/leagues/LeagueRulebookService';
|
||||
import { RulebookViewDataBuilder } from '@/lib/builders/view-data/RulebookViewDataBuilder';
|
||||
import { RulebookViewData } from '@/lib/view-data/leagues/RulebookViewData';
|
||||
|
||||
interface PresentationError {
|
||||
type: 'notFound' | 'forbidden' | 'serverError';
|
||||
message: string;
|
||||
}
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
export class LeagueRulebookPageQuery implements PageQuery<RulebookViewData, string, PresentationError> {
|
||||
async execute(leagueId: string): Promise<Result<RulebookViewData, PresentationError>> {
|
||||
@@ -15,7 +11,7 @@ export class LeagueRulebookPageQuery implements PageQuery<RulebookViewData, stri
|
||||
const result = await service.getRulebookData(leagueId);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err({ type: 'serverError', message: 'Failed to load rulebook data' });
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const viewData = RulebookViewDataBuilder.build(result.unwrap());
|
||||
@@ -26,4 +22,4 @@ export class LeagueRulebookPageQuery implements PageQuery<RulebookViewData, stri
|
||||
const query = new LeagueRulebookPageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueService } from '@/lib/services/leagues/LeagueService';
|
||||
import { LeagueScheduleViewDataBuilder } from '@/lib/builders/view-data/LeagueScheduleViewDataBuilder';
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
/**
|
||||
* LeagueScheduleAdminPageQuery
|
||||
*
|
||||
* Fetches league schedule admin data.
|
||||
*/
|
||||
export class LeagueScheduleAdminPageQuery implements PageQuery<unknown, { leagueId: string; seasonId?: string }> {
|
||||
async execute(input: { leagueId: string; seasonId?: string }): Promise<Result<unknown, PresentationError>> {
|
||||
const service = new LeagueService();
|
||||
const result = await service.getScheduleAdminData(input.leagueId, input.seasonId);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const data = result.unwrap();
|
||||
const viewData = LeagueScheduleViewDataBuilder.build({
|
||||
leagueId: data.leagueId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
races: data.schedule.races.map((r: any) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
date: r.scheduledAt,
|
||||
track: r.track,
|
||||
car: r.car,
|
||||
sessionType: r.sessionType,
|
||||
})),
|
||||
});
|
||||
return Result.ok(viewData);
|
||||
}
|
||||
|
||||
static async execute(input: { leagueId: string; seasonId?: string }): Promise<Result<unknown, PresentationError>> {
|
||||
const query = new LeagueScheduleAdminPageQuery();
|
||||
return query.execute(input);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueScheduleService } from '@/lib/services/leagues/LeagueScheduleService';
|
||||
import { LeagueScheduleViewDataBuilder } from '@/lib/builders/view-data/LeagueScheduleViewDataBuilder';
|
||||
import { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData';
|
||||
|
||||
interface PresentationError {
|
||||
type: 'notFound' | 'forbidden' | 'serverError';
|
||||
message: string;
|
||||
}
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
export class LeagueSchedulePageQuery implements PageQuery<LeagueScheduleViewData, string, PresentationError> {
|
||||
async execute(leagueId: string): Promise<Result<LeagueScheduleViewData, PresentationError>> {
|
||||
@@ -15,7 +11,7 @@ export class LeagueSchedulePageQuery implements PageQuery<LeagueScheduleViewData
|
||||
const result = await service.getScheduleData(leagueId);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err({ type: 'serverError', message: 'Failed to load schedule data' });
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const viewData = LeagueScheduleViewDataBuilder.build(result.unwrap());
|
||||
@@ -26,4 +22,4 @@ export class LeagueSchedulePageQuery implements PageQuery<LeagueScheduleViewData
|
||||
const query = new LeagueSchedulePageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueSettingsService } from '@/lib/services/leagues/LeagueSettingsService';
|
||||
import { LeagueSettingsViewDataBuilder } from '@/lib/builders/view-data/LeagueSettingsViewDataBuilder';
|
||||
import { LeagueSettingsViewData } from '@/lib/view-data/leagues/LeagueSettingsViewData';
|
||||
|
||||
interface PresentationError {
|
||||
type: 'notFound' | 'forbidden' | 'serverError';
|
||||
message: string;
|
||||
}
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
export class LeagueSettingsPageQuery implements PageQuery<LeagueSettingsViewData, string, PresentationError> {
|
||||
async execute(leagueId: string): Promise<Result<LeagueSettingsViewData, PresentationError>> {
|
||||
@@ -15,7 +11,7 @@ export class LeagueSettingsPageQuery implements PageQuery<LeagueSettingsViewData
|
||||
const result = await service.getSettingsData(leagueId);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err({ type: 'serverError', message: 'Failed to load settings data' });
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const viewData = LeagueSettingsViewDataBuilder.build(result.unwrap());
|
||||
@@ -26,4 +22,4 @@ export class LeagueSettingsPageQuery implements PageQuery<LeagueSettingsViewData
|
||||
const query = new LeagueSettingsPageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueSponsorshipsService } from '@/lib/services/leagues/LeagueSponsorshipsService';
|
||||
import { LeagueSponsorshipsViewDataBuilder } from '@/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder';
|
||||
import { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData';
|
||||
|
||||
interface PresentationError {
|
||||
type: 'notFound' | 'forbidden' | 'serverError';
|
||||
message: string;
|
||||
}
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
export class LeagueSponsorshipsPageQuery implements PageQuery<LeagueSponsorshipsViewData, string, PresentationError> {
|
||||
async execute(leagueId: string): Promise<Result<LeagueSponsorshipsViewData, PresentationError>> {
|
||||
@@ -15,7 +11,7 @@ export class LeagueSponsorshipsPageQuery implements PageQuery<LeagueSponsorships
|
||||
const result = await service.getSponsorshipsData(leagueId);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err({ type: 'serverError', message: 'Failed to load sponsorships data' });
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const viewData = LeagueSponsorshipsViewDataBuilder.build(result.unwrap());
|
||||
@@ -26,4 +22,4 @@ export class LeagueSponsorshipsPageQuery implements PageQuery<LeagueSponsorships
|
||||
const query = new LeagueSponsorshipsPageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueStandingsService } from '@/lib/services/leagues/LeagueStandingsService';
|
||||
import { LeagueStandingsViewDataBuilder } from '@/lib/builders/view-data/LeagueStandingsViewDataBuilder';
|
||||
import { LeagueStandingsViewData } from '@/lib/view-data/LeagueStandingsViewData';
|
||||
|
||||
interface PresentationError {
|
||||
type: 'notFound' | 'forbidden' | 'serverError';
|
||||
message: string;
|
||||
}
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
export class LeagueStandingsPageQuery implements PageQuery<LeagueStandingsViewData, string, PresentationError> {
|
||||
async execute(leagueId: string): Promise<Result<LeagueStandingsViewData, PresentationError>> {
|
||||
@@ -15,7 +11,7 @@ export class LeagueStandingsPageQuery implements PageQuery<LeagueStandingsViewDa
|
||||
const result = await service.getStandingsData(leagueId);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err({ type: 'serverError', message: 'Failed to load standings data' });
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const { standings, memberships } = result.unwrap();
|
||||
@@ -27,4 +23,4 @@ export class LeagueStandingsPageQuery implements PageQuery<LeagueStandingsViewDa
|
||||
const query = new LeagueStandingsPageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,7 @@ import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { LeagueStewardingService } from '@/lib/services/leagues/LeagueStewardingService';
|
||||
import { StewardingViewDataBuilder } from '@/lib/builders/view-data/StewardingViewDataBuilder';
|
||||
import { StewardingViewData } from '@/lib/view-data/leagues/StewardingViewData';
|
||||
|
||||
interface PresentationError {
|
||||
type: 'notFound' | 'forbidden' | 'notImplemented' | 'serverError';
|
||||
message: string;
|
||||
}
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
export class LeagueStewardingPageQuery implements PageQuery<StewardingViewData, string, PresentationError> {
|
||||
async execute(leagueId: string): Promise<Result<StewardingViewData, PresentationError>> {
|
||||
@@ -16,7 +12,7 @@ export class LeagueStewardingPageQuery implements PageQuery<StewardingViewData,
|
||||
|
||||
if (result.isErr()) {
|
||||
// Map domain errors to presentation errors
|
||||
return Result.err({ type: 'serverError', message: 'Failed to load stewarding data' });
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const viewData = StewardingViewDataBuilder.build(result.unwrap());
|
||||
@@ -28,4 +24,4 @@ export class LeagueStewardingPageQuery implements PageQuery<StewardingViewData,
|
||||
const query = new LeagueStewardingPageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,7 @@ import { Result } from '@/lib/contracts/Result';
|
||||
import { LeagueWalletService } from '@/lib/services/leagues/LeagueWalletService';
|
||||
import { LeagueWalletViewDataBuilder } from '@/lib/builders/view-data/LeagueWalletViewDataBuilder';
|
||||
import { LeagueWalletViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
|
||||
|
||||
interface PresentationError {
|
||||
type: 'notFound' | 'forbidden' | 'serverError';
|
||||
message: string;
|
||||
}
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
export class LeagueWalletPageQuery implements PageQuery<LeagueWalletViewData, string, PresentationError> {
|
||||
async execute(leagueId: string): Promise<Result<LeagueWalletViewData, PresentationError>> {
|
||||
@@ -15,7 +11,7 @@ export class LeagueWalletPageQuery implements PageQuery<LeagueWalletViewData, st
|
||||
const result = await service.getWalletData(leagueId);
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err({ type: 'serverError', message: 'Failed to load wallet data' });
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const viewData = LeagueWalletViewDataBuilder.build(result.unwrap());
|
||||
@@ -26,4 +22,4 @@ export class LeagueWalletPageQuery implements PageQuery<LeagueWalletViewData, st
|
||||
const query = new LeagueWalletPageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { SponsorService } from '@/lib/services/sponsors/SponsorService';
|
||||
import { SponsorDashboardViewDataBuilder } from '@/lib/builders/view-data/SponsorDashboardViewDataBuilder';
|
||||
import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardViewData';
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
|
||||
/**
|
||||
* Sponsor Dashboard Page Query
|
||||
@@ -10,13 +11,13 @@ import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardV
|
||||
* Composes data for the sponsor dashboard page.
|
||||
* Maps domain errors to presentation errors.
|
||||
*/
|
||||
export class SponsorDashboardPageQuery implements PageQuery<SponsorDashboardViewData, string> {
|
||||
async execute(sponsorId: string): Promise<Result<SponsorDashboardViewData, string>> {
|
||||
export class SponsorDashboardPageQuery implements PageQuery<SponsorDashboardViewData, string, PresentationError> {
|
||||
async execute(sponsorId: string): Promise<Result<SponsorDashboardViewData, PresentationError>> {
|
||||
const service = new SponsorService();
|
||||
|
||||
const dashboardResult = await service.getSponsorDashboard(sponsorId);
|
||||
if (dashboardResult.isErr()) {
|
||||
return Result.err(this.mapToPresentationError(dashboardResult.getError()));
|
||||
return Result.err(mapToPresentationError(dashboardResult.getError()));
|
||||
}
|
||||
|
||||
const dto = dashboardResult.unwrap();
|
||||
@@ -25,14 +26,8 @@ export class SponsorDashboardPageQuery implements PageQuery<SponsorDashboardView
|
||||
return Result.ok(viewData);
|
||||
}
|
||||
|
||||
private mapToPresentationError(domainError: { type: string }): string {
|
||||
switch (domainError.type) {
|
||||
case 'notFound':
|
||||
return 'Dashboard not found';
|
||||
case 'notImplemented':
|
||||
return 'Dashboard feature not yet implemented';
|
||||
default:
|
||||
return 'Failed to load dashboard';
|
||||
}
|
||||
static async execute(sponsorId: string): Promise<Result<SponsorDashboardViewData, PresentationError>> {
|
||||
const query = new SponsorDashboardPageQuery();
|
||||
return query.execute(sponsorId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
119
apps/website/lib/page-queries/TeamDetailPageQuery.ts
Normal file
119
apps/website/lib/page-queries/TeamDetailPageQuery.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { TeamService } from '@/lib/services/teams/TeamService';
|
||||
import { TeamDetailViewDataBuilder } from '@/lib/builders/view-data/TeamDetailViewDataBuilder';
|
||||
import type { TeamDetailViewData } from '@/lib/view-data/TeamDetailViewData';
|
||||
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
import { SessionGateway } from '@/lib/gateways/SessionGateway';
|
||||
import type { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
|
||||
|
||||
/**
|
||||
* TeamDetailPageDto - Raw serializable data for team detail page
|
||||
* Contains only raw data, no derived/computed properties
|
||||
*/
|
||||
export interface TeamDetailPageDto {
|
||||
team: {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description?: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
createdAt?: string;
|
||||
specialization?: string;
|
||||
region?: string;
|
||||
languages?: string[];
|
||||
category?: string;
|
||||
membership?: {
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
isActive: boolean;
|
||||
} | null;
|
||||
canManage: boolean;
|
||||
};
|
||||
memberships: Array<{
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
role: 'owner' | 'manager' | 'member';
|
||||
joinedAt: string;
|
||||
isActive: boolean;
|
||||
avatarUrl: string;
|
||||
}>;
|
||||
currentDriverId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TeamDetailPageQuery - Server-side composition for team detail page
|
||||
*/
|
||||
export class TeamDetailPageQuery implements PageQuery<TeamDetailViewData, string> {
|
||||
async execute(teamId: string): Promise<Result<TeamDetailViewData, PresentationError>> {
|
||||
// Get session to determine current driver
|
||||
const sessionGateway = new SessionGateway();
|
||||
const session = await sessionGateway.getSession();
|
||||
|
||||
if (!session?.user?.primaryDriverId) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
const currentDriverId = session.user.primaryDriverId;
|
||||
const service = new TeamService();
|
||||
|
||||
// Fetch team details
|
||||
const teamResult = await service.getTeamDetails(teamId, currentDriverId);
|
||||
|
||||
if (teamResult.isErr()) {
|
||||
return Result.err(mapToPresentationError(teamResult.getError()));
|
||||
}
|
||||
|
||||
const teamData = teamResult.unwrap();
|
||||
|
||||
// Fetch team members
|
||||
const membersResult = await service.getTeamMembers(teamId, currentDriverId, teamData.ownerId);
|
||||
|
||||
if (membersResult.isErr()) {
|
||||
return Result.err(mapToPresentationError(membersResult.getError()));
|
||||
}
|
||||
|
||||
const membersData = membersResult.unwrap();
|
||||
|
||||
// Transform to raw serializable DTO
|
||||
const dto: TeamDetailPageDto = {
|
||||
team: {
|
||||
id: teamData.id,
|
||||
name: teamData.name,
|
||||
tag: teamData.tag,
|
||||
description: teamData.description,
|
||||
ownerId: teamData.ownerId,
|
||||
leagues: teamData.leagues,
|
||||
createdAt: teamData.createdAt,
|
||||
specialization: teamData.specialization,
|
||||
region: teamData.region,
|
||||
languages: teamData.languages,
|
||||
category: teamData.category,
|
||||
membership: teamData.membership ? {
|
||||
role: teamData.membership.role,
|
||||
joinedAt: teamData.membership.joinedAt,
|
||||
isActive: teamData.membership.isActive,
|
||||
} : null,
|
||||
canManage: teamData.canManage,
|
||||
},
|
||||
memberships: membersData.map((member: TeamMemberViewModel) => ({
|
||||
driverId: member.driverId,
|
||||
driverName: member.driverName,
|
||||
role: member.role,
|
||||
joinedAt: member.joinedAt,
|
||||
isActive: member.isActive,
|
||||
avatarUrl: member.avatarUrl,
|
||||
})),
|
||||
currentDriverId,
|
||||
};
|
||||
|
||||
const viewData = TeamDetailViewDataBuilder.build(dto);
|
||||
return Result.ok(viewData);
|
||||
}
|
||||
|
||||
static async execute(teamId: string): Promise<Result<TeamDetailViewData, PresentationError>> {
|
||||
const query = new TeamDetailPageQuery();
|
||||
return query.execute(teamId);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,11 @@ import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-
|
||||
import { TeamService } from '@/lib/services/teams/TeamService';
|
||||
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
|
||||
import { TeamsViewDataBuilder } from '@/lib/builders/view-data/TeamsViewDataBuilder';
|
||||
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
|
||||
|
||||
export interface TeamsPageDto {
|
||||
teams: TeamListItemDTO[];
|
||||
}
|
||||
|
||||
/**
|
||||
* TeamsPageQuery - Server-side composition for teams list page
|
||||
@@ -1,10 +1,3 @@
|
||||
/**
|
||||
* Forgot Password Page Query
|
||||
*
|
||||
* Composes data for the forgot password page using RSC pattern.
|
||||
* No business logic, only data composition.
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { ForgotPasswordViewDataBuilder } from '@/lib/builders/view-data/ForgotPasswordViewDataBuilder';
|
||||
@@ -28,14 +21,14 @@ export class ForgotPasswordPageQuery implements PageQuery<ForgotPasswordViewData
|
||||
const serviceResult = await authService.processForgotPasswordParams({ returnTo, token });
|
||||
|
||||
if (serviceResult.isErr()) {
|
||||
return Result.err(serviceResult.getError());
|
||||
return Result.err(serviceResult.getError().message);
|
||||
}
|
||||
|
||||
// Transform to ViewData using builder
|
||||
const viewData = ForgotPasswordViewDataBuilder.build(serviceResult.unwrap());
|
||||
return Result.ok(viewData);
|
||||
} catch (error) {
|
||||
return Result.err('Failed to execute forgot password page query');
|
||||
} catch (error: any) {
|
||||
return Result.err(error.message || 'Failed to execute forgot password page query');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +37,4 @@ export class ForgotPasswordPageQuery implements PageQuery<ForgotPasswordViewData
|
||||
const query = new ForgotPasswordPageQuery();
|
||||
return query.execute(searchParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
/**
|
||||
* Login Page Query
|
||||
*
|
||||
* Composes data for the login page using RSC pattern.
|
||||
* No business logic, only data composition.
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { LoginViewDataBuilder } from '@/lib/builders/view-data/LoginViewDataBuilder';
|
||||
@@ -28,14 +21,14 @@ export class LoginPageQuery implements PageQuery<LoginViewData, URLSearchParams>
|
||||
const serviceResult = await authService.processLoginParams({ returnTo, token });
|
||||
|
||||
if (serviceResult.isErr()) {
|
||||
return Result.err(serviceResult.getError());
|
||||
return Result.err(serviceResult.getError().message);
|
||||
}
|
||||
|
||||
// Transform to ViewData using builder
|
||||
const viewData = LoginViewDataBuilder.build(serviceResult.unwrap());
|
||||
return Result.ok(viewData);
|
||||
} catch (error) {
|
||||
return Result.err('Failed to execute login page query');
|
||||
} catch (error: any) {
|
||||
return Result.err(error.message || 'Failed to execute login page query');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +37,4 @@ export class LoginPageQuery implements PageQuery<LoginViewData, URLSearchParams>
|
||||
const query = new LoginPageQuery();
|
||||
return query.execute(searchParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
/**
|
||||
* Reset Password Page Query
|
||||
*
|
||||
* Composes data for the reset password page using RSC pattern.
|
||||
* No business logic, only data composition.
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { ResetPasswordViewDataBuilder } from '@/lib/builders/view-data/ResetPasswordViewDataBuilder';
|
||||
@@ -28,14 +21,14 @@ export class ResetPasswordPageQuery implements PageQuery<ResetPasswordViewData,
|
||||
const serviceResult = await authService.processResetPasswordParams({ returnTo, token });
|
||||
|
||||
if (serviceResult.isErr()) {
|
||||
return Result.err(serviceResult.getError());
|
||||
return Result.err(serviceResult.getError().message);
|
||||
}
|
||||
|
||||
// Transform to ViewData using builder
|
||||
const viewData = ResetPasswordViewDataBuilder.build(serviceResult.unwrap());
|
||||
return Result.ok(viewData);
|
||||
} catch (error) {
|
||||
return Result.err('Failed to execute reset password page query');
|
||||
} catch (error: any) {
|
||||
return Result.err(error.message || 'Failed to execute reset password page query');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +37,4 @@ export class ResetPasswordPageQuery implements PageQuery<ResetPasswordViewData,
|
||||
const query = new ResetPasswordPageQuery();
|
||||
return query.execute(searchParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
/**
|
||||
* Signup Page Query
|
||||
*
|
||||
* Composes data for the signup page using RSC pattern.
|
||||
* No business logic, only data composition.
|
||||
*/
|
||||
|
||||
import { SignupViewDataBuilder } from '@/lib/builders/view-data/SignupViewDataBuilder';
|
||||
import { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
@@ -28,14 +21,14 @@ export class SignupPageQuery implements PageQuery<SignupViewData, URLSearchParam
|
||||
const serviceResult = await authService.processSignupParams({ returnTo, token });
|
||||
|
||||
if (serviceResult.isErr()) {
|
||||
return Result.err(serviceResult.getError());
|
||||
return Result.err(serviceResult.getError().message);
|
||||
}
|
||||
|
||||
// Transform to ViewData using builder
|
||||
const viewData = SignupViewDataBuilder.build(serviceResult.unwrap());
|
||||
return Result.ok(viewData);
|
||||
} catch (error) {
|
||||
return Result.err('Failed to execute signup page query');
|
||||
} catch (error: any) {
|
||||
return Result.err(error.message || 'Failed to execute signup page query');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +37,4 @@ export class SignupPageQuery implements PageQuery<SignupViewData, URLSearchParam
|
||||
const query = new SignupPageQuery();
|
||||
return query.execute(searchParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
|
||||
|
||||
/**
|
||||
* DriverRankingsPageDto - Raw data structure for Driver Rankings page
|
||||
* Plain data, no methods, no business logic
|
||||
*/
|
||||
|
||||
export interface DriverRankingsPageDto {
|
||||
drivers: DriverLeaderboardItemDTO[];
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
|
||||
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
|
||||
|
||||
export interface LeaderboardsPageDto {
|
||||
drivers: { drivers: DriverLeaderboardItemDTO[] };
|
||||
teams: { teams: TeamListItemDTO[] };
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
|
||||
/**
|
||||
* LeagueDetail page query
|
||||
* Returns the raw API DTO for the league detail page
|
||||
* No DI container usage - constructs dependencies explicitly
|
||||
*/
|
||||
export class LeagueDetailPageQuery implements PageQuery<any, string> {
|
||||
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
// Manual wiring: create API client
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
// Fetch data using API client
|
||||
try {
|
||||
const apiDto = await apiClient.getAllWithCapacityAndScoring();
|
||||
|
||||
if (!apiDto || !apiDto.leagues) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
// Find the specific league
|
||||
const league = apiDto.leagues.find(l => l.id === leagueId);
|
||||
if (!league) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
// Return the raw DTO - the page will handle ViewModel conversion
|
||||
return Result.ok({
|
||||
league,
|
||||
apiDto,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('LeagueDetailPageQuery failed:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('403') || error.message.includes('401')) {
|
||||
return Result.err('redirect');
|
||||
}
|
||||
if (error.message.includes('404')) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
if (error.message.includes('5') || error.message.includes('server')) {
|
||||
return Result.err('LEAGUE_FETCH_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
return Result.err('UNKNOWN_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
// Static method to avoid object construction in server code
|
||||
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'LEAGUE_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
const query = new LeagueDetailPageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
|
||||
/**
|
||||
* LeagueRosterAdminPageQuery
|
||||
*
|
||||
* Fetches league roster admin data (members and join requests).
|
||||
*/
|
||||
export class LeagueRosterAdminPageQuery implements PageQuery<any, string> {
|
||||
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'ROSTER_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
// Manual wiring: create API client
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
try {
|
||||
// Get admin roster members and join requests
|
||||
const [members, joinRequests] = await Promise.all([
|
||||
apiClient.getAdminRosterMembers(leagueId),
|
||||
apiClient.getAdminRosterJoinRequests(leagueId),
|
||||
]);
|
||||
|
||||
if (!members || !joinRequests) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
leagueId,
|
||||
members,
|
||||
joinRequests,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('LeagueRosterAdminPageQuery failed:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('403') || error.message.includes('401')) {
|
||||
return Result.err('redirect');
|
||||
}
|
||||
if (error.message.includes('404')) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
if (error.message.includes('5') || error.message.includes('server')) {
|
||||
return Result.err('ROSTER_FETCH_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
return Result.err('UNKNOWN_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'ROSTER_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
const query = new LeagueRosterAdminPageQuery();
|
||||
return query.execute(leagueId);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
|
||||
/**
|
||||
* LeagueScheduleAdminPageQuery
|
||||
*
|
||||
* Fetches league schedule admin data.
|
||||
*/
|
||||
export class LeagueScheduleAdminPageQuery implements PageQuery<any, { leagueId: string; seasonId?: string }> {
|
||||
async execute(input: { leagueId: string; seasonId?: string }): Promise<Result<any, 'notFound' | 'redirect' | 'SCHEDULE_ADMIN_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
// Manual wiring: create API client
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
try {
|
||||
// Get seasons
|
||||
const seasons = await apiClient.getSeasons(input.leagueId);
|
||||
|
||||
if (!seasons || seasons.length === 0) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
// Determine season to use
|
||||
const seasonId = input.seasonId || (seasons.find(s => s.status === 'active')?.seasonId || seasons[0].seasonId);
|
||||
|
||||
// Get schedule
|
||||
const schedule = await apiClient.getSchedule(input.leagueId, seasonId);
|
||||
|
||||
return Result.ok({
|
||||
leagueId: input.leagueId,
|
||||
seasonId,
|
||||
seasons,
|
||||
schedule,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('LeagueScheduleAdminPageQuery failed:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('403') || error.message.includes('401')) {
|
||||
return Result.err('redirect');
|
||||
}
|
||||
if (error.message.includes('404')) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
if (error.message.includes('5') || error.message.includes('server')) {
|
||||
return Result.err('SCHEDULE_ADMIN_FETCH_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
return Result.err('UNKNOWN_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
static async execute(input: { leagueId: string; seasonId?: string }): Promise<Result<any, 'notFound' | 'redirect' | 'SCHEDULE_ADMIN_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
|
||||
const query = new LeagueScheduleAdminPageQuery();
|
||||
return query.execute(input);
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
|
||||
import { SessionGateway } from '@/lib/gateways/SessionGateway';
|
||||
import { TeamService } from '@/lib/services/teams/TeamService';
|
||||
import type { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
|
||||
|
||||
/**
|
||||
* TeamDetailPageDto - Raw serializable data for team detail page
|
||||
* Contains only raw data, no derived/computed properties
|
||||
*/
|
||||
export interface TeamDetailPageDto {
|
||||
team: {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description?: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
createdAt?: string;
|
||||
specialization?: string;
|
||||
region?: string;
|
||||
languages?: string[];
|
||||
category?: string;
|
||||
membership?: {
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
isActive: boolean;
|
||||
} | null;
|
||||
canManage: boolean;
|
||||
};
|
||||
memberships: Array<{
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
role: 'owner' | 'manager' | 'member';
|
||||
joinedAt: string;
|
||||
isActive: boolean;
|
||||
avatarUrl: string;
|
||||
}>;
|
||||
currentDriverId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TeamDetailPageQuery - Server-side composition for team detail page
|
||||
* Manual wiring only; no ContainerManager; no PageDataFetcher
|
||||
* Returns raw serializable DTO
|
||||
*/
|
||||
export class TeamDetailPageQuery {
|
||||
static async execute(teamId: string): Promise<PageQueryResult<TeamDetailPageDto>> {
|
||||
try {
|
||||
// Validate teamId
|
||||
if (!teamId) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
// Get session to determine current driver
|
||||
const sessionGateway = new SessionGateway();
|
||||
const session = await sessionGateway.getSession();
|
||||
|
||||
if (!session?.user?.primaryDriverId) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
const currentDriverId = session.user.primaryDriverId;
|
||||
|
||||
// Manual dependency creation
|
||||
const service = new TeamService();
|
||||
|
||||
// Fetch team details
|
||||
const teamResult = await service.getTeamDetails(teamId, currentDriverId);
|
||||
|
||||
if (teamResult.isErr()) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
const teamData = teamResult.unwrap();
|
||||
|
||||
// Fetch team members
|
||||
const membersResult = await service.getTeamMembers(teamId, currentDriverId, teamData.ownerId);
|
||||
|
||||
if (membersResult.isErr()) {
|
||||
return { status: 'error', errorId: 'TEAM_MEMBERS_FETCH_FAILED' };
|
||||
}
|
||||
|
||||
const membersData = membersResult.unwrap();
|
||||
|
||||
// Transform to raw serializable DTO
|
||||
const dto: TeamDetailPageDto = {
|
||||
team: {
|
||||
id: teamData.id,
|
||||
name: teamData.name,
|
||||
tag: teamData.tag,
|
||||
description: teamData.description,
|
||||
ownerId: teamData.ownerId,
|
||||
leagues: teamData.leagues,
|
||||
createdAt: teamData.createdAt,
|
||||
specialization: teamData.specialization,
|
||||
region: teamData.region,
|
||||
languages: teamData.languages,
|
||||
category: teamData.category,
|
||||
membership: teamData.membership ? {
|
||||
role: teamData.membership.role,
|
||||
joinedAt: teamData.membership.joinedAt,
|
||||
isActive: teamData.membership.isActive,
|
||||
} : null,
|
||||
canManage: teamData.canManage,
|
||||
},
|
||||
memberships: membersData.map((member: TeamMemberViewModel) => ({
|
||||
driverId: member.driverId,
|
||||
driverName: member.driverName,
|
||||
role: member.role,
|
||||
joinedAt: member.joinedAt,
|
||||
isActive: member.isActive,
|
||||
avatarUrl: member.avatarUrl,
|
||||
})),
|
||||
currentDriverId,
|
||||
};
|
||||
|
||||
return { status: 'ok', dto };
|
||||
} catch (error) {
|
||||
// Handle specific error types
|
||||
if (error instanceof Error) {
|
||||
const errorAny = error as { statusCode?: number; message?: string };
|
||||
if (errorAny.message?.includes('not found') || errorAny.statusCode === 404) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
if (errorAny.message?.includes('redirect') || errorAny.statusCode === 302) {
|
||||
return { status: 'redirect', to: '/' };
|
||||
}
|
||||
return { status: 'error', errorId: 'TEAM_DETAIL_FETCH_FAILED' };
|
||||
}
|
||||
return { status: 'error', errorId: 'UNKNOWN_ERROR' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useQuery, useQueries, UseQueryOptions, useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useQuery, useQueries, useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
export interface PageDataConfig<TData, TError = ApiError> {
|
||||
@@ -25,6 +25,7 @@ export interface PageDataConfig<TData, TError = ApiError> {
|
||||
export function usePageData<TData, TError = ApiError>(
|
||||
config: PageDataConfig<TData, TError>
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const queryOptions: any = {
|
||||
queryKey: config.queryKey,
|
||||
queryFn: config.queryFn,
|
||||
@@ -55,13 +56,14 @@ export function usePageData<TData, TError = ApiError>(
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function usePageDataMultiple<T extends Record<string, any>>(
|
||||
export function usePageDataMultiple<T extends Record<string, unknown>>(
|
||||
queries: {
|
||||
[K in keyof T]: PageDataConfig<T[K]>;
|
||||
}
|
||||
) {
|
||||
const queryResults = useQueries({
|
||||
queries: Object.entries(queries).map(([key, config]) => {
|
||||
queries: Object.entries(queries).map(([_key, config]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const queryOptions: any = {
|
||||
queryKey: config.queryKey,
|
||||
queryFn: config.queryFn,
|
||||
@@ -132,7 +134,7 @@ export function usePageMutation<TData, TVariables, TError = ApiError>(
|
||||
*/
|
||||
export function useHydrateSSRData<TData>(
|
||||
ssrData: TData | null,
|
||||
queryKey: string[]
|
||||
_queryKey: string[]
|
||||
): { data: TData | null; isHydrated: boolean } {
|
||||
const [isHydrated, setIsHydrated] = React.useState(false);
|
||||
|
||||
|
||||
@@ -357,7 +357,7 @@ export const routeMatchers = {
|
||||
export function buildPath(
|
||||
routeName: string,
|
||||
params: Record<string, string> = {},
|
||||
_locale?: string
|
||||
_: string = ''
|
||||
): string {
|
||||
// This is a placeholder for future i18n implementation
|
||||
// For now, it just builds the path using the route config
|
||||
|
||||
@@ -151,6 +151,14 @@ export class SearchParamBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Wizard params
|
||||
step(value: string | null): this {
|
||||
if (value !== null) {
|
||||
this.params.set('step', value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// Generic setter
|
||||
set(key: string, value: string | null): this {
|
||||
if (value !== null) {
|
||||
|
||||
@@ -37,6 +37,10 @@ export interface ParsedFilterParams {
|
||||
tier?: string | null;
|
||||
}
|
||||
|
||||
export interface ParsedWizardParams {
|
||||
step?: string | null;
|
||||
}
|
||||
|
||||
export class SearchParamParser {
|
||||
// Parse auth parameters
|
||||
static parseAuth(params: URLSearchParams): Result<ParsedAuthParams, string> {
|
||||
@@ -172,6 +176,13 @@ export class SearchParamParser {
|
||||
});
|
||||
}
|
||||
|
||||
// Parse wizard parameters
|
||||
static parseWizard(params: URLSearchParams): Result<ParsedWizardParams, string> {
|
||||
return Result.ok({
|
||||
step: params.get('step'),
|
||||
});
|
||||
}
|
||||
|
||||
// Parse all parameters at once
|
||||
static parseAll(params: URLSearchParams): Result<
|
||||
{
|
||||
|
||||
@@ -84,7 +84,7 @@ export class SearchParamValidators {
|
||||
return { isValid: true, errors: [] };
|
||||
}
|
||||
|
||||
static validateOptional(value: string | null): ValidationResult {
|
||||
static validateOptional(_value: string | null): ValidationResult {
|
||||
return { isValid: true, errors: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { LoginPageDTO } from './types/LoginPageDTO';
|
||||
import { ForgotPasswordPageDTO } from './types/ForgotPasswordPageDTO';
|
||||
import { ResetPasswordPageDTO } from './types/ResetPasswordPageDTO';
|
||||
import { SignupPageDTO } from './types/SignupPageDTO';
|
||||
import { AuthPageParams } from './AuthPageParams';
|
||||
|
||||
export class AuthPageService {
|
||||
async processLoginParams(params: AuthPageParams): Promise<Result<LoginPageDTO, string>> {
|
||||
export class AuthPageService implements Service {
|
||||
async processLoginParams(params: AuthPageParams): Promise<Result<LoginPageDTO, DomainError>> {
|
||||
try {
|
||||
const returnTo = params.returnTo ?? '/dashboard';
|
||||
const hasInsufficientPermissions = params.returnTo !== null;
|
||||
@@ -23,38 +24,38 @@ export class AuthPageService {
|
||||
hasInsufficientPermissions,
|
||||
});
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process login parameters');
|
||||
return Result.err({ type: 'unknown', message: 'Failed to process login parameters' });
|
||||
}
|
||||
}
|
||||
|
||||
async processForgotPasswordParams(params: AuthPageParams): Promise<Result<ForgotPasswordPageDTO, string>> {
|
||||
async processForgotPasswordParams(params: AuthPageParams): Promise<Result<ForgotPasswordPageDTO, DomainError>> {
|
||||
try {
|
||||
const returnTo = params.returnTo ?? '/auth/login';
|
||||
return Result.ok({ returnTo });
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process forgot password parameters');
|
||||
return Result.err({ type: 'unknown', message: 'Failed to process forgot password parameters' });
|
||||
}
|
||||
}
|
||||
|
||||
async processResetPasswordParams(params: AuthPageParams): Promise<Result<ResetPasswordPageDTO, string>> {
|
||||
async processResetPasswordParams(params: AuthPageParams): Promise<Result<ResetPasswordPageDTO, DomainError>> {
|
||||
try {
|
||||
const token = params.token;
|
||||
if (!token) {
|
||||
return Result.err('Missing reset token');
|
||||
return Result.err({ type: 'validation', message: 'Missing reset token' });
|
||||
}
|
||||
const returnTo = params.returnTo ?? '/auth/login';
|
||||
return Result.ok({ token, returnTo });
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process reset password parameters');
|
||||
return Result.err({ type: 'unknown', message: 'Failed to process reset password parameters' });
|
||||
}
|
||||
}
|
||||
|
||||
async processSignupParams(params: AuthPageParams): Promise<Result<SignupPageDTO, string>> {
|
||||
async processSignupParams(params: AuthPageParams): Promise<Result<SignupPageDTO, DomainError>> {
|
||||
try {
|
||||
const returnTo = params.returnTo ?? '/onboarding';
|
||||
return Result.ok({ returnTo });
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process signup parameters');
|
||||
return Result.err({ type: 'unknown', message: 'Failed to process signup parameters' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,87 @@
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import type { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
|
||||
import type { LoginParamsDTO } from '@/lib/types/generated/LoginParamsDTO';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Auth Service
|
||||
*
|
||||
* Orchestrates authentication operations.
|
||||
* Calls AuthApiClient for API calls.
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
*/
|
||||
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
import { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
|
||||
import { LoginParamsDTO } from '@/lib/types/generated/LoginParamsDTO';
|
||||
import { SignupParamsDTO } from '@/lib/types/generated/SignupParamsDTO';
|
||||
import { ForgotPasswordDTO } from '@/lib/types/generated/ForgotPasswordDTO';
|
||||
import { ResetPasswordDTO } from '@/lib/types/generated/ResetPasswordDTO';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
|
||||
export class AuthService {
|
||||
export class AuthService implements Service {
|
||||
private apiClient: AuthApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: false,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
this.apiClient = new AuthApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async login(params: LoginParamsDTO): Promise<SessionViewModel> {
|
||||
const dto = await this.apiClient.login(params);
|
||||
return new SessionViewModel(dto.user);
|
||||
async login(params: LoginParamsDTO): Promise<Result<AuthSessionDTO, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.login(params);
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'unauthorized', message: (error as Error).message || 'Login failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async signup(params: SignupParamsDTO): Promise<SessionViewModel> {
|
||||
const dto = await this.apiClient.signup(params);
|
||||
return new SessionViewModel(dto.user);
|
||||
async signup(params: SignupParamsDTO): Promise<Result<AuthSessionDTO, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.signup(params);
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Signup failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.apiClient.logout();
|
||||
async logout(): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
await this.apiClient.logout();
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Logout failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async forgotPassword(params: ForgotPasswordDTO): Promise<{ message: string; magicLink?: string }> {
|
||||
return await this.apiClient.forgotPassword(params);
|
||||
async forgotPassword(params: ForgotPasswordDTO): Promise<Result<{ message: string; magicLink?: string }, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.forgotPassword(params);
|
||||
return Result.ok(result);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Forgot password request failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async resetPassword(params: ResetPasswordDTO): Promise<{ message: string }> {
|
||||
return await this.apiClient.resetPassword(params);
|
||||
async resetPassword(params: ResetPasswordDTO): Promise<Result<{ message: string }, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.resetPassword(params);
|
||||
return Result.ok(result);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Reset password failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async getSession(): Promise<Result<AuthSessionDTO | null, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.getSession();
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch session' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
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
|
||||
*
|
||||
* Returns SessionViewModel for client consumption.
|
||||
* Orchestrates session-related operations.
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
*/
|
||||
export class SessionService {
|
||||
constructor(
|
||||
private readonly apiClient: AuthApiClient
|
||||
) {}
|
||||
export class SessionService implements Service {
|
||||
private authService: AuthService;
|
||||
|
||||
constructor() {
|
||||
this.authService = new AuthService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user session (returns ViewModel)
|
||||
* Get current user session
|
||||
*/
|
||||
async getSession(): Promise<SessionViewModel | null> {
|
||||
const dto = await this.apiClient.getSession();
|
||||
if (!dto) return null;
|
||||
return new SessionViewModel(dto.user);
|
||||
async getSession(): Promise<Result<AuthSessionDTO | null, DomainError>> {
|
||||
return this.authService.getSession();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDT
|
||||
import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Driver Service - DTO Only
|
||||
@@ -11,58 +16,97 @@ import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverP
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class DriverService {
|
||||
constructor(
|
||||
private readonly apiClient: DriversApiClient
|
||||
) {}
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver leaderboard (returns DTO)
|
||||
*/
|
||||
async getDriverLeaderboard() {
|
||||
return this.apiClient.getLeaderboard();
|
||||
async getDriverLeaderboard(): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getLeaderboard();
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get leaderboard' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete driver onboarding (returns DTO)
|
||||
*/
|
||||
async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingOutputDTO> {
|
||||
return this.apiClient.completeOnboarding(input);
|
||||
async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise<Result<CompleteOnboardingOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.completeOnboarding(input);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to complete onboarding' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current driver (returns DTO)
|
||||
*/
|
||||
async getCurrentDriver(): Promise<DriverDTO | null> {
|
||||
return this.apiClient.getCurrent();
|
||||
async getCurrentDriver(): Promise<Result<DriverDTO | null, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getCurrent();
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get current driver' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver profile (returns DTO)
|
||||
*/
|
||||
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
|
||||
return this.apiClient.getDriverProfile(driverId);
|
||||
async getDriverProfile(driverId: string): Promise<Result<GetDriverProfileOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getDriverProfile(driverId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get driver profile' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current driver profile (returns DTO)
|
||||
*/
|
||||
async updateProfile(updates: { bio?: string; country?: string }): Promise<DriverDTO> {
|
||||
return this.apiClient.updateProfile(updates);
|
||||
async updateProfile(updates: { bio?: string; country?: string }): Promise<Result<DriverDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.updateProfile(updates);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update profile' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find driver by ID (returns DTO)
|
||||
*/
|
||||
async findById(id: string): Promise<GetDriverOutputDTO | null> {
|
||||
return this.apiClient.getDriver(id);
|
||||
async findById(id: string): Promise<Result<GetDriverOutputDTO | null, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getDriver(id);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to find driver' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find multiple drivers by IDs (returns DTOs)
|
||||
*/
|
||||
async findByIds(ids: string[]): Promise<GetDriverOutputDTO[]> {
|
||||
const drivers = await Promise.all(ids.map(id => this.apiClient.getDriver(id)));
|
||||
return drivers.filter((d): d is GetDriverOutputDTO => d !== null);
|
||||
async findByIds(ids: string[]): Promise<Result<GetDriverOutputDTO[], DomainError>> {
|
||||
try {
|
||||
const drivers = await Promise.all(ids.map(id => this.apiClient.getDriver(id)));
|
||||
return Result.ok(drivers.filter((d): d is GetDriverOutputDTO => d !== null));
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to find drivers' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { Service } from '@/lib/contracts/services/Service';
|
||||
import type { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import type { GetLiveriesOutputDTO } from '@/lib/types/tbd/GetLiveriesOutputDTO';
|
||||
|
||||
/**
|
||||
@@ -8,7 +8,7 @@ import type { GetLiveriesOutputDTO } from '@/lib/types/tbd/GetLiveriesOutputDTO'
|
||||
* Provides livery management functionality.
|
||||
*/
|
||||
export class LiveryService implements Service {
|
||||
async getLiveries(driverId: string): Promise<Result<GetLiveriesOutputDTO, string>> {
|
||||
async getLiveries(_: string): Promise<Result<GetLiveriesOutputDTO, DomainError>> {
|
||||
// Mock data for now
|
||||
const mockLiveries: GetLiveriesOutputDTO = {
|
||||
liveries: [
|
||||
@@ -31,7 +31,7 @@ export class LiveryService implements Service {
|
||||
return Result.ok(mockLiveries);
|
||||
}
|
||||
|
||||
async uploadLivery(driverId: string, file: File): Promise<Result<{ liveryId: string }, string>> {
|
||||
async uploadLivery(__: string, ___: File): Promise<Result<{ liveryId: string }, DomainError>> {
|
||||
// Mock implementation
|
||||
return Result.ok({ liveryId: 'new-livery-id' });
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user