refactor page to use services

This commit is contained in:
2025-12-18 17:02:48 +01:00
parent fc386db06a
commit 9814d9682c
27 changed files with 434 additions and 282 deletions

View File

@@ -0,0 +1,101 @@
'use client';
import React, { createContext, useContext, useMemo, ReactNode } from 'react';
import { ServiceFactory } from './ServiceFactory';
// Import all service types
import { RaceService } from './races/RaceService';
import { RaceResultsService } from './races/RaceResultsService';
import { DriverService } from './drivers/DriverService';
import { DriverRegistrationService } from './drivers/DriverRegistrationService';
import { TeamService } from './teams/TeamService';
import { TeamJoinService } from './teams/TeamJoinService';
import { LeagueService } from './leagues/LeagueService';
import { LeagueMembershipService } from './leagues/LeagueMembershipService';
import { LeagueSettingsService } from './leagues/LeagueSettingsService';
import { SponsorService } from './sponsors/SponsorService';
import { SponsorshipService } from './sponsors/SponsorshipService';
import { PaymentService } from './payments/PaymentService';
import { AnalyticsService } from './analytics/AnalyticsService';
import { DashboardService } from './dashboard/DashboardService';
import { MediaService } from './media/MediaService';
import { AvatarService } from './media/AvatarService';
import { WalletService } from './payments/WalletService';
import { MembershipFeeService } from './payments/MembershipFeeService';
import { AuthService } from './auth/AuthService';
import { SessionService } from './auth/SessionService';
import { ProtestService } from './protests/ProtestService';
export interface Services {
raceService: RaceService;
raceResultsService: RaceResultsService;
driverService: DriverService;
driverRegistrationService: DriverRegistrationService;
teamService: TeamService;
teamJoinService: TeamJoinService;
leagueService: LeagueService;
leagueMembershipService: LeagueMembershipService;
leagueSettingsService: LeagueSettingsService;
sponsorService: SponsorService;
sponsorshipService: SponsorshipService;
paymentService: PaymentService;
analyticsService: AnalyticsService;
dashboardService: DashboardService;
mediaService: MediaService;
avatarService: AvatarService;
walletService: WalletService;
membershipFeeService: MembershipFeeService;
authService: AuthService;
sessionService: SessionService;
protestService: ProtestService;
}
const ServicesContext = createContext<Services | null>(null);
interface ServiceProviderProps {
children: ReactNode;
}
export function ServiceProvider({ children }: ServiceProviderProps) {
const services = useMemo(() => {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
return {
raceService: serviceFactory.createRaceService(),
raceResultsService: serviceFactory.createRaceResultsService(),
driverService: serviceFactory.createDriverService(),
driverRegistrationService: serviceFactory.createDriverRegistrationService(),
teamService: serviceFactory.createTeamService(),
teamJoinService: serviceFactory.createTeamJoinService(),
leagueService: serviceFactory.createLeagueService(),
leagueMembershipService: serviceFactory.createLeagueMembershipService(),
leagueSettingsService: serviceFactory.createLeagueSettingsService(),
sponsorService: serviceFactory.createSponsorService(),
sponsorshipService: serviceFactory.createSponsorshipService(),
paymentService: serviceFactory.createPaymentService(),
analyticsService: serviceFactory.createAnalyticsService(),
dashboardService: serviceFactory.createDashboardService(),
mediaService: serviceFactory.createMediaService(),
avatarService: serviceFactory.createAvatarService(),
walletService: serviceFactory.createWalletService(),
membershipFeeService: serviceFactory.createMembershipFeeService(),
authService: serviceFactory.createAuthService(),
sessionService: serviceFactory.createSessionService(),
protestService: serviceFactory.createProtestService(),
};
}, []);
return (
<ServicesContext.Provider value={services}>
{children}
</ServicesContext.Provider>
);
}
export function useServices(): Services {
const services = useContext(ServicesContext);
if (!services) {
throw new Error('useServices must be used within a ServiceProvider');
}
return services;
}

View File

