website refactor

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

View File

@@ -3,8 +3,6 @@
*/
import { EventEmitter } from 'events';
import { ApiError } from './ApiError';
import { CircuitBreakerRegistry } from './RetryHandler';
export type ConnectionStatus = 'connected' | 'disconnected' | 'degraded' | 'checking';

View File

@@ -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;

View File

@@ -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(),
}
);
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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 };
/**

View File

@@ -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[] };

View File

@@ -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);
}

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -1,8 +0,0 @@
/**
* @file index.ts
* Blockers exports
*/
export { Blocker } from './Blocker';
export { SubmitBlocker } from './SubmitBlocker';
export { ThrottleBlocker } from './ThrottleBlocker';

View File

@@ -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,
}));

View File

@@ -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;
}

View File

@@ -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,
};
}
}

View File

@@ -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;
}

View File

@@ -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 = {

View File

@@ -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,
};
}
}
}

View File

@@ -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,
};
}
}
}

View File

@@ -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,
};
}
}

View File

@@ -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,
};
}
}

View File

@@ -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

View File

@@ -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';

View File

@@ -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 };
}
}
}

View File

@@ -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,
})),

View File

@@ -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';
/**

View File

@@ -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>;
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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

View File

@@ -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)

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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

View File

@@ -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
});

View File

@@ -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)}`;

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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';
};

View File

@@ -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' });
}
}
}
}

View File

@@ -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' });
}
}
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);

View File

@@ -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

View File

@@ -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) {

View File

@@ -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<
{

View File

@@ -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: [] };
}
}

View File

@@ -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' });
}
}
}
}

View File

@@ -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' });
}
}
}

View File

@@ -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();
}
}

View File

@@ -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' });
}
}
}

View File

@@ -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