website cleanup
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
60
apps/website/lib/services/onboarding/OnboardingService.ts
Normal file
60
apps/website/lib/services/onboarding/OnboardingService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user