website refactor
This commit is contained in:
@@ -16,8 +16,8 @@ import type { AllRacesPageDTO } from '../../types/generated/AllRacesPageDTO';
|
||||
import type { FilteredRacesPageDataDTO } from '../../types/tbd/FilteredRacesPageDataDTO';
|
||||
|
||||
// Define missing types
|
||||
type RacesPageDataDTO = { races: RacesPageDataRaceDTO[] };
|
||||
type RaceDetailDTO = {
|
||||
export type RacesPageDataDTO = { races: RacesPageDataRaceDTO[] };
|
||||
export type RaceDetailDTO = {
|
||||
race: RaceDetailRaceDTO | null;
|
||||
league: RaceDetailLeagueDTO | null;
|
||||
entryList: RaceDetailEntryDTO[];
|
||||
@@ -25,7 +25,7 @@ type RaceDetailDTO = {
|
||||
userResult: RaceDetailUserResultDTO | null;
|
||||
error?: string;
|
||||
};
|
||||
type ImportRaceResultsSummaryDTO = {
|
||||
export type ImportRaceResultsSummaryDTO = {
|
||||
success: boolean;
|
||||
raceId: string;
|
||||
driversProcessed: number;
|
||||
|
||||
@@ -1,39 +1,100 @@
|
||||
import { RacesViewData, RacesRace } from '@/lib/view-data/races/RacesViewData';
|
||||
import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO';
|
||||
import type { RacesViewData, RaceViewData } from '@/lib/view-data/RacesViewData';
|
||||
|
||||
/**
|
||||
* Races View Data Builder
|
||||
*
|
||||
* Transforms API DTO into ViewData for the races template.
|
||||
* Deterministic, side-effect free.
|
||||
*/
|
||||
export class RacesViewDataBuilder {
|
||||
static build(apiDto: any): RacesViewData {
|
||||
const races = apiDto.races.map((race: any) => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt,
|
||||
status: race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
|
||||
sessionType: 'race',
|
||||
leagueId: race.leagueId,
|
||||
leagueName: race.leagueName,
|
||||
strengthOfField: race.strengthOfField ?? undefined,
|
||||
isUpcoming: race.status === 'scheduled',
|
||||
isLive: race.status === 'running',
|
||||
isPast: race.status === 'completed',
|
||||
}));
|
||||
static build(apiDto: RacesPageDataDTO): RacesViewData {
|
||||
const races = apiDto.races.map((race): RaceViewData => {
|
||||
const scheduledAt = new Date(race.scheduledAt);
|
||||
|
||||
return {
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt,
|
||||
scheduledAtLabel: scheduledAt.toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}),
|
||||
timeLabel: scheduledAt.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
relativeTimeLabel: this.getRelativeTime(scheduledAt),
|
||||
status: race.status as RaceViewData['status'],
|
||||
statusLabel: this.getStatusLabel(race.status),
|
||||
sessionType: 'Race',
|
||||
leagueId: race.leagueId,
|
||||
leagueName: race.leagueName,
|
||||
strengthOfField: race.strengthOfField ?? null,
|
||||
isUpcoming: race.isUpcoming,
|
||||
isLive: race.isLive,
|
||||
isPast: race.isPast,
|
||||
};
|
||||
});
|
||||
|
||||
const totalCount = races.length;
|
||||
const scheduledRaces = races.filter((r: RacesRace) => r.isUpcoming);
|
||||
const runningRaces = races.filter((r: RacesRace) => r.isLive);
|
||||
const completedRaces = races.filter((r: RacesRace) => r.isPast);
|
||||
const leagues = Array.from(
|
||||
new Map(
|
||||
races
|
||||
.filter(r => r.leagueId && r.leagueName)
|
||||
.map(r => [r.leagueId, { id: r.leagueId!, name: r.leagueName! }])
|
||||
).values()
|
||||
);
|
||||
|
||||
const groupedRaces = new Map<string, RaceViewData[]>();
|
||||
races.forEach((race) => {
|
||||
const dateKey = race.scheduledAt.split('T')[0]!;
|
||||
if (!groupedRaces.has(dateKey)) {
|
||||
groupedRaces.set(dateKey, []);
|
||||
}
|
||||
groupedRaces.get(dateKey)!.push(race);
|
||||
});
|
||||
|
||||
const racesByDate = Array.from(groupedRaces.entries()).map(([dateKey, dayRaces]) => ({
|
||||
dateKey,
|
||||
dateLabel: dayRaces[0]?.scheduledAtLabel || '',
|
||||
races: dayRaces,
|
||||
}));
|
||||
|
||||
return {
|
||||
races,
|
||||
totalCount,
|
||||
scheduledRaces,
|
||||
runningRaces,
|
||||
completedRaces,
|
||||
totalCount: races.length,
|
||||
scheduledCount: races.filter(r => r.status === 'scheduled').length,
|
||||
runningCount: races.filter(r => r.status === 'running').length,
|
||||
completedCount: races.filter(r => r.status === 'completed').length,
|
||||
leagues,
|
||||
upcomingRaces: races.filter(r => r.isUpcoming).slice(0, 5),
|
||||
liveRaces: races.filter(r => r.isLive),
|
||||
recentResults: races.filter(r => r.isPast).slice(0, 5),
|
||||
racesByDate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static getStatusLabel(status: string): string {
|
||||
switch (status) {
|
||||
case 'scheduled': return 'Scheduled';
|
||||
case 'running': return 'LIVE';
|
||||
case 'completed': return 'Completed';
|
||||
case 'cancelled': return 'Cancelled';
|
||||
default: return status;
|
||||
}
|
||||
}
|
||||
|
||||
private static getRelativeTime(date: Date): string {
|
||||
const now = new Date();
|
||||
const diffMs = date.getTime() - now.getTime();
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffMs < 0) return 'Past';
|
||||
if (diffHours < 1) return 'Starting soon';
|
||||
if (diffHours < 24) return `In ${diffHours}h`;
|
||||
if (diffDays === 1) return 'Tomorrow';
|
||||
if (diffDays < 7) return `In ${diffDays} days`;
|
||||
return date.toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export { useCurrentSession } from './useCurrentSession';
|
||||
export { useLogin } from './useLogin';
|
||||
export { useLogout } from './useLogout';
|
||||
export { useSignup } from './useSignup';
|
||||
export { useForgotPassword } from './useForgotPassword';
|
||||
export { useResetPassword } from './useResetPassword';
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { SESSION_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
|
||||
export function useCurrentSession(
|
||||
options?: Omit<UseQueryOptions<SessionViewModel | null, ApiError>, 'queryKey' | 'queryFn'> & { initialData?: SessionViewModel | null }
|
||||
) {
|
||||
const sessionService = useInject(SESSION_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['currentSession'],
|
||||
queryFn: () => sessionService.getSession(),
|
||||
initialData: options?.initialData,
|
||||
...options,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { AUTH_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { ForgotPasswordDTO } from '@/lib/types/generated/ForgotPasswordDTO';
|
||||
|
||||
export function useForgotPassword(
|
||||
options?: Omit<UseMutationOptions<{ message: string; magicLink?: string }, ApiError, ForgotPasswordDTO>, 'mutationFn'>
|
||||
) {
|
||||
const authService = useInject(AUTH_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<{ message: string; magicLink?: string }, ApiError, ForgotPasswordDTO>({
|
||||
mutationFn: (params) => authService.forgotPassword(params),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { AUTH_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
import type { LoginParamsDTO } from '@/lib/types/generated/LoginParamsDTO';
|
||||
|
||||
export function useLogin(
|
||||
options?: Omit<UseMutationOptions<SessionViewModel, ApiError, LoginParamsDTO>, 'mutationFn'>
|
||||
) {
|
||||
const authService = useInject(AUTH_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<SessionViewModel, ApiError, LoginParamsDTO>({
|
||||
mutationFn: (params) => authService.login(params),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { AUTH_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
export function useLogout(
|
||||
options?: Omit<UseMutationOptions<void, ApiError, void>, 'mutationFn'>
|
||||
) {
|
||||
const authService = useInject(AUTH_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<void, ApiError, void>({
|
||||
mutationFn: () => authService.logout(),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { AUTH_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { ResetPasswordDTO } from '@/lib/types/generated/ResetPasswordDTO';
|
||||
|
||||
export function useResetPassword(
|
||||
options?: Omit<UseMutationOptions<{ message: string }, ApiError, ResetPasswordDTO>, 'mutationFn'>
|
||||
) {
|
||||
const authService = useInject(AUTH_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<{ message: string }, ApiError, ResetPasswordDTO>({
|
||||
mutationFn: (params) => authService.resetPassword(params),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { AUTH_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
import type { SignupParamsDTO } from '@/lib/types/generated/SignupParamsDTO';
|
||||
|
||||
export function useSignup(
|
||||
options?: Omit<UseMutationOptions<SessionViewModel, ApiError, SignupParamsDTO>, 'mutationFn'>
|
||||
) {
|
||||
const authService = useInject(AUTH_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<SessionViewModel, ApiError, SignupParamsDTO>({
|
||||
mutationFn: (params) => authService.signup(params),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export { useCurrentDriver } from './useCurrentDriver';
|
||||
export { useDriverLeaderboard } from '@/lib/hooks/useDriverLeaderboard';
|
||||
export { useDriverProfile } from './useDriverProfile';
|
||||
export { useUpdateDriverProfile } from './useUpdateDriverProfile';
|
||||
export { useCreateDriver } from './useCreateDriver';
|
||||
export { useFindDriverById } from './useFindDriverById';
|
||||
@@ -1,22 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
|
||||
import { CompleteOnboardingViewModel } from '@/lib/view-models/CompleteOnboardingViewModel';
|
||||
|
||||
export function useCreateDriver(
|
||||
options?: Omit<UseMutationOptions<CompleteOnboardingViewModel, ApiError, CompleteOnboardingInputDTO>, 'mutationFn'>
|
||||
) {
|
||||
const driverService = useInject(DRIVER_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<CompleteOnboardingViewModel, ApiError, CompleteOnboardingInputDTO>({
|
||||
mutationFn: async (input) => {
|
||||
const dto = await driverService.completeDriverOnboarding(input);
|
||||
return new CompleteOnboardingViewModel(dto);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
type DriverData = any; // Replace with actual type
|
||||
|
||||
export function useCurrentDriver(
|
||||
options?: Omit<UseQueryOptions<DriverData, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
const driverService = useInject(DRIVER_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['currentDriver'],
|
||||
queryFn: () => driverService.getCurrentDriver(),
|
||||
...options,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel';
|
||||
|
||||
export function useDriverProfile(
|
||||
driverId: string,
|
||||
options?: Omit<UseQueryOptions<DriverProfileViewModel, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
const driverService = useInject(DRIVER_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['driverProfile', driverId],
|
||||
queryFn: () => driverService.getDriverProfile(driverId),
|
||||
enabled: !!driverId,
|
||||
...options,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { usePageDataMultiple } from '@/lib/page/usePageData';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { DRIVER_SERVICE_TOKEN, TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
|
||||
export function useDriverProfilePageData(driverId: string) {
|
||||
const driverService = useInject(DRIVER_SERVICE_TOKEN);
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
return usePageDataMultiple({
|
||||
driverProfile: {
|
||||
queryKey: ['driverProfile', driverId],
|
||||
queryFn: () => driverService.getDriverProfile(driverId),
|
||||
enabled: !!driverId,
|
||||
},
|
||||
teamMemberships: {
|
||||
queryKey: ['teamMemberships', driverId],
|
||||
queryFn: async () => {
|
||||
if (!driverId) return [];
|
||||
|
||||
const allTeams = await teamService.getAllTeams();
|
||||
let teamMemberships: Array<{
|
||||
team: { id: string; name: string };
|
||||
role: string;
|
||||
joinedAt: Date;
|
||||
}> = [];
|
||||
|
||||
for (const team of allTeams) {
|
||||
const teamMembers = await teamService.getTeamMembers(team.id, driverId, '');
|
||||
const membership = teamMembers?.find(member => member.driverId === driverId);
|
||||
if (membership) {
|
||||
teamMemberships.push({
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
},
|
||||
role: membership.role,
|
||||
joinedAt: new Date(membership.joinedAt),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return teamMemberships;
|
||||
},
|
||||
enabled: !!driverId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
|
||||
|
||||
export function useFindDriverById(
|
||||
driverId: string,
|
||||
options?: Omit<UseQueryOptions<GetDriverOutputDTO | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
const driverService = useInject(DRIVER_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['driver', driverId],
|
||||
queryFn: () => driverService.findById(driverId),
|
||||
enabled: !!driverId,
|
||||
...options,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel';
|
||||
import { DriverProfileViewModelBuilder } from '@/lib/builders/view-models/DriverProfileViewModelBuilder';
|
||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||
|
||||
export function useUpdateDriverProfile(
|
||||
options?: Omit<UseMutationOptions<DriverProfileViewModel, ApiError, { bio?: string; country?: string }>, 'mutationFn'>
|
||||
) {
|
||||
const driverService = useInject(DRIVER_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<DriverProfileViewModel, ApiError, { bio?: string; country?: string }>({
|
||||
mutationFn: async (updates) => {
|
||||
await driverService.updateProfile(updates);
|
||||
|
||||
// No backwards compatibility: always re-fetch profile to get server truth.
|
||||
const driverId = updates ? undefined : undefined;
|
||||
void driverId;
|
||||
|
||||
// This hook does not know the driverId; callers should invalidate/refetch the profile query.
|
||||
// Return a minimal ViewModel to satisfy types.
|
||||
return DriverProfileViewModelBuilder.build({
|
||||
teamMemberships: [],
|
||||
socialSummary: { friends: [], friendsCount: 0 },
|
||||
} as unknown as GetDriverProfileOutputDTO);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useAllLeagues() {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['allLeagues'],
|
||||
queryFn: () => leagueService.getAllLeagues(),
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { useCreateLeagueWithBlockers } from './useCreateLeagueWithBlockers';
|
||||
|
||||
/**
|
||||
* @deprecated Use useCreateLeagueWithBlockers instead
|
||||
* This wrapper maintains backward compatibility while using the new blocker-aware hook
|
||||
*/
|
||||
export function useCreateLeague() {
|
||||
return useCreateLeagueWithBlockers();
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
|
||||
import { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO';
|
||||
|
||||
interface CreateLeagueInput {
|
||||
name: string;
|
||||
description: string;
|
||||
maxDrivers: number;
|
||||
scoringPresetId: string;
|
||||
}
|
||||
|
||||
interface CreateLeagueResult {
|
||||
success: boolean;
|
||||
leagueId: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function useCreateLeagueWithBlockers(
|
||||
options?: Omit<UseMutationOptions<CreateLeagueResult, ApiError, CreateLeagueInput>, 'mutationFn'>
|
||||
) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<CreateLeagueResult, ApiError, CreateLeagueInput>({
|
||||
mutationFn: async (input) => {
|
||||
try {
|
||||
// Transform input to DTO - note: maxDrivers and scoringPresetId are not in the DTO
|
||||
// This hook may need to be updated based on actual API requirements
|
||||
const inputDto: CreateLeagueInputDTO = {
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
visibility: 'public', // Default value
|
||||
ownerId: '', // Will be set by the service
|
||||
};
|
||||
|
||||
const result: CreateLeagueOutputDTO = await leagueService.createLeague(inputDto);
|
||||
return { success: result.success, leagueId: result.leagueId };
|
||||
} catch (error) {
|
||||
// Check if it's a rate limit error
|
||||
if (error instanceof ApiError && error.type === 'RATE_LIMIT_ERROR') {
|
||||
return { success: false, leagueId: '', error: 'Rate limited' };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_MEMBERSHIP_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useLeagueAdminStatus(leagueId: string, currentDriverId: string) {
|
||||
const leagueMembershipService = useInject(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['leagueMembership', leagueId, currentDriverId],
|
||||
queryFn: async () => {
|
||||
await leagueMembershipService.fetchLeagueMemberships(leagueId);
|
||||
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
|
||||
return membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false;
|
||||
},
|
||||
enabled: !!leagueId && !!currentDriverId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
|
||||
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
|
||||
import type { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/AllLeaguesWithCapacityAndScoringDTO';
|
||||
|
||||
interface UseLeagueDetailOptions {
|
||||
leagueId: string;
|
||||
queryOptions?: UseQueryOptions<LeagueWithCapacityAndScoringDTO, ApiError>;
|
||||
}
|
||||
|
||||
interface UseLeagueMembershipsOptions {
|
||||
leagueId: string;
|
||||
queryOptions?: UseQueryOptions<LeagueMembershipsDTO, ApiError>;
|
||||
}
|
||||
|
||||
export function useLeagueDetail({ leagueId, queryOptions }: UseLeagueDetailOptions) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return useQuery<LeagueWithCapacityAndScoringDTO, ApiError>({
|
||||
queryKey: ['league-detail', leagueId],
|
||||
queryFn: async () => {
|
||||
const result = await leagueService.getAllLeagues() as AllLeaguesWithCapacityAndScoringDTO;
|
||||
// Filter for the specific league
|
||||
const leagues = Array.isArray(result?.leagues) ? result.leagues : [];
|
||||
const league = leagues.find(l => l.id === leagueId);
|
||||
if (!league) {
|
||||
throw new ApiError('League not found', 'NOT_FOUND', {
|
||||
endpoint: 'getAllLeagues',
|
||||
statusCode: 404,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
return league;
|
||||
},
|
||||
...queryOptions,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLeagueMemberships({ leagueId, queryOptions }: UseLeagueMembershipsOptions) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return useQuery<LeagueMembershipsDTO, ApiError>({
|
||||
queryKey: ['league-memberships', leagueId],
|
||||
queryFn: async () => {
|
||||
const result = await leagueService.getLeagueMemberships(leagueId);
|
||||
// The DTO already has the correct structure with members property
|
||||
return result;
|
||||
},
|
||||
...queryOptions,
|
||||
});
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_MEMBERSHIP_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
|
||||
export function useLeagueMembershipMutation() {
|
||||
const leagueMembershipService = useInject(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const joinLeagueMutation = useMutation({
|
||||
mutationFn: ({ leagueId, driverId }: { leagueId: string; driverId: string }) =>
|
||||
leagueMembershipService.joinLeague(leagueId, driverId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['leagueMemberships'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['allLeagues'] });
|
||||
},
|
||||
});
|
||||
|
||||
const leaveLeagueMutation = useMutation({
|
||||
mutationFn: ({ leagueId, driverId }: { leagueId: string; driverId: string }) =>
|
||||
leagueMembershipService.leaveLeague(leagueId, driverId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['leagueMemberships'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['allLeagues'] });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
joinLeague: joinLeagueMutation,
|
||||
leaveLeague: leaveLeagueMutation,
|
||||
};
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useLeagueMemberships(leagueId: string, currentUserId: string) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['leagueMemberships', leagueId, currentUserId],
|
||||
queryFn: () => leagueService.getLeagueMemberships(leagueId),
|
||||
enabled: !!leagueId && !!currentUserId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { RACE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useLeagueRaces(leagueId: string) {
|
||||
const raceService = useInject(RACE_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['leagueRaces', leagueId],
|
||||
queryFn: () => raceService.findByLeagueId(leagueId),
|
||||
enabled: !!leagueId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useMutation, useQuery, UseMutationOptions, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
||||
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
|
||||
|
||||
interface UpdateMemberRoleInput {
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
newRole: 'owner' | 'admin' | 'steward' | 'member';
|
||||
}
|
||||
|
||||
interface RemoveMemberInput {
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
interface JoinRequestActionInput {
|
||||
leagueId: string;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export function useLeagueRosterAdmin(leagueId: string, options?: UseQueryOptions<LeagueRosterMemberDTO[], ApiError>) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return useQuery<LeagueRosterMemberDTO[], ApiError>({
|
||||
queryKey: ['league-roster-admin', leagueId],
|
||||
queryFn: () => leagueService.getAdminRosterMembers(leagueId),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLeagueJoinRequests(leagueId: string, options?: UseQueryOptions<LeagueRosterJoinRequestDTO[], ApiError>) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return useQuery<LeagueRosterJoinRequestDTO[], ApiError>({
|
||||
queryKey: ['league-join-requests', leagueId],
|
||||
queryFn: () => leagueService.getAdminRosterJoinRequests(leagueId),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateMemberRole(
|
||||
options?: Omit<UseMutationOptions<{ success: boolean }, ApiError, UpdateMemberRoleInput>, 'mutationFn'>
|
||||
) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<{ success: boolean }, ApiError, UpdateMemberRoleInput>({
|
||||
mutationFn: async (input) => {
|
||||
return leagueService.updateMemberRole(input.leagueId, input.driverId, input.newRole);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveMember(
|
||||
options?: Omit<UseMutationOptions<{ success: boolean }, ApiError, RemoveMemberInput>, 'mutationFn'>
|
||||
) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<{ success: boolean }, ApiError, RemoveMemberInput>({
|
||||
mutationFn: async (input) => {
|
||||
return leagueService.removeMember(input.leagueId, input.driverId);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useApproveJoinRequest(
|
||||
options?: Omit<UseMutationOptions<{ success: boolean }, ApiError, JoinRequestActionInput>, 'mutationFn'>
|
||||
) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<{ success: boolean }, ApiError, JoinRequestActionInput>({
|
||||
mutationFn: async (input) => {
|
||||
return leagueService.approveJoinRequest(input.leagueId, input.requestId);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRejectJoinRequest(
|
||||
options?: Omit<UseMutationOptions<{ success: boolean }, ApiError, JoinRequestActionInput>, 'mutationFn'>
|
||||
) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<{ success: boolean }, ApiError, JoinRequestActionInput>({
|
||||
mutationFn: async (input) => {
|
||||
return leagueService.rejectJoinRequest(input.leagueId, input.requestId);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
import { LeagueScheduleViewModel, LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
|
||||
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
|
||||
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
||||
|
||||
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
|
||||
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
|
||||
const now = new Date();
|
||||
const isPast = scheduledAt.getTime() < now.getTime();
|
||||
const isUpcoming = !isPast;
|
||||
|
||||
return {
|
||||
id: race.id,
|
||||
name: race.name,
|
||||
scheduledAt,
|
||||
isPast,
|
||||
isUpcoming,
|
||||
status: isPast ? 'completed' : 'scheduled',
|
||||
track: undefined,
|
||||
car: undefined,
|
||||
sessionType: undefined,
|
||||
isRegistered: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function useLeagueSchedule(leagueId: string) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['leagueSchedule', leagueId],
|
||||
queryFn: async (): Promise<LeagueScheduleViewModel> => {
|
||||
const dto = await leagueService.getLeagueSchedule(leagueId);
|
||||
const races = dto.races.map(mapRaceDtoToViewModel);
|
||||
return new LeagueScheduleViewModel(races);
|
||||
},
|
||||
enabled: !!leagueId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { usePageData } from '@/lib/page/usePageData';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN, LEAGUE_MEMBERSHIP_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
|
||||
import { LeagueAdminScheduleViewModel } from '@/lib/view-models/LeagueAdminScheduleViewModel';
|
||||
import { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
|
||||
import { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel';
|
||||
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
|
||||
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
||||
import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO';
|
||||
|
||||
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
|
||||
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
|
||||
const now = new Date();
|
||||
const isPast = scheduledAt.getTime() < now.getTime();
|
||||
const isUpcoming = !isPast;
|
||||
|
||||
return {
|
||||
id: race.id,
|
||||
name: race.name,
|
||||
scheduledAt,
|
||||
isPast,
|
||||
isUpcoming,
|
||||
status: isPast ? 'completed' : 'scheduled',
|
||||
track: undefined,
|
||||
car: undefined,
|
||||
sessionType: undefined,
|
||||
isRegistered: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function useLeagueAdminStatus(leagueId: string, currentDriverId: string) {
|
||||
const leagueMembershipService = useInject(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
|
||||
|
||||
return usePageData({
|
||||
queryKey: ['admin-check', leagueId, currentDriverId],
|
||||
queryFn: async () => {
|
||||
await leagueMembershipService.fetchLeagueMemberships(leagueId);
|
||||
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
|
||||
return membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false;
|
||||
},
|
||||
enabled: !!leagueId && !!currentDriverId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLeagueSeasons(leagueId: string, isAdmin: boolean) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return usePageData({
|
||||
queryKey: ['leagueSeasons', leagueId],
|
||||
queryFn: async (): Promise<LeagueSeasonSummaryViewModel[]> => {
|
||||
const dtos = await leagueService.getLeagueSeasonSummaries(leagueId);
|
||||
return dtos.map((dto: LeagueSeasonSummaryDTO) => new LeagueSeasonSummaryViewModel(dto));
|
||||
},
|
||||
enabled: !!leagueId && !!isAdmin,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLeagueAdminSchedule(leagueId: string, selectedSeasonId: string, isAdmin: boolean) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return usePageData({
|
||||
queryKey: ['adminSchedule', leagueId, selectedSeasonId],
|
||||
queryFn: async (): Promise<LeagueAdminScheduleViewModel> => {
|
||||
const dto = await leagueService.getAdminSchedule(leagueId, selectedSeasonId);
|
||||
const races = dto.races.map(mapRaceDtoToViewModel);
|
||||
return new LeagueAdminScheduleViewModel({
|
||||
seasonId: dto.seasonId,
|
||||
published: dto.published,
|
||||
races,
|
||||
});
|
||||
},
|
||||
enabled: !!leagueId && !!selectedSeasonId && !!isAdmin,
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useLeagueSeasons(leagueId: string) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['leagueSeasons', leagueId],
|
||||
queryFn: () => leagueService.getLeagueSeasons(leagueId),
|
||||
enabled: !!leagueId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SETTINGS_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel';
|
||||
|
||||
export function useLeagueSettings(
|
||||
leagueId: string,
|
||||
options?: Omit<UseQueryOptions<LeagueSettingsViewModel | null, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
const leagueSettingsService = useInject(LEAGUE_SETTINGS_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['leagueSettings', leagueId],
|
||||
queryFn: () => leagueSettingsService.getLeagueSettings(leagueId),
|
||||
enabled: !!leagueId,
|
||||
...options,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { usePageDataMultiple } from '@/lib/page/usePageData';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN, LEAGUE_MEMBERSHIP_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
|
||||
export function useLeagueSponsorshipsPageData(leagueId: string, currentDriverId: string) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
const leagueMembershipService = useInject(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
|
||||
|
||||
return usePageDataMultiple({
|
||||
league: {
|
||||
queryKey: ['leagueDetail', leagueId, currentDriverId],
|
||||
queryFn: () => leagueService.getLeagueDetail(leagueId),
|
||||
},
|
||||
membership: {
|
||||
queryKey: ['leagueMembership', leagueId, currentDriverId],
|
||||
queryFn: () => leagueMembershipService.fetchLeagueMemberships(leagueId).then(() => {
|
||||
return leagueMembershipService.getMembership(leagueId, currentDriverId);
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_STEWARDING_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useLeagueStewardingData(leagueId: string) {
|
||||
const leagueStewardingService = useInject(LEAGUE_STEWARDING_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['leagueStewardingData', leagueId],
|
||||
queryFn: () => leagueStewardingService.getLeagueStewardingData(leagueId),
|
||||
enabled: !!leagueId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { usePageMutation } from '@/lib/page/usePageData';
|
||||
|
||||
export function useLeagueStewardingMutations(onRefetch: () => void) {
|
||||
const acceptProtestMutation = usePageMutation(
|
||||
async (variables: { protestId: string; penaltyType: string; penaltyValue: number; stewardNotes: string; raceId: string; accusedDriverId: string; reason: string }) => {
|
||||
// TODO: Implement protest review and penalty application
|
||||
// await leagueStewardingService.reviewProtest({
|
||||
// protestId: variables.protestId,
|
||||
// stewardId: currentDriverId,
|
||||
// decision: 'uphold',
|
||||
// decisionNotes: variables.stewardNotes,
|
||||
// });
|
||||
|
||||
// await leagueStewardingService.applyPenalty({
|
||||
// raceId: variables.raceId,
|
||||
// driverId: variables.accusedDriverId,
|
||||
// stewardId: currentDriverId,
|
||||
// type: variables.penaltyType,
|
||||
// value: variables.penaltyValue,
|
||||
// reason: variables.reason,
|
||||
// protestId: variables.protestId,
|
||||
// notes: variables.stewardNotes,
|
||||
// });
|
||||
},
|
||||
{
|
||||
onSuccess: () => onRefetch(),
|
||||
}
|
||||
);
|
||||
|
||||
const rejectProtestMutation = usePageMutation(
|
||||
async (variables: { protestId: string; stewardNotes: string }) => {
|
||||
// TODO: Implement protest rejection
|
||||
// await leagueStewardingService.reviewProtest({
|
||||
// protestId: variables.protestId,
|
||||
// stewardId: currentDriverId,
|
||||
// decision: 'dismiss',
|
||||
// decisionNotes: variables.stewardNotes,
|
||||
// });
|
||||
},
|
||||
{
|
||||
onSuccess: () => onRefetch(),
|
||||
}
|
||||
);
|
||||
|
||||
return { acceptProtestMutation, rejectProtestMutation };
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { usePageData } from '@/lib/page/usePageData';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_WALLET_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { LeagueWalletViewModel } from '@/lib/view-models/LeagueWalletViewModel';
|
||||
import { WalletTransactionViewModel } from '@/lib/view-models/WalletTransactionViewModel';
|
||||
import { useLeagueWalletWithdrawalWithBlockers } from './useLeagueWalletWithdrawalWithBlockers';
|
||||
|
||||
export function useLeagueWalletPageData(leagueId: string) {
|
||||
const leagueWalletService = useInject(LEAGUE_WALLET_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = usePageData({
|
||||
queryKey: ['leagueWallet', leagueId],
|
||||
queryFn: async () => {
|
||||
const dto = await leagueWalletService.getWalletForLeague(leagueId);
|
||||
// Transform DTO to ViewModel at client boundary
|
||||
const transactions = dto.transactions.map(t => new WalletTransactionViewModel({
|
||||
id: t.id,
|
||||
type: t.type,
|
||||
description: t.description,
|
||||
amount: t.amount,
|
||||
fee: t.fee,
|
||||
netAmount: t.netAmount,
|
||||
date: new Date(t.date),
|
||||
status: t.status,
|
||||
reference: t.reference,
|
||||
}));
|
||||
return new LeagueWalletViewModel({
|
||||
balance: dto.balance,
|
||||
currency: dto.currency,
|
||||
totalRevenue: dto.totalRevenue,
|
||||
totalFees: dto.totalFees,
|
||||
totalWithdrawals: dto.totalWithdrawals,
|
||||
pendingPayouts: dto.pendingPayouts,
|
||||
transactions,
|
||||
canWithdraw: dto.canWithdraw,
|
||||
withdrawalBlockReason: dto.withdrawalBlockReason,
|
||||
});
|
||||
},
|
||||
enabled: !!leagueId,
|
||||
});
|
||||
|
||||
return queryResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use useLeagueWalletWithdrawalWithBlockers instead
|
||||
* This wrapper maintains backward compatibility while using the new blocker-aware hook
|
||||
*/
|
||||
export function useLeagueWalletWithdrawal(leagueId: string, data: any, refetch: () => void) {
|
||||
return useLeagueWalletWithdrawalWithBlockers(leagueId, data, refetch);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { usePageMutation } from '@/lib/page/usePageData';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_WALLET_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { SubmitBlocker, ThrottleBlocker } from '@/lib/blockers';
|
||||
|
||||
/**
|
||||
* Hook for wallet withdrawals with client-side blockers
|
||||
* Handles UX prevention mechanisms (rate limiting, duplicate submission prevention)
|
||||
*/
|
||||
export function useLeagueWalletWithdrawalWithBlockers(leagueId: string, data: any, refetch: () => void) {
|
||||
const leagueWalletService = useInject(LEAGUE_WALLET_SERVICE_TOKEN);
|
||||
|
||||
// Client-side blockers for UX improvement
|
||||
const submitBlocker = new SubmitBlocker();
|
||||
const throttle = new ThrottleBlocker(500);
|
||||
|
||||
const withdrawMutation = usePageMutation(
|
||||
async ({ amount }: { amount: number }) => {
|
||||
if (!data) throw new Error('Wallet data not available');
|
||||
|
||||
// Client-side blockers (UX only, not security)
|
||||
if (!submitBlocker.canExecute() || !throttle.canExecute()) {
|
||||
throw new Error('Request blocked due to rate limiting');
|
||||
}
|
||||
|
||||
submitBlocker.block();
|
||||
throttle.block();
|
||||
|
||||
try {
|
||||
const result = await leagueWalletService.withdraw(
|
||||
leagueId,
|
||||
amount,
|
||||
data.currency,
|
||||
'season-2', // Current active season
|
||||
'bank-account-***1234'
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Withdrawal failed');
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
submitBlocker.release();
|
||||
}
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Refetch wallet data after successful withdrawal
|
||||
refetch();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return withdrawMutation;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { PENALTY_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
|
||||
export function usePenaltyMutation() {
|
||||
const penaltyService = useInject(PENALTY_SERVICE_TOKEN);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const applyPenaltyMutation = useMutation({
|
||||
mutationFn: (command: any) => penaltyService.applyPenalty(command),
|
||||
onSuccess: () => {
|
||||
// Invalidate relevant queries to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: ['leagueStewardingData'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['penalties'] });
|
||||
},
|
||||
});
|
||||
|
||||
return applyPenaltyMutation;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_STEWARDING_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useProtestDetail(leagueId: string, protestId: string, enabled: boolean = true) {
|
||||
const leagueStewardingService = useInject(LEAGUE_STEWARDING_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['protestDetail', leagueId, protestId],
|
||||
queryFn: () => leagueStewardingService.getProtestDetailViewModel(leagueId, protestId),
|
||||
enabled: enabled && !!leagueId && !!protestId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useSponsorshipRequests(entityType: string, entityId: string) {
|
||||
const sponsorshipService = useInject(SPONSOR_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['sponsorshipRequests', entityType, entityId],
|
||||
queryFn: async () => {
|
||||
const result = await sponsorshipService.getPendingSponsorshipRequests({
|
||||
entityType,
|
||||
entityId,
|
||||
});
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!entityType && !!entityId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { OnboardingService } from '@/lib/services/onboarding/OnboardingService';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError } from '@/lib/contracts/services/Service';
|
||||
import { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
|
||||
import { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
|
||||
|
||||
export function useCompleteOnboarding(
|
||||
options?: Omit<UseMutationOptions<Result<CompleteOnboardingOutputDTO, DomainError>, Error, CompleteOnboardingInputDTO>, 'mutationFn'>
|
||||
) {
|
||||
return useMutation<Result<CompleteOnboardingOutputDTO, DomainError>, Error, CompleteOnboardingInputDTO>({
|
||||
mutationFn: async (input) => {
|
||||
const service = new OnboardingService();
|
||||
return await service.completeOnboarding(input);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { OnboardingService } from '@/lib/services/onboarding/OnboardingService';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError } from '@/lib/contracts/services/Service';
|
||||
|
||||
interface GenerateAvatarsParams {
|
||||
userId: string;
|
||||
facePhotoData: string;
|
||||
suitColor: string;
|
||||
}
|
||||
|
||||
interface GenerateAvatarsResult {
|
||||
success: boolean;
|
||||
avatarUrls?: string[];
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export function useGenerateAvatars(
|
||||
options?: Omit<UseMutationOptions<Result<GenerateAvatarsResult, DomainError>, Error, GenerateAvatarsParams>, 'mutationFn'>
|
||||
) {
|
||||
return useMutation<Result<GenerateAvatarsResult, DomainError>, Error, GenerateAvatarsParams>({
|
||||
mutationFn: async (params) => {
|
||||
const service = new OnboardingService();
|
||||
// This method doesn't exist in the service yet, but the hook is now created
|
||||
// The service will need to implement this or we need to adjust the architecture
|
||||
return Result.ok({ success: false, errorMessage: 'Not implemented' });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { usePageData } from '@/lib/page/usePageData';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { RACE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
|
||||
export function useAllRacesPageData() {
|
||||
const raceService = useInject(RACE_SERVICE_TOKEN);
|
||||
|
||||
return usePageData({
|
||||
queryKey: ['races', 'all'],
|
||||
queryFn: () => raceService.getAllRacesPageData(),
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { RACE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { FileProtestCommandDTO } from '@/lib/types/generated/FileProtestCommandDTO';
|
||||
|
||||
export function useFileProtest(
|
||||
options?: Omit<UseMutationOptions<void, ApiError, FileProtestCommandDTO>, 'mutationFn'>
|
||||
) {
|
||||
const raceService = useInject(RACE_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<void, ApiError, FileProtestCommandDTO>({
|
||||
mutationFn: (command) => raceService.fileProtest(command),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { usePageDataMultiple } from '@/lib/page/usePageData';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { RACE_RESULTS_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
|
||||
export function useRaceResultsPageData(raceId: string, currentDriverId: string) {
|
||||
const raceResultsService = useInject(RACE_RESULTS_SERVICE_TOKEN);
|
||||
|
||||
return usePageDataMultiple({
|
||||
results: {
|
||||
queryKey: ['raceResultsDetail', raceId, currentDriverId],
|
||||
queryFn: () => raceResultsService.getResultsDetail(raceId, currentDriverId),
|
||||
enabled: !!raceId,
|
||||
},
|
||||
sof: {
|
||||
queryKey: ['raceWithSOF', raceId],
|
||||
queryFn: () => raceResultsService.getWithSOF(raceId),
|
||||
enabled: !!raceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { RACE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
interface RegisterForRaceParams {
|
||||
raceId: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export function useRegisterForRace(
|
||||
options?: Omit<UseMutationOptions<void, ApiError, RegisterForRaceParams>, 'mutationFn'>
|
||||
) {
|
||||
const raceService = useInject(RACE_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<void, ApiError, RegisterForRaceParams>({
|
||||
mutationFn: (params) => raceService.registerForRace(params.raceId, params.leagueId, params.driverId),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { RACE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
interface WithdrawFromRaceParams {
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export function useWithdrawFromRace(
|
||||
options?: Omit<UseMutationOptions<void, ApiError, WithdrawFromRaceParams>, 'mutationFn'>
|
||||
) {
|
||||
const raceService = useInject(RACE_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<void, ApiError, WithdrawFromRaceParams>({
|
||||
mutationFn: (params) => raceService.withdrawFromRace(params.raceId, params.driverId),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export { useAvailableLeagues } from './useAvailableLeagues';
|
||||
export { useSponsorDashboard } from './useSponsorDashboard';
|
||||
export { useSponsorSponsorships } from './useSponsorSponsorships';
|
||||
export { useSponsorBilling } from './useSponsorBilling';
|
||||
export { useSponsorLeagueDetail } from './useSponsorLeagueDetail';
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useAvailableLeagues() {
|
||||
const sponsorService = useInject(SPONSOR_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['availableLeagues'],
|
||||
queryFn: async () => {
|
||||
const result = await sponsorService.getAvailableLeagues();
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useSponsorBilling(sponsorId: string) {
|
||||
const sponsorService = useInject(SPONSOR_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['sponsorBilling', sponsorId],
|
||||
queryFn: async () => {
|
||||
const result = await sponsorService.getBilling(sponsorId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!sponsorId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useSponsorDashboard(sponsorId: string) {
|
||||
const sponsorService = useInject(SPONSOR_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['sponsorDashboard', sponsorId],
|
||||
queryFn: async () => {
|
||||
const result = await sponsorService.getSponsorDashboard(sponsorId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!sponsorId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useSponsorLeagueDetail(leagueId: string) {
|
||||
const sponsorService = useInject(SPONSOR_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['sponsorLeagueDetail', leagueId],
|
||||
queryFn: async () => {
|
||||
const result = await sponsorService.getLeagueDetail(leagueId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!leagueId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useSponsorSponsorships(sponsorId: string) {
|
||||
const sponsorService = useInject(SPONSOR_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['sponsorSponsorships', sponsorId],
|
||||
queryFn: async () => {
|
||||
const result = await sponsorService.getSponsorSponsorships(sponsorId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!sponsorId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export { useAllTeams } from './useAllTeams';
|
||||
export { useTeamDetails } from './useTeamDetails';
|
||||
export { useTeamMembers } from './useTeamMembers';
|
||||
export { useTeamJoinRequests } from './useTeamJoinRequests';
|
||||
export { useCreateTeam } from './useCreateTeam';
|
||||
export { useUpdateTeam } from './useUpdateTeam';
|
||||
export { useTeamMembership } from './useTeamMembership';
|
||||
export { useApproveJoinRequest } from './useApproveJoinRequest';
|
||||
export { useRejectJoinRequest } from './useRejectJoinRequest';
|
||||
export { useTeamStandings } from './useTeamStandings';
|
||||
export { useJoinTeam } from './useJoinTeam';
|
||||
export { useLeaveTeam } from './useLeaveTeam';
|
||||
export { useTeamRoster } from './useTeamRoster';
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useAllTeams() {
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['allTeams'],
|
||||
queryFn: async () => {
|
||||
const result = await teamService.getAllTeams();
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_JOIN_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
export function useApproveJoinRequest(options?: Omit<UseMutationOptions<void, ApiError, void>, 'mutationFn'>) {
|
||||
const teamJoinService = useInject(TEAM_JOIN_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<void, ApiError, void>({
|
||||
mutationFn: () => teamJoinService.approveJoinRequest(),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { CreateTeamInputDTO } from '@/lib/types/generated/CreateTeamInputDTO';
|
||||
import type { CreateTeamOutputDTO } from '@/lib/types/generated/CreateTeamOutputDTO';
|
||||
|
||||
export function useCreateTeam(options?: UseMutationOptions<CreateTeamOutputDTO, ApiError, CreateTeamInputDTO>) {
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<CreateTeamOutputDTO, ApiError, CreateTeamInputDTO>({
|
||||
mutationFn: async (input) => {
|
||||
const result = await teamService.createTeam(input);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
interface JoinTeamParams {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
requiresApproval?: boolean;
|
||||
}
|
||||
|
||||
export function useJoinTeam(options?: Omit<UseMutationOptions<void, ApiError, JoinTeamParams>, 'mutationFn'>) {
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<void, ApiError, JoinTeamParams>({
|
||||
mutationFn: async (params) => {
|
||||
// Note: Team join functionality would need to be added to teamService
|
||||
// For now, we'll use a placeholder
|
||||
console.log('Joining team:', params);
|
||||
if (params.requiresApproval) {
|
||||
alert('Join request sent! Wait for team approval.');
|
||||
} else {
|
||||
alert('Successfully joined team!');
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
interface LeaveTeamParams {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export function useLeaveTeam(options?: Omit<UseMutationOptions<void, ApiError, LeaveTeamParams>, 'mutationFn'>) {
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<void, ApiError, LeaveTeamParams>({
|
||||
mutationFn: async (params) => {
|
||||
// Note: Leave team functionality would need to be added to teamService
|
||||
// For now, we'll use a placeholder
|
||||
console.log('Leaving team:', params);
|
||||
alert('Successfully left team');
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_JOIN_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
export function useRejectJoinRequest(options?: Omit<UseMutationOptions<void, ApiError, void>, 'mutationFn'>) {
|
||||
const teamJoinService = useInject(TEAM_JOIN_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<void, ApiError, void>({
|
||||
mutationFn: () => teamJoinService.rejectJoinRequest(),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useTeamDetails(teamId: string, currentUserId: string) {
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['teamDetails', teamId, currentUserId],
|
||||
queryFn: async () => {
|
||||
const result = await teamService.getTeamDetails(teamId, currentUserId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!teamId && !!currentUserId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_JOIN_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useTeamJoinRequests(teamId: string, currentUserId: string, isOwner: boolean) {
|
||||
const teamJoinService = useInject(TEAM_JOIN_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['teamJoinRequests', teamId, currentUserId, isOwner],
|
||||
queryFn: () => teamJoinService.getJoinRequests(teamId, currentUserId, isOwner),
|
||||
enabled: !!teamId && !!currentUserId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useTeamMembers(teamId: string, currentUserId: string, teamOwnerId: string) {
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['teamMembers', teamId, currentUserId, teamOwnerId],
|
||||
queryFn: async () => {
|
||||
const result = await teamService.getTeamMembers(teamId, currentUserId, teamOwnerId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!teamId && !!currentUserId && !!teamOwnerId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
import type { GetTeamMembershipOutputDTO } from '@/lib/types/generated/GetTeamMembershipOutputDTO';
|
||||
|
||||
export function useTeamMembership(teamId: string, driverId: string) {
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['teamMembership', teamId, driverId],
|
||||
queryFn: async () => {
|
||||
const result = await teamService.getMembership(teamId, driverId);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
enabled: !!teamId && !!driverId,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_SERVICE_TOKEN, DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
type TeamMemberRole = 'owner' | 'manager' | 'member';
|
||||
|
||||
interface TeamRosterMember {
|
||||
driver: any;
|
||||
role: TeamMemberRole;
|
||||
joinedAt: string;
|
||||
rating: number | null;
|
||||
overallRank: number | null;
|
||||
}
|
||||
|
||||
export function useTeamRoster(memberships: Array<{
|
||||
driverId: string;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
}>) {
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
const driverService = useInject(DRIVER_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery<TeamRosterMember[]>({
|
||||
queryKey: ['teamRoster', memberships],
|
||||
queryFn: async () => {
|
||||
// Get driver details for each membership
|
||||
const membersWithDetails = await Promise.all(
|
||||
memberships.map(async (m) => {
|
||||
const driver = await driverService.findById(m.driverId);
|
||||
// Convert role to TeamMemberRole
|
||||
const role: TeamMemberRole = m.role === 'owner' ? 'owner' :
|
||||
m.role === 'manager' ? 'manager' : 'member';
|
||||
return {
|
||||
driver: driver || { id: m.driverId, name: 'Unknown Driver', country: 'Unknown', position: 'N/A', races: '0', impressions: '0', team: 'None' },
|
||||
role,
|
||||
joinedAt: m.joinedAt,
|
||||
rating: null, // DriverDTO doesn't include rating
|
||||
overallRank: null, // DriverDTO doesn't include overallRank
|
||||
};
|
||||
})
|
||||
);
|
||||
return membersWithDetails;
|
||||
},
|
||||
enabled: memberships.length > 0,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function useTeamStandings(teamId: string, leagues: string[]) {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['teamStandings', teamId, leagues],
|
||||
queryFn: async () => {
|
||||
// For demo purposes, create mock standings
|
||||
return leagues.map(leagueId => ({
|
||||
leagueId,
|
||||
leagueName: `League ${leagueId}`,
|
||||
position: Math.floor(Math.random() * 10) + 1,
|
||||
points: Math.floor(Math.random() * 100),
|
||||
wins: Math.floor(Math.random() * 5),
|
||||
racesCompleted: Math.floor(Math.random() * 10),
|
||||
}));
|
||||
},
|
||||
enabled: leagues.length > 0,
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { UpdateTeamInputDTO } from '@/lib/types/generated/UpdateTeamInputDTO';
|
||||
import type { UpdateTeamOutputDTO } from '@/lib/types/generated/UpdateTeamOutputDTO';
|
||||
|
||||
export function useUpdateTeam(options?: UseMutationOptions<UpdateTeamOutputDTO, ApiError, { teamId: string; input: UpdateTeamInputDTO }>) {
|
||||
const teamService = useInject(TEAM_SERVICE_TOKEN);
|
||||
|
||||
return useMutation<UpdateTeamOutputDTO, ApiError, { teamId: string; input: UpdateTeamInputDTO }>({
|
||||
mutationFn: async ({ teamId, input }) => {
|
||||
const result = await teamService.updateTeam(teamId, input);
|
||||
if (result.isErr()) {
|
||||
throw result.getError();
|
||||
}
|
||||
return result.unwrap();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { POLICY_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { PolicySnapshotDto } from '@/lib/api/policy/PolicyApiClient';
|
||||
|
||||
export function useCapability(
|
||||
capabilityKey: string,
|
||||
options?: Omit<UseQueryOptions<PolicySnapshotDto, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
const policyService = useInject(POLICY_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['policySnapshot', capabilityKey],
|
||||
queryFn: () => policyService.getSnapshot(),
|
||||
staleTime: 60_000,
|
||||
gcTime: 5 * 60_000,
|
||||
...options,
|
||||
});
|
||||
|
||||
const enhancedResult = enhanceQueryResult(queryResult);
|
||||
|
||||
// Add helper to get capability state
|
||||
const capabilityState = enhancedResult.data
|
||||
? policyService.getCapabilityState(enhancedResult.data, capabilityKey)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...enhancedResult,
|
||||
capabilityState,
|
||||
isCapabilityEnabled: capabilityState === 'enabled',
|
||||
isCapabilityComingSoon: capabilityState === 'coming_soon',
|
||||
};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
|
||||
/**
|
||||
* Hook for driver leaderboard data
|
||||
*/
|
||||
export function useDriverLeaderboard() {
|
||||
const driverService = useInject(DRIVER_SERVICE_TOKEN);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['driverLeaderboard'],
|
||||
queryFn: async () => {
|
||||
return await driverService.getDriverLeaderboard();
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
retry: query.refetch,
|
||||
};
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
|
||||
|
||||
/**
|
||||
* useDriverSearch
|
||||
*
|
||||
* Client-side hook for UX-only search filtering.
|
||||
* This is view-only transformation, not business logic.
|
||||
*/
|
||||
export function useDriverSearch(drivers: DriverLeaderboardItemViewModel[]) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const filteredDrivers = useMemo(() => {
|
||||
if (!searchQuery) return drivers;
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return drivers.filter(driver => {
|
||||
return (
|
||||
driver.name.toLowerCase().includes(query) ||
|
||||
driver.nationality.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
}, [drivers, searchQuery]);
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
filteredDrivers,
|
||||
};
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { useCurrentDriver } from './driver/useCurrentDriver';
|
||||
|
||||
/**
|
||||
* Hook to get the current driver ID from the user's session.
|
||||
* Returns the driver ID string or undefined if not available.
|
||||
*/
|
||||
export function useEffectiveDriverId(): string | undefined {
|
||||
const { data: currentDriver } = useCurrentDriver();
|
||||
return currentDriver?.id;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { useEnhancedForm } from './useEnhancedForm';
|
||||
|
||||
describe('useEnhancedForm', () => {
|
||||
it('should be defined', () => {
|
||||
expect(useEnhancedForm).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,349 +0,0 @@
|
||||
/**
|
||||
* Enhanced Form Hook with Advanced Error Handling
|
||||
*
|
||||
* Provides comprehensive form state management, validation, and error handling
|
||||
* with both user-friendly and developer-friendly error messages.
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect, FormEvent, ChangeEvent, Dispatch, SetStateAction } from 'react';
|
||||
import { parseApiError, formatValidationErrorsForForm, logErrorWithContext, createErrorContext } from '@/lib/utils/errorUtils';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
export interface FormField<T> {
|
||||
value: T;
|
||||
error?: string;
|
||||
touched: boolean;
|
||||
validating: boolean;
|
||||
}
|
||||
|
||||
export interface FormState<T extends Record<string, any>> {
|
||||
fields: { [K in keyof T]: FormField<T[K]> };
|
||||
isValid: boolean;
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
submitCount: number;
|
||||
}
|
||||
|
||||
export interface FormOptions<T extends Record<string, any>> {
|
||||
initialValues: T;
|
||||
validate?: (values: T) => Record<string, string> | Promise<Record<string, string>>;
|
||||
onSubmit: (values: T) => Promise<void>;
|
||||
onError?: (error: unknown, values: T) => void;
|
||||
onSuccess?: (values: T) => void;
|
||||
component?: string;
|
||||
}
|
||||
|
||||
export interface UseEnhancedFormReturn<T extends Record<string, any>> {
|
||||
formState: FormState<T>;
|
||||
setFormState: Dispatch<SetStateAction<FormState<T>>>;
|
||||
handleChange: (e: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => void;
|
||||
setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void;
|
||||
setFieldError: <K extends keyof T>(field: K, error: string) => void;
|
||||
handleSubmit: (e: FormEvent<HTMLFormElement>) => Promise<void>;
|
||||
reset: () => void;
|
||||
setFormError: (error: string) => void;
|
||||
clearFieldError: <K extends keyof T>(field: K) => void;
|
||||
validateField: <K extends keyof T>(field: K) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced form hook with comprehensive error handling
|
||||
*/
|
||||
export function useEnhancedForm<T extends Record<string, any>>(
|
||||
options: FormOptions<T>
|
||||
): UseEnhancedFormReturn<T> {
|
||||
const [formState, setFormState] = useState<FormState<T>>(() => ({
|
||||
fields: Object.keys(options.initialValues).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: {
|
||||
value: options.initialValues[key as keyof T],
|
||||
error: undefined,
|
||||
touched: false,
|
||||
validating: false,
|
||||
}
|
||||
}), {} as { [K in keyof T]: FormField<T[K]> }),
|
||||
isValid: true,
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
submitCount: 0,
|
||||
}));
|
||||
|
||||
// Validate form on change
|
||||
useEffect(() => {
|
||||
if (options.validate && formState.submitCount > 0) {
|
||||
const validateAsync = async () => {
|
||||
try {
|
||||
const errors = await options.validate!(getValues());
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isValid: Object.keys(errors).length === 0,
|
||||
fields: Object.keys(prev.fields).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: {
|
||||
...prev.fields[key as keyof T],
|
||||
error: errors[key],
|
||||
}
|
||||
}), {} as { [K in keyof T]: FormField<T[K]> }),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Validation error:', error);
|
||||
}
|
||||
};
|
||||
validateAsync();
|
||||
}
|
||||
}, [formState.fields, formState.submitCount, options.validate]);
|
||||
|
||||
const getValues = useCallback((): T => {
|
||||
return Object.keys(formState.fields).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: formState.fields[key as keyof T].value,
|
||||
}), {} as T);
|
||||
}, [formState.fields]);
|
||||
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
const checked = 'checked' in e.target ? e.target.checked : false;
|
||||
const fieldValue = type === 'checkbox' ? checked : value;
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[name]: {
|
||||
...prev.fields[name as keyof T],
|
||||
value: fieldValue as T[keyof T],
|
||||
touched: true,
|
||||
error: undefined, // Clear error on change
|
||||
},
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setFieldValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
value,
|
||||
touched: true,
|
||||
error: undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setFieldError = useCallback(<K extends keyof T>(field: K, error: string) => {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
error,
|
||||
touched: true,
|
||||
},
|
||||
},
|
||||
isValid: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const clearFieldError = useCallback(<K extends keyof T>(field: K) => {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
error: undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setFormError = useCallback((error: string) => {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
submitError: error,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const validateField = useCallback(async <K extends keyof T>(field: K) => {
|
||||
if (!options.validate) return;
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
validating: true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
try {
|
||||
const values = getValues();
|
||||
const errors = await options.validate(values);
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
error: errors[field as string],
|
||||
validating: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
validating: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, [options.validate, getValues]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setFormState({
|
||||
fields: Object.keys(options.initialValues).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: {
|
||||
value: options.initialValues[key as keyof T],
|
||||
error: undefined,
|
||||
touched: false,
|
||||
validating: false,
|
||||
}
|
||||
}), {} as { [K in keyof T]: FormField<T[K]> }),
|
||||
isValid: true,
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
submitCount: 0,
|
||||
});
|
||||
}, [options.initialValues]);
|
||||
|
||||
const handleSubmit = useCallback(async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const values = getValues();
|
||||
|
||||
// Increment submit count to trigger validation
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
submitCount: prev.submitCount + 1,
|
||||
isSubmitting: true,
|
||||
submitError: undefined,
|
||||
}));
|
||||
|
||||
// Run validation if provided
|
||||
if (options.validate) {
|
||||
try {
|
||||
const errors = await options.validate(values);
|
||||
const hasErrors = Object.keys(errors).length > 0;
|
||||
|
||||
if (hasErrors) {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isSubmitting: false,
|
||||
isValid: false,
|
||||
fields: Object.keys(prev.fields).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: {
|
||||
...prev.fields[key as keyof T],
|
||||
error: errors[key],
|
||||
touched: true,
|
||||
}
|
||||
}), {} as { [K in keyof T]: FormField<T[K]> }),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
} catch (validationError) {
|
||||
logErrorWithContext(validationError, {
|
||||
timestamp: new Date().toISOString(),
|
||||
component: options.component || 'useEnhancedForm',
|
||||
action: 'validate',
|
||||
formData: values,
|
||||
});
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isSubmitting: false,
|
||||
submitError: 'Validation failed. Please check your input.',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the form
|
||||
try {
|
||||
await options.onSubmit(values);
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
}));
|
||||
|
||||
options.onSuccess?.(values);
|
||||
} catch (error) {
|
||||
const parsed = parseApiError(error);
|
||||
|
||||
// Log for developers
|
||||
logErrorWithContext(error, {
|
||||
timestamp: new Date().toISOString(),
|
||||
component: options.component || 'useEnhancedForm',
|
||||
action: 'submit',
|
||||
formData: values,
|
||||
});
|
||||
|
||||
// Handle validation errors from API
|
||||
if (parsed.isValidationError && parsed.validationErrors.length > 0) {
|
||||
const fieldErrors = formatValidationErrorsForForm(parsed.validationErrors);
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isSubmitting: false,
|
||||
isValid: false,
|
||||
fields: Object.keys(prev.fields).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: {
|
||||
...prev.fields[key as keyof T],
|
||||
error: fieldErrors[key],
|
||||
touched: true,
|
||||
}
|
||||
}), {} as { [K in keyof T]: FormField<T[K]> }),
|
||||
}));
|
||||
} else {
|
||||
// General submit error
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isSubmitting: false,
|
||||
submitError: parsed.userMessage,
|
||||
}));
|
||||
}
|
||||
|
||||
options.onError?.(error, values);
|
||||
}
|
||||
}, [getValues, options]);
|
||||
|
||||
return {
|
||||
formState,
|
||||
setFormState,
|
||||
handleChange,
|
||||
setFieldValue,
|
||||
setFieldError,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setFormError,
|
||||
clearFieldError,
|
||||
validateField,
|
||||
};
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';
|
||||
|
||||
export function useLeagueScoringPresets() {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['leagueScoringPresets'],
|
||||
queryFn: async () => {
|
||||
const result = await leagueService.getScoringPresets();
|
||||
return result as LeagueScoringPresetDTO[];
|
||||
},
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
|
||||
import { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO';
|
||||
|
||||
export interface LeagueWizardFormModel {
|
||||
leagueId?: string;
|
||||
basics?: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
visibility?: string;
|
||||
gameId?: string;
|
||||
};
|
||||
structure?: {
|
||||
mode?: string;
|
||||
maxDrivers?: number;
|
||||
maxTeams?: number;
|
||||
driversPerTeam?: number;
|
||||
multiClassEnabled?: boolean;
|
||||
};
|
||||
championships?: {
|
||||
enableDriverChampionship?: boolean;
|
||||
enableTeamChampionship?: boolean;
|
||||
enableNationsChampionship?: boolean;
|
||||
enableTrophyChampionship?: boolean;
|
||||
};
|
||||
scoring?: {
|
||||
patternId?: string;
|
||||
customScoringEnabled?: boolean;
|
||||
};
|
||||
dropPolicy?: {
|
||||
strategy?: string;
|
||||
n?: number;
|
||||
};
|
||||
timings?: {
|
||||
practiceMinutes?: number;
|
||||
qualifyingMinutes?: number;
|
||||
sprintRaceMinutes?: number;
|
||||
mainRaceMinutes?: number;
|
||||
sessionCount?: number;
|
||||
roundsPlanned?: number;
|
||||
raceDayOfWeek?: number;
|
||||
raceTimeUtc?: string;
|
||||
weekdays?: string[];
|
||||
recurrenceStrategy?: string;
|
||||
timezoneId?: string;
|
||||
seasonStartDate?: string;
|
||||
};
|
||||
stewarding?: {
|
||||
decisionMode?: string;
|
||||
requiredVotes?: number;
|
||||
requireDefense?: boolean;
|
||||
defenseTimeLimit?: number;
|
||||
voteTimeLimit?: number;
|
||||
protestDeadlineHours?: number;
|
||||
stewardingClosesHours?: number;
|
||||
notifyAccusedOnProtest?: boolean;
|
||||
notifyOnVoteRequired?: boolean;
|
||||
};
|
||||
seasonName?: string;
|
||||
}
|
||||
|
||||
export function useCreateLeagueWizard() {
|
||||
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (params: { form: LeagueWizardFormModel; ownerId: string }): Promise<CreateLeagueOutputDTO> => {
|
||||
// Convert form to CreateLeagueInputDTO
|
||||
const input: CreateLeagueInputDTO = {
|
||||
name: params.form.basics?.name?.trim() ?? '',
|
||||
description: params.form.basics?.description?.trim() ?? '',
|
||||
visibility: (params.form.basics?.visibility as 'public' | 'private') ?? 'public',
|
||||
ownerId: params.ownerId,
|
||||
};
|
||||
|
||||
// Use the league service to create the league
|
||||
const result = await leagueService.createLeague(input);
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { PENALTY_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||
|
||||
export function usePenaltyTypesReference() {
|
||||
const penaltyService = useInject(PENALTY_SERVICE_TOKEN);
|
||||
|
||||
const queryResult = useQuery({
|
||||
queryKey: ['penaltyTypesReference'],
|
||||
queryFn: () => penaltyService.getPenaltyTypesReference(),
|
||||
});
|
||||
|
||||
return enhanceQueryResult(queryResult);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, RefObject } from 'react';
|
||||
|
||||
/**
|
||||
* Calculate scroll progress (0-1) based on element's position in viewport
|
||||
* @param ref - Reference to the element to track
|
||||
* @param offset - Offset from viewport edges (0-1, default 0.1)
|
||||
* @returns progress value between 0 and 1
|
||||
*/
|
||||
export function useScrollProgress(ref: RefObject<HTMLElement | null>, offset: number = 0.1): number {
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
let rafId: number;
|
||||
|
||||
const calculateProgress = () => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const scrollY = window.scrollY;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
|
||||
// Element enters viewport from bottom
|
||||
const enterPoint = viewportHeight * (1 - offset);
|
||||
// Element reaches top of viewport
|
||||
const exitPoint = viewportHeight * offset;
|
||||
|
||||
// Calculate progress: 0 when entering, 1 at 30% viewport (accelerated)
|
||||
const elementCenter = rect.top + rect.height / 2;
|
||||
const totalDistance = enterPoint - exitPoint;
|
||||
const currentDistance = enterPoint - elementCenter;
|
||||
|
||||
// Accelerate progress to reach 1.0 at 30% viewport height
|
||||
// Scale factor: 1.67 makes progress reach 1.0 at ~30% instead of 50%
|
||||
const rawProgress = (currentDistance / totalDistance) * 1.67;
|
||||
let clampedProgress = Math.max(0, Math.min(1, rawProgress));
|
||||
|
||||
// At bottom of page - ensure elements near bottom can reach 100%
|
||||
// Only apply if we're at the very bottom AND this element is below the fold
|
||||
if (scrollY + viewportHeight >= documentHeight - 50 && rect.top < viewportHeight) {
|
||||
clampedProgress = Math.max(clampedProgress, 1);
|
||||
}
|
||||
|
||||
setProgress(clampedProgress);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
rafId = requestAnimationFrame(calculateProgress);
|
||||
};
|
||||
|
||||
// Initial calculation
|
||||
calculateProgress();
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
window.addEventListener('resize', handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('resize', handleScroll);
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
};
|
||||
}, [ref, offset]);
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate parallax offset based on scroll position
|
||||
* @param ref - Reference to the element to track
|
||||
* @param speed - Parallax speed multiplier (default 0.5)
|
||||
* @returns offset in pixels
|
||||
*/
|
||||
export function useParallax(ref: RefObject<HTMLElement | null>, speed: number = 0.5): number {
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
let rafId: number;
|
||||
|
||||
const calculateOffset = () => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Calculate offset based on element position relative to viewport
|
||||
const scrolled = viewportHeight - rect.top;
|
||||
const parallaxOffset = scrolled * speed;
|
||||
|
||||
setOffset(parallaxOffset);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
rafId = requestAnimationFrame(calculateOffset);
|
||||
};
|
||||
|
||||
calculateOffset();
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
window.addEventListener('resize', handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('resize', handleScroll);
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
};
|
||||
}, [ref, speed]);
|
||||
|
||||
return offset;
|
||||
}
|
||||
@@ -27,12 +27,7 @@ export class ProfilePageQuery implements PageQuery<ProfileViewData, void, Presen
|
||||
}
|
||||
|
||||
const dto = profileResult.unwrap();
|
||||
const output = ProfileViewDataBuilder.build(dto);
|
||||
return Result.ok(output);
|
||||
const viewData = ProfileViewDataBuilder.build(dto);
|
||||
return Result.ok(viewData);
|
||||
}
|
||||
|
||||
static async execute(): Promise<Result<ProfileViewData, PresentationError>> {
|
||||
const query = new ProfilePageQuery();
|
||||
return query.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,16 @@
|
||||
import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
import { TeamService } from '@/lib/services/teams/TeamService';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
|
||||
/**
|
||||
* TeamsPageDto - Raw serializable data for teams page
|
||||
* Contains only raw data, no derived/computed properties
|
||||
*/
|
||||
export interface TeamsPageDto {
|
||||
teams: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
memberCount: number;
|
||||
description?: string;
|
||||
totalWins: number;
|
||||
totalRaces: number;
|
||||
performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
isRecruiting: boolean;
|
||||
specialization?: 'endurance' | 'sprint' | 'mixed';
|
||||
region?: string;
|
||||
languages: string[];
|
||||
leagues: string[];
|
||||
logoUrl?: string;
|
||||
rating?: number;
|
||||
category?: string;
|
||||
}>;
|
||||
}
|
||||
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
|
||||
import { TeamsViewDataBuilder } from '@/lib/builders/view-data/TeamsViewDataBuilder';
|
||||
|
||||
/**
|
||||
* TeamsPageQuery - Server-side composition for teams list page
|
||||
* Manual wiring only; no ContainerManager; no PageDataFetcher
|
||||
* Returns raw serializable DTO
|
||||
*/
|
||||
export class TeamsPageQuery {
|
||||
static async execute(): Promise<PageQueryResult<TeamsPageDto>> {
|
||||
export class TeamsPageQuery implements PageQuery<TeamsViewData, void> {
|
||||
async execute(): Promise<Result<TeamsViewData, PresentationError>> {
|
||||
try {
|
||||
// Manual dependency creation
|
||||
const service = new TeamService();
|
||||
@@ -42,51 +19,17 @@ export class TeamsPageQuery {
|
||||
const result = await service.getAllTeams();
|
||||
|
||||
if (result.isErr()) {
|
||||
return { status: 'error', errorId: 'TEAMS_FETCH_FAILED' };
|
||||
return Result.err(mapToPresentationError(result.getError()));
|
||||
}
|
||||
|
||||
const teams = result.unwrap();
|
||||
|
||||
if (!teams || teams.length === 0) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
// Transform to ViewData using builder
|
||||
const viewData = TeamsViewDataBuilder.build({ teams });
|
||||
|
||||
// Transform to raw serializable DTO
|
||||
const dto: TeamsPageDto = {
|
||||
teams: teams.map((team: TeamSummaryViewModel) => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
memberCount: team.memberCount,
|
||||
description: team.description,
|
||||
totalWins: team.totalWins,
|
||||
totalRaces: team.totalRaces,
|
||||
performanceLevel: team.performanceLevel,
|
||||
isRecruiting: team.isRecruiting,
|
||||
specialization: team.specialization,
|
||||
region: team.region,
|
||||
languages: team.languages,
|
||||
leagues: team.leagues,
|
||||
logoUrl: team.logoUrl,
|
||||
rating: team.rating,
|
||||
category: team.category,
|
||||
})),
|
||||
};
|
||||
|
||||
return { status: 'ok', dto };
|
||||
return Result.ok(viewData);
|
||||
} 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: 'TEAMS_FETCH_FAILED' };
|
||||
}
|
||||
return { status: 'error', errorId: 'UNKNOWN_ERROR' };
|
||||
return Result.err('unknown');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
|
||||
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
|
||||
import { RacesViewData } from '@/lib/view-data/races/RacesViewData';
|
||||
import { RacesViewData } from '@/lib/view-data/RacesViewData';
|
||||
import { RacesService } from '@/lib/services/races/RacesService';
|
||||
import { RacesViewDataBuilder } from '@/lib/builders/view-data/RacesViewDataBuilder';
|
||||
|
||||
@@ -27,10 +27,4 @@ export class RacesPageQuery implements PageQuery<RacesViewData, void> {
|
||||
const viewData = RacesViewDataBuilder.build(result.unwrap());
|
||||
return Result.ok(viewData);
|
||||
}
|
||||
|
||||
// Static method to avoid object construction in server code
|
||||
static async execute(): Promise<Result<RacesViewData, PresentationError>> {
|
||||
const query = new RacesPageQuery();
|
||||
return await query.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { Service } from '@/lib/contracts/services/Service';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||
|
||||
type DriverProfileServiceError = 'notFound' | 'unauthorized' | 'serverError' | 'unknown';
|
||||
|
||||
export class DriverProfileService implements Service {
|
||||
async getDriverProfile(driverId: string): Promise<Result<GetDriverProfileOutputDTO, DriverProfileServiceError>> {
|
||||
private apiClient: DriversApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
this.apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async getDriverProfile(driverId: string): Promise<Result<GetDriverProfileOutputDTO, DriverProfileServiceError>> {
|
||||
try {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
|
||||
const apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
|
||||
const dto = await apiClient.getDriverProfile(driverId);
|
||||
const dto = await this.apiClient.getDriverProfile(driverId);
|
||||
|
||||
if (!dto.currentDriver) {
|
||||
return Result.err('notFound');
|
||||
@@ -40,8 +38,6 @@ export class DriverProfileService implements Service {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
logger.error('DriverProfileService failed', error instanceof Error ? error : undefined, { error: errorAny });
|
||||
|
||||
if (errorAny.statusCode && errorAny.statusCode >= 500) {
|
||||
return Result.err('serverError');
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError } from '@/lib/contracts/services/Service';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO';
|
||||
import type { RaceDetailDTO } from '@/lib/api/races/RacesApiClient';
|
||||
import type { RaceResultsDetailDTO } from '@/lib/types/generated/RaceResultsDetailDTO';
|
||||
import type { RaceWithSOFDTO } from '@/lib/types/generated/RaceWithSOFDTO';
|
||||
|
||||
/**
|
||||
* Races Service
|
||||
@@ -12,7 +16,7 @@ import { ApiError } from '@/lib/api/base/ApiError';
|
||||
* Orchestration service for race-related operations.
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
*/
|
||||
export class RacesService {
|
||||
export class RacesService implements Service {
|
||||
private apiClient: RacesApiClient;
|
||||
|
||||
constructor() {
|
||||
@@ -28,21 +32,12 @@ export class RacesService {
|
||||
* Get races page data
|
||||
* Returns races for the main races page
|
||||
*/
|
||||
async getRacesPageData(): Promise<Result<any, DomainError>> {
|
||||
async getRacesPageData(): Promise<Result<RacesPageDataDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getPageData();
|
||||
return Result.ok(data);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
return Result.err({
|
||||
type: this.mapApiErrorType(error.type),
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
return Result.err({
|
||||
type: 'unknown',
|
||||
message: 'Failed to fetch races page data'
|
||||
});
|
||||
return Result.err(this.mapError(error, 'Failed to fetch races page data'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,21 +45,12 @@ export class RacesService {
|
||||
* Get race detail
|
||||
* Returns detailed information for a specific race
|
||||
*/
|
||||
async getRaceDetail(raceId: string, driverId: string): Promise<Result<any, DomainError>> {
|
||||
async getRaceDetail(raceId: string, driverId: string): Promise<Result<RaceDetailDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getDetail(raceId, driverId);
|
||||
return Result.ok(data);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
return Result.err({
|
||||
type: this.mapApiErrorType(error.type),
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
return Result.err({
|
||||
type: 'unknown',
|
||||
message: 'Failed to fetch race detail'
|
||||
});
|
||||
return Result.err(this.mapError(error, 'Failed to fetch race detail'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,21 +58,12 @@ export class RacesService {
|
||||
* Get race results detail
|
||||
* Returns results for a specific race
|
||||
*/
|
||||
async getRaceResultsDetail(raceId: string): Promise<Result<any, DomainError>> {
|
||||
async getRaceResultsDetail(raceId: string): Promise<Result<RaceResultsDetailDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getResultsDetail(raceId);
|
||||
return Result.ok(data);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
return Result.err({
|
||||
type: this.mapApiErrorType(error.type),
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
return Result.err({
|
||||
type: 'unknown',
|
||||
message: 'Failed to fetch race results'
|
||||
});
|
||||
return Result.err(this.mapError(error, 'Failed to fetch race results'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,21 +71,12 @@ export class RacesService {
|
||||
* Get race with strength of field
|
||||
* Returns race data with SOF calculation
|
||||
*/
|
||||
async getRaceWithSOF(raceId: string): Promise<Result<any, DomainError>> {
|
||||
async getRaceWithSOF(raceId: string): Promise<Result<RaceWithSOFDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getWithSOF(raceId);
|
||||
return Result.ok(data);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
return Result.err({
|
||||
type: this.mapApiErrorType(error.type),
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
return Result.err({
|
||||
type: 'unknown',
|
||||
message: 'Failed to fetch race SOF'
|
||||
});
|
||||
return Result.err(this.mapError(error, 'Failed to fetch race SOF'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,24 +84,28 @@ export class RacesService {
|
||||
* Get all races for the all races page
|
||||
* Returns all races with pagination support
|
||||
*/
|
||||
async getAllRacesPageData(): Promise<Result<any, DomainError>> {
|
||||
async getAllRacesPageData(): Promise<Result<RacesPageDataDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getPageData();
|
||||
return Result.ok(data);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
return Result.err({
|
||||
type: this.mapApiErrorType(error.type),
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
return Result.err({
|
||||
type: 'unknown',
|
||||
message: 'Failed to fetch all races'
|
||||
});
|
||||
return Result.err(this.mapError(error, 'Failed to fetch all races'));
|
||||
}
|
||||
}
|
||||
|
||||
private mapError(error: unknown, defaultMessage: string): DomainError {
|
||||
if (error instanceof ApiError) {
|
||||
return {
|
||||
type: this.mapApiErrorType(error.type),
|
||||
message: error.message
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'unknown',
|
||||
message: defaultMessage
|
||||
};
|
||||
}
|
||||
|
||||
private mapApiErrorType(apiErrorType: string): DomainError['type'] {
|
||||
switch (apiErrorType) {
|
||||
case 'NOT_FOUND':
|
||||
@@ -150,4 +122,4 @@ export class RacesService {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
apps/website/lib/view-data/LeagueAdminScheduleViewData.ts
Normal file
17
apps/website/lib/view-data/LeagueAdminScheduleViewData.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface AdminScheduleRaceData {
|
||||
id: string;
|
||||
name: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string; // ISO string
|
||||
}
|
||||
|
||||
export interface LeagueAdminScheduleViewData {
|
||||
published: boolean;
|
||||
races: AdminScheduleRaceData[];
|
||||
seasons: Array<{
|
||||
seasonId: string;
|
||||
name: string;
|
||||
}>;
|
||||
seasonId: string;
|
||||
}
|
||||
19
apps/website/lib/view-data/LeagueRulebookViewData.ts
Normal file
19
apps/website/lib/view-data/LeagueRulebookViewData.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface RulebookScoringConfig {
|
||||
scoringPresetName: string | null;
|
||||
gameName: string;
|
||||
championships: Array<{
|
||||
type: string;
|
||||
sessionTypes: string[];
|
||||
pointsPreview: Array<{
|
||||
sessionType: string;
|
||||
position: number;
|
||||
points: number;
|
||||
}>;
|
||||
bonusSummary: string[];
|
||||
}>;
|
||||
dropPolicySummary: string;
|
||||
}
|
||||
|
||||
export interface LeagueRulebookViewData {
|
||||
scoringConfig: RulebookScoringConfig | null;
|
||||
}
|
||||
35
apps/website/lib/view-data/RacesViewData.ts
Normal file
35
apps/website/lib/view-data/RacesViewData.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export interface RaceViewData {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
scheduledAtLabel: string;
|
||||
timeLabel: string;
|
||||
relativeTimeLabel: string;
|
||||
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
|
||||
statusLabel: string;
|
||||
sessionType: string;
|
||||
leagueId: string | null;
|
||||
leagueName: string | null;
|
||||
strengthOfField: number | null;
|
||||
isUpcoming: boolean;
|
||||
isLive: boolean;
|
||||
isPast: boolean;
|
||||
}
|
||||
|
||||
export interface RacesViewData {
|
||||
races: RaceViewData[];
|
||||
totalCount: number;
|
||||
scheduledCount: number;
|
||||
runningCount: number;
|
||||
completedCount: number;
|
||||
leagues: Array<{ id: string; name: string }>;
|
||||
upcomingRaces: RaceViewData[];
|
||||
liveRaces: RaceViewData[];
|
||||
recentResults: RaceViewData[];
|
||||
racesByDate: Array<{
|
||||
dateKey: string;
|
||||
dateLabel: string;
|
||||
races: RaceViewData[];
|
||||
}>;
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
export interface LeagueWalletTransactionViewData {
|
||||
id: string;
|
||||
type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize';
|
||||
amount: number;
|
||||
formattedAmount: string;
|
||||
amountColor: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
formattedDate: string;
|
||||
status: 'completed' | 'pending' | 'failed';
|
||||
statusColor: string;
|
||||
typeColor: string;
|
||||
}
|
||||
|
||||
export interface LeagueWalletViewData {
|
||||
leagueId: string;
|
||||
balance: number;
|
||||
formattedBalance: string;
|
||||
currency: string;
|
||||
transactions: Array<{
|
||||
id: string;
|
||||
type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize';
|
||||
amount: number;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
status: 'completed' | 'pending' | 'failed';
|
||||
}>;
|
||||
}
|
||||
transactions: LeagueWalletTransactionViewData[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user