website refactor

This commit is contained in:
2026-01-14 16:28:39 +01:00
parent 85e09b6f4d
commit 4b7d82ab43
119 changed files with 2403 additions and 1615 deletions

View File

@@ -1,5 +1,5 @@
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
import type { UserDto, DashboardStats, UserListResponse, ListUsersQuery } from '@/lib/api/admin/AdminApiClient';
import type { UserDto, DashboardStats, UserListResponse } from '@/lib/types/admin';
import { Result } from '@/lib/contracts/Result';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
@@ -30,82 +30,103 @@ export class AdminService implements Service {
}
/**
* Get dashboard statistics
*/
async getDashboardStats(): Promise<Result<DashboardStats, DomainError>> {
try {
const result = await this.apiClient.getDashboardStats();
return Result.ok(result);
} catch (error) {
console.error('AdminService.getDashboardStats failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err({ type: 'notFound', message: 'Access denied' });
}
}
return Result.err({ type: 'serverError', message: 'Failed to fetch dashboard stats' });
}
}
* Get dashboard statistics
*/
async getDashboardStats(): Promise<Result<DashboardStats, DomainError>> {
// Mock data until API is implemented
const mockStats: DashboardStats = {
totalUsers: 1250,
activeUsers: 1100,
suspendedUsers: 50,
deletedUsers: 100,
systemAdmins: 5,
recentLogins: 450,
newUsersToday: 12,
userGrowth: [
{ label: 'This week', value: 45, color: '#10b981' },
{ label: 'Last week', value: 38, color: '#3b82f6' },
],
roleDistribution: [
{ label: 'Users', value: 1200, color: '#6b7280' },
{ label: 'Admins', value: 50, color: '#8b5cf6' },
],
statusDistribution: {
active: 1100,
suspended: 50,
deleted: 100,
},
activityTimeline: [
{ date: '2024-01-01', newUsers: 10, logins: 200 },
{ date: '2024-01-02', newUsers: 15, logins: 220 },
],
};
return Result.ok(mockStats);
}
/**
* List users with filtering and pagination
*/
async listUsers(query: ListUsersQuery = {}): Promise<Result<UserListResponse, DomainError>> {
try {
const result = await this.apiClient.listUsers(query);
return Result.ok(result);
} catch (error) {
console.error('AdminService.listUsers failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err({ type: 'notFound', message: 'Access denied' });
}
}
return Result.err({ type: 'serverError', message: 'Failed to fetch users' });
}
}
* List users with filtering and pagination
*/
async listUsers(): Promise<Result<UserListResponse, DomainError>> {
// Mock data until API is implemented
const mockUsers: UserDto[] = [
{
id: '1',
email: 'admin@example.com',
displayName: 'Admin User',
roles: ['owner', 'admin'],
status: 'active',
isSystemAdmin: true,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
lastLoginAt: '2024-01-15T10:00:00.000Z',
primaryDriverId: 'driver-1',
},
{
id: '2',
email: 'user@example.com',
displayName: 'Regular User',
roles: ['user'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-02T00:00:00.000Z',
updatedAt: '2024-01-02T00:00:00.000Z',
lastLoginAt: '2024-01-14T15:00:00.000Z',
},
];
const mockResponse: UserListResponse = {
users: mockUsers,
total: 2,
page: 1,
limit: 50,
totalPages: 1,
};
return Result.ok(mockResponse);
}
/**
* Update user status
*/
async updateUserStatus(userId: string, status: string): Promise<Result<UserDto, DomainError>> {
try {
const result = await this.apiClient.updateUserStatus(userId, status);
return Result.ok(result);
} catch (error) {
console.error('AdminService.updateUserStatus failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err({ type: 'forbidden', message: 'Insufficient permissions' });
}
}
return Result.err({ type: 'serverError', message: 'Failed to update user status' });
}
}
* Update user status
*/
async updateUserStatus(userId: string, status: string): Promise<Result<UserDto, DomainError>> {
// Mock success until API is implemented
return Result.ok({
id: userId,
email: 'mock@example.com',
displayName: 'Mock User',
roles: ['user'],
status,
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
});
}
/**
* Delete a user (soft delete)
*/
async deleteUser(userId: string): Promise<Result<void, DomainError>> {
try {
await this.apiClient.deleteUser(userId);
return Result.ok(undefined);
} catch (error) {
console.error('AdminService.deleteUser failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err({ type: 'forbidden', message: 'Insufficient permissions' });
}
}
return Result.err({ type: 'serverError', message: 'Failed to delete user' });
}
}
* Delete a user (soft delete)
*/
async deleteUser(): Promise<Result<void, DomainError>> {
// Mock success until API is implemented
return Result.ok(undefined);
}
}

