website refactor

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

View File

@@ -1,166 +1,130 @@
import { ApiClient } from '@/lib/api';
import type { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
import { Result } from '@/lib/contracts/Result';
import { Service, type DomainError } from '@/lib/contracts/services/Service';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { isProductionEnvironment } from '@/lib/config/env';
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
import type { LeagueMembershipDTO } from '@/lib/types/generated/LeagueMembershipDTO';
let cachedLeaguesApiClient: LeaguesApiClient | undefined;
function getDefaultLeaguesApiClient(): LeaguesApiClient {
if (cachedLeaguesApiClient) return cachedLeaguesApiClient;
const api = new ApiClient(getWebsiteApiBaseUrl());
cachedLeaguesApiClient = api.leagues;
return cachedLeaguesApiClient;
export interface LeagueRosterAdminData {
leagueId: string;
members: LeagueRosterMemberDTO[];
joinRequests: LeagueRosterJoinRequestDTO[];
}
export class LeagueMembershipService {
// In-memory cache for memberships (populated via API calls)
private static leagueMemberships = new Map<string, LeagueMembership[]>();
export class LeagueMembershipService implements Service {
private apiClient: LeaguesApiClient;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private static cachedMemberships = new Map<string, any[]>();
constructor(private readonly leaguesApiClient?: LeaguesApiClient) {}
private getClient(): LeaguesApiClient {
return this.leaguesApiClient ?? getDefaultLeaguesApiClient();
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
}
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMembershipsDTO> {
const dto = await this.getClient().getMemberships(leagueId);
return dto;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static getMembership(leagueId: string, driverId: string): any | null {
const members = this.cachedMemberships.get(leagueId);
if (!members) return null;
return members.find(m => m.driverId === driverId) || null;
}
async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> {
return this.getClient().removeMember(leagueId, performerDriverId, targetDriverId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static getLeagueMembers(leagueId: string): any[] {
return this.cachedMemberships.get(leagueId) || [];
}
/**
* Get a specific membership from cache.
*/
static getMembership(leagueId: string, driverId: string): LeagueMembership | null {
const list = this.leagueMemberships.get(leagueId);
if (!list) return null;
return list.find((m) => m.driverId === driverId) ?? null;
}
/**
* Get all members of a league from cache.
*/
static getLeagueMembers(leagueId: string): LeagueMembership[] {
return [...(this.leagueMemberships.get(leagueId) ?? [])];
}
/**
* Fetch and cache memberships for a league via API.
*/
static async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
try {
const result = await getDefaultLeaguesApiClient().getMemberships(leagueId);
const memberships: LeagueMembership[] = (result.members ?? []).map((member) => ({
id: `${member.driverId}-${leagueId}`, // Generate ID since API doesn't provide it
leagueId,
driverId: member.driverId,
role: member.role as 'owner' | 'admin' | 'steward' | 'member',
status: 'active', // Assume active since API returns current members
joinedAt: member.joinedAt,
}));
this.setLeagueMemberships(leagueId, memberships);
return memberships;
} catch (error) {
console.error('Failed to fetch league memberships:', error);
return [];
}
}
/**
* Join a league.
*
* NOTE: The API currently exposes membership mutations via league member management endpoints.
* For now we keep the website decoupled by consuming only the API through this service.
*/
async joinLeague(leagueId: string, driverId: string): Promise<void> {
// Temporary: no join endpoint exposed yet in API.
// Keep behavior predictable for UI.
throw new Error('Joining leagues is not available in this build.');
}
/**
* Leave a league.
*/
async leaveLeague(leagueId: string, driverId: string): Promise<void> {
// Temporary: no leave endpoint exposed yet in API.
throw new Error('Leaving leagues is not available in this build.');
}
/**
* Set memberships in cache (for use after API calls).
*/
static setLeagueMemberships(leagueId: string, memberships: LeagueMembership[]): void {
this.leagueMemberships.set(leagueId, memberships);
}
/**
* Clear cached memberships for a league.
*/
static clearLeagueMemberships(leagueId: string): void {
this.leagueMemberships.delete(leagueId);
}
/**
* Get iterator for cached memberships (for utility functions).
*/
static getCachedMembershipsIterator(): IterableIterator<[string, LeagueMembership[]]> {
return this.leagueMemberships.entries();
}
/**
* Get all memberships for a specific driver across all leagues.
*/
static getAllMembershipsForDriver(driverId: string): LeagueMembership[] {
const allMemberships: LeagueMembership[] = [];
for (const [leagueId, memberships] of this.leagueMemberships.entries()) {
const driverMembership = memberships.find((m) => m.driverId === driverId);
if (driverMembership) {
allMemberships.push(driverMembership);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static getAllMembershipsForDriver(driverId: string): any[] {
const allMemberships: any[] = [];
for (const [leagueId, members] of this.cachedMemberships.entries()) {
const membership = members.find(m => m.driverId === driverId);
if (membership) {
allMemberships.push({ ...membership, leagueId });
}
}
return allMemberships;
}
// Instance methods that delegate to static methods for consistency with service pattern
async fetchLeagueMemberships(leagueId: string): Promise<void> {
try {
const members = await this.apiClient.getMemberships(leagueId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
LeagueMembershipService.cachedMemberships.set(leagueId, (members as any).members || []);
} catch (error) {
console.error('Failed to fetch memberships', error);
}
}
/**
* Get a specific membership from cache.
*/
getMembership(leagueId: string, driverId: string): LeagueMembership | null {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getMembership(leagueId: string, driverId: string): any | null {
return LeagueMembershipService.getMembership(leagueId, driverId);
}
/**
* Get all members of a league from cache.
*/
getLeagueMembers(leagueId: string): LeagueMembership[] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getLeagueMembers(leagueId: string): any[] {
return LeagueMembershipService.getLeagueMembers(leagueId);
}
/**
* Fetch and cache memberships for a league via API.
*/
async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
return LeagueMembershipService.fetchLeagueMemberships(leagueId);
async joinLeague(_: string, __: string): Promise<Result<void, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'joinLeague' });
}
/**
* Set memberships in cache (for use after API calls).
*/
setLeagueMemberships(leagueId: string, memberships: LeagueMembership[]): void {
LeagueMembershipService.setLeagueMemberships(leagueId, memberships);
async leaveLeague(_: string, __: string): Promise<Result<void, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'leaveLeague' });
}
/**
* Clear cached memberships for a league.
*/
clearLeagueMemberships(leagueId: string): void {
LeagueMembershipService.clearLeagueMemberships(leagueId);
async getRosterAdminData(leagueId: string): Promise<Result<LeagueRosterAdminData, DomainError>> {
try {
const [members, joinRequests] = await Promise.all([
this.apiClient.getAdminRosterMembers(leagueId),
this.apiClient.getAdminRosterJoinRequests(leagueId),
]);
return Result.ok({
leagueId,
members,
joinRequests,
});
} catch (error: unknown) {
console.error('LeagueMembershipService.getRosterAdminData failed:', error);
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster data' });
}
}
}
async removeMember(leagueId: string, targetDriverId: string): Promise<Result<{ success: boolean }, DomainError>> {
try {
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
return Result.ok({ success: dto.success });
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to remove member' });
}
}
async approveJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
try {
const dto = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId);
return Result.ok({ success: dto.success });
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to approve join request' });
}
}
async rejectJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
try {
const dto = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId);
return Result.ok({ success: dto.success });
} catch (error: unknown) {
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to reject join request' });
}
}
}