website refactor
This commit is contained in:
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
25
apps/website/lib/contracts/builders/ViewDataBuilder.ts
Normal file
25
apps/website/lib/contracts/builders/ViewDataBuilder.ts
Normal 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;
|
||||
}
|
||||
25
apps/website/lib/contracts/builders/ViewModelBuilder.ts
Normal file
25
apps/website/lib/contracts/builders/ViewModelBuilder.ts
Normal 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;
|
||||
}
|
||||
33
apps/website/lib/contracts/mutations/Mutation.ts
Normal file
33
apps/website/lib/contracts/mutations/Mutation.ts
Normal 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>;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PageQueryResult } from "@/lib/page-queries/page-query-result/PageQueryResult";
|
||||
import { PageQueryResult } from "./PageQueryResult";
|
||||
|
||||
|
||||
/**
|
||||
|
||||
37
apps/website/lib/mutations/admin/DeleteUserMutation.ts
Normal file
37
apps/website/lib/mutations/admin/DeleteUserMutation.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
37
apps/website/lib/mutations/admin/UpdateUserStatusMutation.ts
Normal file
37
apps/website/lib/mutations/admin/UpdateUserStatusMutation.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
50
apps/website/lib/page-queries/AdminDashboardPageQuery.ts
Normal file
50
apps/website/lib/page-queries/AdminDashboardPageQuery.ts
Normal 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' };
|
||||
}
|
||||
}
|
||||
}
|
||||
93
apps/website/lib/page-queries/AdminUsersPageQuery.ts
Normal file
93
apps/website/lib/page-queries/AdminUsersPageQuery.ts
Normal 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' };
|
||||
}
|
||||
}
|
||||
}
|
||||
17
apps/website/lib/view-data/AdminDashboardViewData.ts
Normal file
17
apps/website/lib/view-data/AdminDashboardViewData.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user