website cleanup

This commit is contained in:
2025-12-25 00:19:36 +01:00
parent d78854a4c6
commit 9486455b9e
82 changed files with 1223 additions and 363 deletions

View File

@@ -42,6 +42,7 @@ import { MembershipFeeService } from './payments/MembershipFeeService';
import { AuthService } from './auth/AuthService';
import { SessionService } from './auth/SessionService';
import { ProtestService } from './protests/ProtestService';
import { OnboardingService } from './onboarding/OnboardingService';
/**
* ServiceFactory - Composition root for all services
@@ -298,10 +299,17 @@ export class ServiceFactory {
return new PenaltyService(this.apiClients.penalties);
}
/**
* Create OnboardingService instance
*/
createOnboardingService(): OnboardingService {
return new OnboardingService(this.apiClients.media, this.apiClients.drivers);
}
/**
* Create LandingService instance (used by server components)
*/
createLandingService(): LandingService {
return new LandingService(this.apiClients.races, this.apiClients.leagues, this.apiClients.teams);
return new LandingService(this.apiClients.races, this.apiClients.leagues, this.apiClients.teams, this.apiClients.auth);
}
}

View File

@@ -30,6 +30,8 @@ import { SponsorService } from './sponsors/SponsorService';
import { SponsorshipService } from './sponsors/SponsorshipService';
import { TeamJoinService } from './teams/TeamJoinService';
import { TeamService } from './teams/TeamService';
import { OnboardingService } from './onboarding/OnboardingService';
import { LandingService } from './landing/LandingService';
export interface Services {
raceService: RaceService;
@@ -57,6 +59,8 @@ export interface Services {
sessionService: SessionService;
protestService: ProtestService;
penaltyService: PenaltyService;
onboardingService: OnboardingService;
landingService: LandingService;
}
const queryClient = new QueryClient({
@@ -104,6 +108,8 @@ export function ServiceProvider({ children }: ServiceProviderProps) {
sessionService: serviceFactory.createSessionService(),
protestService: serviceFactory.createProtestService(),
penaltyService: serviceFactory.createPenaltyService(),
onboardingService: serviceFactory.createOnboardingService(),
landingService: serviceFactory.createLandingService(),
};
}, []);

View File

@@ -1,22 +1,27 @@
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
import type { AllLeaguesWithCapacityDTO } from '@/lib/types/generated/AllLeaguesWithCapacityDTO';
import type { GetAllTeamsOutputDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO';
import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO';
import type { LeagueSummaryDTO } from '@/lib/types/generated/LeagueSummaryDTO';
import type { LeagueWithCapacityDTO } from '@/lib/types/generated/LeagueWithCapacityDTO';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
import type { SignupParamsDTO } from '@/lib/types/generated/SignupParamsDTO';
import type { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
import { RacesPageViewModel } from '@/lib/view-models/RacesPageViewModel';
import { HomeDiscoveryViewModel } from '@/lib/view-models/HomeDiscoveryViewModel';
import { LeagueCardViewModel } from '@/lib/view-models/LeagueCardViewModel';
import { TeamCardViewModel } from '@/lib/view-models/TeamCardViewModel';
import { UpcomingRaceCardViewModel } from '@/lib/view-models/UpcomingRaceCardViewModel';
import { EmailSignupViewModel } from '@/lib/view-models/EmailSignupViewModel';
export class LandingService {
constructor(
private readonly racesApi: RacesApiClient,
private readonly leaguesApi: LeaguesApiClient,
private readonly teamsApi: TeamsApiClient,
private readonly authApi: AuthApiClient,
) {}
async getHomeDiscovery(): Promise<HomeDiscoveryViewModel> {
@@ -29,10 +34,10 @@ export class LandingService {
const racesVm = new RacesPageViewModel(racesDto);
const topLeagues = leaguesDto.leagues.slice(0, 4).map(
(league: LeagueSummaryDTO) => new LeagueCardViewModel({
(league: LeagueWithCapacityDTO) => new LeagueCardViewModel({
id: league.id,
name: league.name,
description: 'Competitive iRacing league',
description: league.description ?? 'Competitive iRacing league',
}),
);
@@ -62,4 +67,36 @@ export class LandingService {
upcomingRaces,
});
}
}
/**
* Sign up for early access with email
* Uses the auth signup endpoint
*/
async signup(email: string): Promise<EmailSignupViewModel> {
try {
// Create signup params with default values for early access
const signupParams: SignupParamsDTO = {
email,
password: 'temp_password_' + Math.random().toString(36).substring(7), // Temporary password
displayName: email.split('@')[0], // Use email prefix as display name
};
const session: AuthSessionDTO = await this.authApi.signup(signupParams);
if (session?.user?.userId) {
return new EmailSignupViewModel(email, 'Welcome to GridPilot! Check your email to confirm.', 'success');
} else {
return new EmailSignupViewModel(email, 'Signup successful but session not created.', 'error');
}
} catch (error: any) {
// Handle specific error cases
if (error?.status === 429) {
return new EmailSignupViewModel(email, 'Too many requests. Please try again later.', 'error');
}
if (error?.status === 409) {
return new EmailSignupViewModel(email, 'This email is already registered.', 'info');
}
return new EmailSignupViewModel(email, 'Something broke. Try again?', 'error');
}
}
}

View File

@@ -31,6 +31,7 @@ export class LeagueSettingsService {
id: leagueDto.id,
name: leagueDto.name,
ownerId: leagueDto.ownerId,
createdAt: leagueDto.createdAt || new Date().toISOString(),
};
// Get config
@@ -101,4 +102,4 @@ export class LeagueSettingsService {
throw error;
}
}
}
}

View File

@@ -29,7 +29,12 @@ export class AvatarService {
*/
async getAvatar(driverId: string): Promise<AvatarViewModel> {
const dto = await this.apiClient.getAvatar(driverId);
return new AvatarViewModel(dto);
// Convert GetAvatarOutputDTO to AvatarDTO format
const avatarDto = {
driverId: driverId,
avatarUrl: dto.avatarUrl
};
return new AvatarViewModel(avatarDto);
}
/**

View File

@@ -0,0 +1,60 @@
import { MediaApiClient } from '@/lib/api/media/MediaApiClient';
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO';
import { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
import { ValidateFaceInputDTO } from '@/lib/types/generated/ValidateFaceInputDTO';
import { ValidateFaceOutputDTO } from '@/lib/types/generated/ValidateFaceOutputDTO';
import { RequestAvatarGenerationViewModel } from '@/lib/view-models/RequestAvatarGenerationViewModel';
import { CompleteOnboardingViewModel } from '@/lib/view-models/CompleteOnboardingViewModel';
import { AvatarGenerationViewModel } from '@/lib/view-models/AvatarGenerationViewModel';
/**
* Onboarding Service
*
* Handles the complete onboarding flow including avatar generation and profile creation.
*/
export class OnboardingService {
constructor(
private readonly mediaApiClient: MediaApiClient,
private readonly driversApiClient: DriversApiClient
) {}
/**
* Validate face photo using the API
*/
async validateFacePhoto(photoData: string): Promise<{ isValid: boolean; errorMessage?: string }> {
const input: ValidateFaceInputDTO = { imageData: photoData };
const dto: ValidateFaceOutputDTO = await this.mediaApiClient.validateFacePhoto(input);
return { isValid: dto.isValid, errorMessage: dto.errorMessage };
}
/**
* Generate avatars based on face photo and suit color
* This method wraps the API call and returns a ViewModel
*/
async generateAvatars(
userId: string,
facePhotoData: string,
suitColor: string
): Promise<AvatarGenerationViewModel> {
const input: RequestAvatarGenerationInputDTO = {
userId,
facePhotoData,
suitColor,
};
const dto: RequestAvatarGenerationOutputDTO = await this.mediaApiClient.requestAvatarGeneration(input);
return new AvatarGenerationViewModel(dto);
}
/**
* Complete onboarding process
*/
async completeOnboarding(
input: CompleteOnboardingInputDTO
): Promise<CompleteOnboardingViewModel> {
const dto = await this.driversApiClient.completeOnboarding(input);
return new CompleteOnboardingViewModel(dto);
}
}

View File

@@ -16,8 +16,22 @@ export class WalletService {
/**
* Get wallet by driver ID with view model transformation
*/
async getWallet(driverId: string): Promise<WalletViewModel> {
const { wallet, transactions } = await this.apiClient.getWallet(driverId);
return new WalletViewModel({ ...wallet, transactions: transactions as FullTransactionDto[] });
async getWallet(leagueId?: string): Promise<WalletViewModel> {
const { wallet, transactions } = await this.apiClient.getWallet({ leagueId });
// Convert TransactionDTO to FullTransactionDto format
const convertedTransactions: FullTransactionDto[] = transactions.map(t => ({
id: t.id,
type: t.type as 'sponsorship' | 'membership' | 'withdrawal' | 'prize',
description: t.description,
amount: t.amount,
fee: t.amount * 0.05, // Calculate fee (5%)
netAmount: t.amount * 0.95, // Calculate net amount
date: new Date(t.createdAt),
status: 'completed',
referenceId: t.referenceId
}));
return new WalletViewModel({ ...wallet, transactions: convertedTransactions });
}
}

View File

@@ -2,7 +2,11 @@ import { ProtestsApiClient } from '../../api/protests/ProtestsApiClient';
import { ProtestViewModel } from '../../view-models/ProtestViewModel';
import { RaceViewModel } from '../../view-models/RaceViewModel';
import { ProtestDriverViewModel } from '../../view-models/ProtestDriverViewModel';
import type { LeagueAdminProtestsDTO, ApplyPenaltyCommandDTO, RequestProtestDefenseCommandDTO, DriverSummaryDTO } from '../../types';
import type { LeagueAdminProtestsDTO } from '../../types/generated/LeagueAdminProtestsDTO';
import type { ApplyPenaltyCommandDTO } from '../../types/generated/ApplyPenaltyCommandDTO';
import type { RequestProtestDefenseCommandDTO } from '../../types/generated/RequestProtestDefenseCommandDTO';
import type { ReviewProtestCommandDTO } from '../../types/generated/ReviewProtestCommandDTO';
import type { DriverDTO } from '../../types/generated/DriverDTO';
/**
* Protest Service
@@ -45,8 +49,11 @@ export class ProtestService {
if (!protest) return null;
const race = Object.values(dto.racesById)[0];
const protestingDriver = dto.driversById[protest.protestingDriverId];
const accusedDriver = dto.driversById[protest.accusedDriverId];
// Cast to the correct type for indexing
const driversById = dto.driversById as unknown as Record<string, DriverDTO>;
const protestingDriver = driversById[protest.protestingDriverId];
const accusedDriver = driversById[protest.accusedDriverId];
return {
protest: new ProtestViewModel(protest),
@@ -74,7 +81,14 @@ export class ProtestService {
* Review protest
*/
async reviewProtest(input: { protestId: string; stewardId: string; decision: string; decisionNotes: string }): Promise<void> {
await this.apiClient.reviewProtest(input);
const command: ReviewProtestCommandDTO = {
protestId: input.protestId,
stewardId: input.stewardId,
enum: input.decision === 'uphold' ? 'uphold' : 'dismiss',
decision: input.decision,
decisionNotes: input.decisionNotes
};
await this.apiClient.reviewProtest(command);
}
/**

View File

@@ -27,10 +27,44 @@ export class RaceStewardingService {
this.penaltiesApiClient.getRacePenalties(raceId),
]);
// Convert API responses to match RaceStewardingViewModel expectations
const convertedProtests = {
protests: protests.protests.map(p => ({
id: p.id,
protestingDriverId: p.protestingDriverId,
accusedDriverId: p.accusedDriverId,
incident: {
lap: p.lap,
description: p.description
},
filedAt: p.filedAt,
status: p.status
})),
driverMap: Object.entries(protests.driverMap).reduce((acc, [id, name]) => {
acc[id] = { id, name: name as string };
return acc;
}, {} as Record<string, { id: string; name: string }>)
};
const convertedPenalties = {
penalties: penalties.penalties.map(p => ({
id: p.id,
driverId: p.driverId,
type: p.type,
value: p.value,
reason: p.reason,
notes: p.notes
})),
driverMap: Object.entries(penalties.driverMap).reduce((acc, [id, name]) => {
acc[id] = { id, name: name as string };
return acc;
}, {} as Record<string, { id: string; name: string }>)
};
return new RaceStewardingViewModel({
raceDetail,
protests,
penalties,
protests: convertedProtests,
penalties: convertedPenalties,
});
}
}

View File

@@ -1,8 +1,9 @@
import type { SponsorsApiClient, CreateSponsorOutputDto, GetEntitySponsorshipPricingResultDto, SponsorDTO } from '../../api/sponsors/SponsorsApiClient';
import type { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient';
import { SponsorViewModel } from '../../view-models/SponsorViewModel';
import { SponsorDashboardViewModel } from '../../view-models/SponsorDashboardViewModel';
import { SponsorSponsorshipsViewModel } from '../../view-models/SponsorSponsorshipsViewModel';
import type { CreateSponsorInputDTO } from '../../types/generated/CreateSponsorInputDTO';
import type { SponsorDTO } from '../../types/generated/SponsorDTO';
/**
* Sponsor Service
@@ -48,14 +49,14 @@ export class SponsorService {
/**
* Create a new sponsor
*/
async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorOutputDto> {
async createSponsor(input: CreateSponsorInputDTO): Promise<any> {
return await this.apiClient.create(input);
}
/**
* Get sponsorship pricing
*/
async getSponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
async getSponsorshipPricing(): Promise<any> {
return await this.apiClient.getPricing();
}

View File

@@ -1,5 +1,6 @@
import { TeamJoinRequestViewModel, type TeamJoinRequestDTO } from '@/lib/view-models/TeamJoinRequestViewModel';
import { TeamJoinRequestViewModel } from '@/lib/view-models/TeamJoinRequestViewModel';
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
import type { TeamJoinRequestDTO } from '../../types/generated/TeamJoinRequestDTO';
// Wrapper for the team join requests collection returned by the teams API in this build
// Mirrors the current API response shape until a generated DTO is available.