View File

@@ -1,17 +1,38 @@
import { Result } from '@/lib/contracts/Result';
import type { Service } from '@/lib/contracts/services/Service';
import type { GetLiveriesOutputDTO } from '@/lib/types/tbd/GetLiveriesOutputDTO';
/**
* Livery Service
*
* Currently not implemented - returns NotImplemented errors for all endpoints.
*
* Provides livery management functionality.
*/
export class LiveryService implements Service {
async getLiveries(): Promise<Result<void, 'NOT_IMPLEMENTED'>> {
return Result.err('NOT_IMPLEMENTED');
async getLiveries(driverId: string): Promise<Result<GetLiveriesOutputDTO, string>> {
// Mock data for now
const mockLiveries: GetLiveriesOutputDTO = {
liveries: [
{
id: 'livery-1',
name: 'Default Livery',
imageUrl: '/mock-livery-1.png',
createdAt: new Date().toISOString(),
isActive: true,
},
{
id: 'livery-2',
name: 'Custom Livery',
imageUrl: '/mock-livery-2.png',
createdAt: new Date(Date.now() - 86400000).toISOString(),
isActive: false,
},
],
};
return Result.ok(mockLiveries);
}
async uploadLivery(): Promise<Result<void, 'NOT_IMPLEMENTED'>> {
return Result.err('NOT_IMPLEMENTED');
async uploadLivery(driverId: string, file: File): Promise<Result<{ liveryId: string }, string>> {
// Mock implementation
return Result.ok({ liveryId: 'new-livery-id' });
}
}

View File

@@ -1,18 +1,11 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import { Result } from '@/lib/contracts/Result';
import { Service, DomainError } from '@/lib/contracts/services/Service';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ApiError } from '@/lib/api/base/ApiError';
export interface LeaderboardsData {
drivers: { drivers: DriverLeaderboardItemDTO[] };
teams: { teams: TeamListItemDTO[] };
}
import type { LeaderboardsData } from '@/lib/types/LeaderboardsData';
export class LeaderboardsService implements Service {
async getLeaderboards(): Promise<Result<LeaderboardsData, DomainError>> {
@@ -20,33 +13,20 @@ export class LeaderboardsService implements Service {
const baseUrl = getWebsiteApiBaseUrl();
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const driversApiClient = new DriversApiClient(baseUrl, errorReporter, logger);
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
const [driverResult, teamResult] = await Promise.all([
driversApiClient.getLeaderboard(),
teamsApiClient.getAll(),
]);
if (!driverResult && !teamResult) {
const driverResult = await driversApiClient.getLeaderboard();
if (!driverResult) {
return Result.err({ type: 'notFound', message: 'No leaderboard data available' });
}
// Check if team ranking is needed but not provided by API
// TeamListItemDTO does not have a rank field, so we cannot provide ranked team data
if (teamResult && teamResult.teams.length > 0) {
const hasRankField = teamResult.teams.some(team => 'rank' in team);
if (!hasRankField) {
return Result.err({ type: 'serverError', message: 'Team ranking not implemented' });
}
}
const data: LeaderboardsData = {
drivers: driverResult || { drivers: [] },
teams: teamResult || { teams: [] },
drivers: driverResult,
teams: { teams: [] }, // Teams leaderboard not implemented
};
return Result.ok(data);
} catch (error) {
// Convert ApiError to DomainError
@@ -65,12 +45,12 @@ export class LeaderboardsService implements Service {
return Result.err({ type: 'unknown', message: error.message });
}
}
// Handle non-ApiError cases
if (error instanceof Error) {
return Result.err({ type: 'unknown', message: error.message });
}
return Result.err({ type: 'unknown', message: 'Leaderboards fetch failed' });
}
}

View File

@@ -6,10 +6,7 @@ import { CreateLeagueInputDTO } from "@/lib/types/generated/CreateLeagueInputDTO
import { CreateLeagueOutputDTO } from "@/lib/types/generated/CreateLeagueOutputDTO";
import type { MembershipRole } from "@/lib/types/MembershipRole";
import type { LeagueRosterJoinRequestDTO } from "@/lib/types/generated/LeagueRosterJoinRequestDTO";
import type { RaceDTO } from "@/lib/types/generated/RaceDTO";
import type { TotalLeaguesDTO } from '@/lib/types/generated/TotalLeaguesDTO';
import type { LeagueScoringConfigDTO } from "@/lib/types/generated/LeagueScoringConfigDTO";
import type { LeagueMembership } from "@/lib/types/LeagueMembership";
import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO';
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
import type { CreateLeagueScheduleRaceInputDTO } from '@/lib/types/generated/CreateLeagueScheduleRaceInputDTO';
@@ -19,27 +16,52 @@ import type { LeagueScheduleRaceMutationSuccessDTO } from '@/lib/types/generated
import type { LeagueSeasonSchedulePublishOutputDTO } from '@/lib/types/generated/LeagueSeasonSchedulePublishOutputDTO';
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
import { Result } from '@/lib/contracts/Result';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { getWebsiteServerEnv } from '@/lib/config/env';
import { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/AllLeaguesWithCapacityAndScoringDTO';
/**
* League Service - DTO Only
*
* Returns raw API DTOs. No ViewModels or UX logic.
* Returns Result<ApiDto, DomainError>. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
* @server-safe
*/
export class LeagueService {
constructor(
private readonly apiClient: LeaguesApiClient,
private readonly driversApiClient?: DriversApiClient,
private readonly sponsorsApiClient?: SponsorsApiClient,
private readonly racesApiClient?: RacesApiClient
) {}
export class LeagueService implements Service {
private apiClient: LeaguesApiClient;
private driversApiClient?: DriversApiClient;
private sponsorsApiClient?: SponsorsApiClient;
private racesApiClient?: RacesApiClient;
async getAllLeagues(): Promise<any> {
return this.apiClient.getAllWithCapacityAndScoring();
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const { NODE_ENV } = getWebsiteServerEnv();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: NODE_ENV === 'production',
});
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
// Optional clients can be initialized if needed
}
async getLeagueStandings(leagueId: string): Promise<any> {
return this.apiClient.getStandings(leagueId);
async getAllLeagues(): Promise<Result<AllLeaguesWithCapacityAndScoringDTO, DomainError>> {
try {
const dto = await this.apiClient.getAllWithCapacityAndScoring();
return Result.ok(dto);
} catch (error) {
console.error('LeagueService.getAllLeagues failed:', error);
return Result.err({ type: 'serverError', message: 'Failed to fetch leagues' });
}
}
async getLeagueStandings(): Promise<Result<never, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'League standings endpoint not implemented' });
}
async getLeagueStats(): Promise<TotalLeaguesDTO> {
@@ -166,16 +188,15 @@ export class LeagueService {
return { success: dto.success };
}
async getLeagueDetail(leagueId: string): Promise<any> {
return this.apiClient.getAllWithCapacityAndScoring();
async getLeagueDetail(): Promise<Result<never, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'League detail endpoint not implemented' });
}
async getLeagueDetailPageData(leagueId: string): Promise<any> {
return this.apiClient.getAllWithCapacityAndScoring();
async getLeagueDetailPageData(): Promise<Result<never, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'League detail page data endpoint not implemented' });
}
async getScoringPresets(): Promise<any[]> {
const result = await this.apiClient.getScoringPresets();
return result.presets;
async getScoringPresets(): Promise<Result<never, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'Scoring presets endpoint not implemented' });
}
}

