website refactor

This commit is contained in:
2026-01-12 19:24:59 +01:00
parent 1f0c4f7fa6
commit 5ea95eaf51
54 changed files with 2894 additions and 2342 deletions

View File

@@ -0,0 +1,26 @@
import { DashboardStats } from '@/lib/api/admin/AdminApiClient';
import { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
/**
* AdminDashboardViewDataBuilder
*
* Server-side builder that transforms API DashboardStats DTO
* directly into ViewData for the AdminDashboardTemplate.
*
* Deterministic, side-effect free.
*/
export class AdminDashboardViewDataBuilder {
static build(apiStats: DashboardStats): AdminDashboardViewData {
return {
stats: {
totalUsers: apiStats.totalUsers,
activeUsers: apiStats.activeUsers,
suspendedUsers: apiStats.suspendedUsers,
deletedUsers: apiStats.deletedUsers,
systemAdmins: apiStats.systemAdmins,
recentLogins: apiStats.recentLogins,
newUsersToday: apiStats.newUsersToday,
},
};
}
}

View File

@@ -0,0 +1,15 @@
'use client';
import { AdminDashboardPageDto } from '@/lib/page-queries/AdminDashboardPageQuery';
import { AdminDashboardViewModel } from '@/lib/view-models/AdminDashboardViewModel';
/**
* AdminDashboardViewModelBuilder
*
* Transforms AdminDashboardPageDto into AdminDashboardViewModel
*/
export class AdminDashboardViewModelBuilder {
static build(dto: AdminDashboardPageDto): AdminDashboardViewModel {
return new AdminDashboardViewModel(dto);
}
}

View File

@@ -0,0 +1,25 @@
/**
* ViewData Builder Contract
*
* Purpose: Transform ViewModels into ViewData for templates
*
* Rules:
* - Deterministic and side-effect free
* - No HTTP/API calls
* - Input: ViewModel
* - Output: ViewData (JSON-serializable)
* - Must be in lib/builders/view-data/
* - Must be named *ViewDataBuilder
* - Must have 'use client' directive
* - Must implement static build() method
*/
export interface ViewDataBuilder<TInput, TOutput> {
/**
* Transform ViewModel into ViewData
*
* @param viewModel - Client-side ViewModel
* @returns ViewData for template
*/
build(viewModel: TInput): TOutput;
}

View File

@@ -0,0 +1,25 @@
/**
* ViewModel Builder Contract
*
* Purpose: Transform API Transport DTOs into ViewModels
*
* Rules:
* - Deterministic and side-effect free
* - No HTTP/API calls
* - Input: API Transport DTO
* - Output: ViewModel
* - Must be in lib/builders/view-models/
* - Must be named *ViewModelBuilder
* - Must have 'use client' directive
* - Must implement static build() method
*/
export interface ViewModelBuilder<TInput, TOutput> {
/**
* Transform DTO into ViewModel
*
* @param dto - API Transport DTO
* @returns ViewModel
*/
build(dto: TInput): TOutput;
}

View File

@@ -0,0 +1,33 @@
/**
* Mutation Contract
*
* Purpose: Framework-agnostic write operations
*
* Rules:
* - Orchestrates services for writes
* - No HTTP/API calls directly
* - No 'use client' directive
* - No 'use server' directive
* - Must be in lib/mutations/
* - Must be named *Mutation
* - Can be called from Server Actions
* - Single responsibility: ONE operation per mutation
*
* Pattern:
* Server Action → Mutation → Service → API Client
*
* Design Principle:
* Each mutation does ONE thing. If you need multiple operations,
* create multiple mutation classes (e.g., UpdateUserStatusMutation, DeleteUserMutation).
* This follows the same pattern as Page Queries.
*/
export interface Mutation<TInput = void, TOutput = void> {
/**
* Execute the mutation
*
* @param input - Mutation input
* @returns Output (optional)
*/
execute(input: TInput): Promise<TOutput>;
}

View File

@@ -1,4 +1,4 @@
import { PageQueryResult } from "@/lib/page-queries/page-query-result/PageQueryResult";
import { PageQueryResult } from "./PageQueryResult";
/**

View File

@@ -0,0 +1,37 @@
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
import { AdminService } from '@/lib/services/admin/AdminService';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { Mutation } from '@/lib/contracts/mutations/Mutation';
/**
* DeleteUserMutation
*
* Framework-agnostic mutation for deleting users.
* Called from Server Actions.
*
* Input: { userId: string }
* Output: void
*
* Pattern: Server Action → Mutation → Service → API Client
*/
export class DeleteUserMutation implements Mutation<{ userId: string }, void> {
private service: AdminService;
constructor() {
// Manual DI for serverless
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
const apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
this.service = new AdminService(apiClient);
}
async execute(input: { userId: string }): Promise<void> {
await this.service.deleteUser(input.userId);
}
}

View File

@@ -0,0 +1,37 @@
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
import { AdminService } from '@/lib/services/admin/AdminService';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { Mutation } from '@/lib/contracts/mutations/Mutation';
/**
* UpdateUserStatusMutation
*
* Framework-agnostic mutation for updating user status.
* Called from Server Actions.
*
* Input: { userId: string; status: string }
* Output: void
*
* Pattern: Server Action → Mutation → Service → API Client
*/
export class UpdateUserStatusMutation implements Mutation<{ userId: string; status: string }, void> {
private service: AdminService;
constructor() {
// Manual DI for serverless
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
const apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
this.service = new AdminService(apiClient);
}
async execute(input: { userId: string; status: string }): Promise<void> {
await this.service.updateUserStatus(input.userId, input.status);
}
}

View File

@@ -0,0 +1,50 @@
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { AdminService } from '@/lib/services/admin/AdminService';
import { AdminDashboardViewDataBuilder } from '@/lib/builders/view-data/AdminDashboardViewDataBuilder';
import { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
/**
* AdminDashboardPageQuery
*
* Server-side composition for admin dashboard page.
* Fetches dashboard statistics from API and transforms to View Data using builders.
*
* Follows Clean Architecture: DTOs never leak into application code.
*/
export class AdminDashboardPageQuery implements PageQuery<AdminDashboardViewData, void> {
async execute(): Promise<PageQueryResult<AdminDashboardViewData>> {
try {
// Create required dependencies
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
const apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
const adminService = new AdminService(apiClient);
// Fetch dashboard stats (API DTO)
const apiDto = await adminService.getDashboardStats();
// Transform API DTO to View Data using builder
const viewData = AdminDashboardViewDataBuilder.build(apiDto);
return { status: 'ok', dto: viewData };
} catch (error) {
console.error('AdminDashboardPageQuery failed:', error);
if (error instanceof Error && (error.message.includes('403') || error.message.includes('401'))) {
return { status: 'notFound' };
}
return { status: 'error', errorId: 'admin_dashboard_fetch_failed' };
}
}
}

View File

@@ -0,0 +1,93 @@
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
import { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { AdminService } from '@/lib/services/admin/AdminService';
export interface AdminUsersPageDto {
users: Array<{
id: string;
email: string;
displayName: string;
roles: string[];
status: string;
isSystemAdmin: boolean;
createdAt: string;
updatedAt: string;
lastLoginAt?: string;
primaryDriverId?: string;
}>;
total: number;
page: number;
limit: number;
totalPages: number;
}
/**
* AdminUsersPageQuery
*
* Server-side composition for admin users page.
* Fetches user list from API with filtering and assembles Page DTO.
*/
export class AdminUsersPageQuery {
async execute(query: {
search?: string;
role?: string;
status?: string;
page?: number;
limit?: number;
}): Promise<PageQueryResult<AdminUsersPageDto>> {
try {
// Create required dependencies
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
const apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
const adminService = new AdminService(apiClient);
// Fetch user list via service
const apiDto = await adminService.listUsers({
search: query.search,
role: query.role,
status: query.status,
page: query.page || 1,
limit: query.limit || 50,
});
// Assemble Page DTO (raw values only)
const pageDto: AdminUsersPageDto = {
users: apiDto.users.map(user => ({
id: user.id,
email: user.email,
displayName: user.displayName,
roles: user.roles,
status: user.status,
isSystemAdmin: user.isSystemAdmin,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
lastLoginAt: user.lastLoginAt?.toISOString(),
primaryDriverId: user.primaryDriverId,
})),
total: apiDto.total,
page: apiDto.page,
limit: apiDto.limit,
totalPages: apiDto.totalPages,
};
return { status: 'ok', dto: pageDto };
} catch (error) {
console.error('AdminUsersPageQuery failed:', error);
if (error instanceof Error && (error.message.includes('403') || error.message.includes('401'))) {
return { status: 'notFound' };
}
return { status: 'error', errorId: 'admin_users_fetch_failed' };
}
}
}

View File

@@ -0,0 +1,17 @@
/**
* AdminDashboardViewData
*
* ViewData for AdminDashboardTemplate.
* Template-ready data structure with only primitives.
*/
export interface AdminDashboardViewData {
stats: {
totalUsers: number;
activeUsers: number;
suspendedUsers: number;
deletedUsers: number;
systemAdmins: number;
recentLogins: number;
newUsersToday: number;
};
}

View File

@@ -45,6 +45,24 @@ export interface DriverSummaryData {
profileUrl: string;
}
export interface SponsorMetric {
icon: any; // React component (lucide-react icon)
label: string;
value: string | number;
color?: string;
trend?: {
value: number;
isPositive: boolean;
};
}
export interface SponsorshipSlot {
tier: 'main' | 'secondary';
available: boolean;
price: number;
benefits: string[];
}
export interface LeagueDetailViewData {
// Basic info
leagueId: string;
@@ -79,5 +97,7 @@ export interface LeagueDetailViewData {
mainSponsorPrice: number;
secondaryPrice: number;
totalImpressions: number;
metrics: SponsorMetric[];
slots: SponsorshipSlot[];
} | null;
}

View File

@@ -6,10 +6,13 @@
export interface StandingEntryData {
driverId: string;
position: number;
points: number;
wins: number;
podiums: number;
races: number;
totalPoints: number;
racesFinished: number;
racesStarted: number;
avgFinish: number | null;
penaltyPoints: number;
bonusPoints: number;
teamName?: string;
}
export interface DriverData {

View File

@@ -3,6 +3,17 @@
* Contains only raw serializable data, no methods or computed properties
*/
export interface SponsorMetric {
icon: any; // React component (lucide-react icon)
label: string;
value: string | number;
color?: string;
trend?: {
value: number;
isPositive: boolean;
};
}
export interface TeamDetailData {
id: string;
name: string;
@@ -32,9 +43,17 @@ export interface TeamMemberData {
avatarUrl: string;
}
export interface TeamTab {
id: 'overview' | 'roster' | 'standings' | 'admin';
label: string;
visible: boolean;
}
export interface TeamDetailViewData {
team: TeamDetailData;
memberships: TeamMemberData[];
currentDriverId: string;
isAdmin: boolean;
teamMetrics: SponsorMetric[];
tabs: TeamTab[];
}