@@ -7,6 +7,10 @@ export class LeagueMembershipService {
// In-memory cache for memberships (populated via API calls)
private static leagueMemberships = new Map<string, LeagueMembership[]>();
constructor() {
// Constructor for dependency injection, but this service uses static methods
}
/**
* Get a specific membership from cache.
*/
@@ -65,4 +69,41 @@ export class LeagueMembershipService {
static getCachedMembershipsIterator(): IterableIterator<[string, LeagueMembership[]]> {
return this.leagueMemberships.entries();
}
// Instance methods that delegate to static methods for consistency with service pattern
/**
* Get a specific membership from cache.
*/
getMembership(leagueId: string, driverId: string): LeagueMembership | null {
return LeagueMembershipService.getMembership(leagueId, driverId);
}
/**
* Get all members of a league from cache.
*/
getLeagueMembers(leagueId: string): LeagueMembership[] {
return LeagueMembershipService.getLeagueMembers(leagueId);
}
/**
* Fetch and cache memberships for a league via API.
*/
async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
return LeagueMembershipService.fetchLeagueMemberships(leagueId);
}
/**
* Set memberships in cache (for use after API calls).
*/
setLeagueMemberships(leagueId: string, memberships: LeagueMembership[]): void {
LeagueMembershipService.setLeagueMemberships(leagueId, memberships);
}
/**
* Clear cached memberships for a league.
*/
clearLeagueMemberships(leagueId: string): void {
LeagueMembershipService.clearLeagueMemberships(leagueId);
}
}

View File

@@ -26,8 +26,8 @@ export class SponsorshipService {
}
/**
* Get sponsor sponsorships with view model transformation
*/
* Get sponsor sponsorships with view model transformation
*/
async getSponsorSponsorships(sponsorId: string): Promise<SponsorSponsorshipsViewModel | null> {
const dto = await this.apiClient.getSponsorships(sponsorId);
if (!dto) {
@@ -35,4 +35,13 @@ export class SponsorshipService {
}
return new SponsorSponsorshipsViewModel(dto);
}
/**
* Get pending sponsorship requests for an entity
*/
async getPendingSponsorshipRequests(params: { entityType: string; entityId: string }): Promise<{ requests: any[] }> {
// TODO: Implement API call
// For now, return empty
return { requests: [] };
}
}

View File