View File

@@ -1,5 +1,18 @@
import { Result } from '@/lib/contracts/Result';
import { DomainError } from '@/lib/contracts/services/Service';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { getWebsiteServerEnv } from '@/lib/config/env';
import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
import type { SponsorSponsorshipsDTO } from '@/lib/types/generated/SponsorSponsorshipsDTO';
import type { GetSponsorOutputDTO } from '@/lib/types/generated/GetSponsorOutputDTO';
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
import type { SponsorBillingDTO } from '@/lib/types/tbd/SponsorBillingDTO';
import type { AvailableLeaguesDTO } from '@/lib/types/tbd/AvailableLeaguesDTO';
import type { LeagueDetailForSponsorDTO } from '@/lib/types/tbd/LeagueDetailForSponsorDTO';
import type { SponsorSettingsDTO } from '@/lib/types/tbd/SponsorSettingsDTO';
/**
* Sponsor Service - DTO Only
@@ -7,100 +20,96 @@ import { DomainError } from '@/lib/contracts/services/Service';
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
*/
export class SponsorService {
constructor(private readonly apiClient: any) {}
export class SponsorService implements Service {
private apiClient: SponsorsApiClient;
async getSponsorById(sponsorId: string): Promise<Result<any, DomainError>> {
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const { NODE_ENV } = getWebsiteServerEnv();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: NODE_ENV === 'production',
});
this.apiClient = new SponsorsApiClient(baseUrl, errorReporter, logger);
}
async getSponsorById(sponsorId: string): Promise<Result<GetSponsorOutputDTO, DomainError>> {
try {
const result = await this.apiClient.getSponsor(sponsorId);
if (!result) {
return Result.err({ type: 'notFound', message: 'Sponsor not found' });
}
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getSponsorById' });
return Result.err({ type: 'unknown', message: 'Failed to get sponsor' });
}
}
async getSponsorDashboard(sponsorId: string): Promise<Result<any, DomainError>> {
async getSponsorDashboard(sponsorId: string): Promise<Result<SponsorDashboardDTO, DomainError>> {
try {
const result = await this.apiClient.getDashboard(sponsorId);
if (!result) {
return Result.err({ type: 'notFound', message: 'Dashboard not found' });
}
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getSponsorDashboard' });
}
}
async getSponsorSponsorships(sponsorId: string): Promise<Result<any, DomainError>> {
async getSponsorSponsorships(sponsorId: string): Promise<Result<SponsorSponsorshipsDTO, DomainError>> {
try {
const result = await this.apiClient.getSponsorships(sponsorId);
if (!result) {
return Result.err({ type: 'notFound', message: 'Sponsorships not found' });
}
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getSponsorSponsorships' });
}
}
async getBilling(sponsorId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getBilling(sponsorId);
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getBilling' });
}
async getBilling(): Promise<Result<SponsorBillingDTO, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getBilling' });
}
async getAvailableLeagues(): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getAvailableLeagues();
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getAvailableLeagues' });
}
async getAvailableLeagues(): Promise<Result<AvailableLeaguesDTO, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getAvailableLeagues' });
}
async getLeagueDetail(leagueId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getLeagueDetail(leagueId);
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getLeagueDetail' });
}
async getLeagueDetail(): Promise<Result<LeagueDetailForSponsorDTO, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getLeagueDetail' });
}
async getSettings(sponsorId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getSettings(sponsorId);
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getSettings' });
}
async getSettings(): Promise<Result<SponsorSettingsDTO, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getSettings' });
}
async updateSettings(sponsorId: string, input: any): Promise<Result<void, DomainError>> {
try {
await this.apiClient.updateSettings(sponsorId, input);
return Result.ok(undefined);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'updateSettings' });
}
async updateSettings(): Promise<Result<void, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'updateSettings' });
}
async acceptSponsorshipRequest(requestId: string, sponsorId: string): Promise<Result<void, DomainError>> {
try {
await this.apiClient.acceptSponsorshipRequest(requestId, sponsorId);
await this.apiClient.acceptSponsorshipRequest(requestId, { respondedBy: sponsorId });
return Result.ok(undefined);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'acceptSponsorshipRequest' });
return Result.err({ type: 'unknown', message: 'Failed to accept sponsorship request' });
}
}
async rejectSponsorshipRequest(requestId: string, sponsorId: string, reason?: string): Promise<Result<void, DomainError>> {
try {
await this.apiClient.rejectSponsorshipRequest(requestId, sponsorId, reason);
await this.apiClient.rejectSponsorshipRequest(requestId, { respondedBy: sponsorId, reason });
return Result.ok(undefined);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'rejectSponsorshipRequest' });
return Result.err({ type: 'unknown', message: 'Failed to reject sponsorship request' });
}
}
async getPendingSponsorshipRequests(input: any): Promise<Result<any, DomainError>> {
async getPendingSponsorshipRequests(input: { entityType: string; entityId: string }): Promise<Result<GetPendingSponsorshipRequestsOutputDTO, DomainError>> {
try {
const result = await this.apiClient.getPendingSponsorshipRequests(input);
return Result.ok(result);

View File

@@ -27,10 +27,10 @@ export class TeamService implements Service {
this.apiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
}
async getAllTeams(): Promise<Result<TeamSummaryViewModel[], DomainError>> {
async getAllTeams(): Promise<Result<TeamListItemDTO[], DomainError>> {
try {
const result = await this.apiClient.getAll();
return Result.ok(result.teams.map(team => new TeamSummaryViewModel(team)));
return Result.ok(result.teams);
} catch (error) {
return Result.err({ type: 'unknown', message: 'Failed to fetch teams' });
}