@@ -11,11 +11,12 @@ export class DriverLeaderboardItemViewModel {
podiums: number;
isActive: boolean;
rank: number;
avatarUrl: string;
position: number;
private previousRating?: number;
constructor(dto: DriverLeaderboardItemDTO, position: number, previousRating?: number) {
constructor(dto: DriverLeaderboardItemDTO & { avatarUrl: string }, position: number, previousRating?: number) {
this.id = dto.id;
this.name = dto.name;
this.rating = dto.rating;
@@ -26,6 +27,7 @@ export class DriverLeaderboardItemViewModel {
this.podiums = dto.podiums;
this.isActive = dto.isActive;
this.rank = dto.rank;
this.avatarUrl = dto.avatarUrl;
this.position = position;
this.previousRating = previousRating;
}

View File

@@ -1,67 +1,23 @@
import { LeagueSummaryDTO } from '../types/generated/LeagueSummaryDTO';
export class LeagueSummaryViewModel {
export interface LeagueSummaryViewModel {
id: string;
name: string;
constructor(dto: LeagueSummaryDTO) {
this.id = dto.id;
this.name = dto.name;
}
// Note: The generated DTO only has id and name
// These fields will need to be added when the OpenAPI spec is updated
description?: string;
logoUrl?: string;
coverImage?: string;
memberCount: number = 0;
maxMembers: number = 0;
isPublic: boolean = false;
ownerId: string = '';
ownerName?: string;
scoringType?: string;
status?: string;
/** UI-specific: Formatted capacity display */
get formattedCapacity(): string {
return `${this.memberCount}/${this.maxMembers}`;
}
/** UI-specific: Capacity bar percentage */
get capacityBarPercent(): number {
return (this.memberCount / this.maxMembers) * 100;
}
/** UI-specific: Label for join button */
get joinButtonLabel(): string {
if (this.isFull) return 'Full';
return this.isJoinable ? 'Join League' : 'Request to Join';
}
/** UI-specific: Whether the league is full */
get isFull(): boolean {
return this.memberCount >= this.maxMembers;
}
/** UI-specific: Whether the league is joinable */
get isJoinable(): boolean {
return this.isPublic && !this.isFull;
}
/** UI-specific: Color for member progress */
get memberProgressColor(): string {
const percent = this.capacityBarPercent;
if (percent < 50) return 'green';
if (percent < 80) return 'yellow';
return 'red';
}
/** UI-specific: Badge variant for status */
get statusBadgeVariant(): string {
switch (this.status) {
case 'active': return 'success';
case 'inactive': return 'secondary';
default: return 'default';
}
}
description: string;
ownerId: string;
createdAt: string;
maxDrivers: number;
usedDriverSlots: number;
maxTeams?: number;
usedTeamSlots?: number;
structureSummary: string;
scoringPatternSummary?: string;
timingSummary: string;
scoring?: {
gameId: string;
gameName: string;
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
scoringPresetId: string;
scoringPresetName: string;
dropPolicySummary: string;
scoringPatternSummary: string;
};
}

View File

@@ -0,0 +1,100 @@
export interface ProfileOverviewDriverSummaryViewModel {
id: string;
name: string;
country: string;
avatarUrl: string;
iracingId: string | null;
joinedAt: string;
rating: number | null;
globalRank: number | null;
consistency: number | null;
bio: string | null;
totalDrivers: number | null;
}
export interface ProfileOverviewStatsViewModel {
totalRaces: number;
wins: number;
podiums: number;
dnfs: number;
avgFinish: number | null;
bestFinish: number | null;
worstFinish: number | null;
finishRate: number | null;
winRate: number | null;
podiumRate: number | null;
percentile: number | null;
rating: number | null;
consistency: number | null;
overallRank: number | null;
}
export interface ProfileOverviewFinishDistributionViewModel {
totalRaces: number;
wins: number;
podiums: number;
topTen: number;
dnfs: number;
other: number;
}
export interface ProfileOverviewTeamMembershipViewModel {
teamId: string;
teamName: string;
teamTag: string | null;
role: string;
joinedAt: string;
isCurrent: boolean;
}
export interface ProfileOverviewSocialFriendSummaryViewModel {
id: string;
name: string;
country: string;
avatarUrl: string;
}
export interface ProfileOverviewSocialSummaryViewModel {
friendsCount: number;
friends: ProfileOverviewSocialFriendSummaryViewModel[];
}
export type ProfileOverviewSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
export type ProfileOverviewAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary';
export interface ProfileOverviewAchievementViewModel {
id: string;
title: string;
description: string;
icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
rarity: ProfileOverviewAchievementRarity;
earnedAt: string;
}
export interface ProfileOverviewSocialHandleViewModel {
platform: ProfileOverviewSocialPlatform;
handle: string;
url: string;
}
export interface ProfileOverviewExtendedProfileViewModel {
socialHandles: ProfileOverviewSocialHandleViewModel[];
achievements: ProfileOverviewAchievementViewModel[];
racingStyle: string;
favoriteTrack: string;
favoriteCar: string;
timezone: string;
availableHours: string;
lookingForTeam: boolean;
openToRequests: boolean;
}
export interface ProfileOverviewViewModel {
currentDriver: ProfileOverviewDriverSummaryViewModel | null;
stats: ProfileOverviewStatsViewModel | null;
finishDistribution: ProfileOverviewFinishDistributionViewModel | null;
teamMemberships: ProfileOverviewTeamMembershipViewModel[];
socialSummary: ProfileOverviewSocialSummaryViewModel;
extendedProfile: ProfileOverviewExtendedProfileViewModel | null;
}

View File

@@ -13,15 +13,31 @@ export class TeamSummaryViewModel {
logoUrl?: string;
memberCount: number;
rating: number;
description?: string;
totalWins: number = 0;
totalRaces: number = 0;
performanceLevel: string = '';
isRecruiting: boolean = false;
specialization?: string;
region?: string;
languages: string[] = [];
private maxMembers = 10; // Assuming max members
constructor(dto: TeamSummaryDTO) {
constructor(dto: TeamSummaryDTO & { description?: string; totalWins?: number; totalRaces?: number; performanceLevel?: string; isRecruiting?: boolean; specialization?: string; region?: string; languages?: string[] }) {
this.id = dto.id;
this.name = dto.name;
if (dto.logoUrl !== undefined) this.logoUrl = dto.logoUrl;
this.memberCount = dto.memberCount;
this.rating = dto.rating;
this.description = dto.description;
this.totalWins = dto.totalWins ?? 0;
this.totalRaces = dto.totalRaces ?? 0;
this.performanceLevel = dto.performanceLevel ?? '';
this.isRecruiting = dto.isRecruiting ?? false;
this.specialization = dto.specialization;
this.region = dto.region;
this.languages = dto.languages ?? [];
}
/** UI-specific: Whether team is full */