website refactor

This commit is contained in:
2026-01-17 22:55:03 +01:00
parent 64d9e7fd16
commit 69d4cce7f1
64 changed files with 1146 additions and 1014 deletions

View File

@@ -30,6 +30,10 @@ describe('AnalyticsService', () => {
const result = await service.recordPageView(input);
expect(mockApiClient.recordPageView).toHaveBeenCalledWith({
entityType: 'page',
entityId: '/dashboard',
visitorType: 'user',
sessionId: 'temp-session',
path: '/dashboard',
userId: 'user-123',
});
@@ -48,6 +52,10 @@ describe('AnalyticsService', () => {
const result = await service.recordPageView(input);
expect(mockApiClient.recordPageView).toHaveBeenCalledWith({
entityType: 'page',
entityId: '/home',
visitorType: 'guest',
sessionId: 'temp-session',
path: '/home',
});
expect(result).toBeInstanceOf(RecordPageViewOutputViewModel);
@@ -69,6 +77,11 @@ describe('AnalyticsService', () => {
const result = await service.recordEngagement(input);
expect(mockApiClient.recordEngagement).toHaveBeenCalledWith({
action: 'button_click',
entityType: 'ui_element',
entityId: 'unknown',
actorType: 'user',
sessionId: 'temp-session',
eventType: 'button_click',
userId: 'user-123',
metadata: { buttonId: 'submit', page: '/form' },
@@ -89,6 +102,11 @@ describe('AnalyticsService', () => {
const result = await service.recordEngagement(input);
expect(mockApiClient.recordEngagement).toHaveBeenCalledWith({
action: 'page_load',
entityType: 'ui_element',
entityId: 'unknown',
actorType: 'guest',
sessionId: 'temp-session',
eventType: 'page_load',
});
expect(result).toBeInstanceOf(RecordEngagementOutputViewModel);

View File

@@ -0,0 +1,47 @@
import { injectable, unmanaged } from 'inversify';
import { AnalyticsApiClient } from '@/lib/api/analytics/AnalyticsApiClient';
import { RecordPageViewOutputViewModel } from '@/lib/view-models/RecordPageViewOutputViewModel';
import { RecordEngagementOutputViewModel } from '@/lib/view-models/RecordEngagementOutputViewModel';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { Service } from '@/lib/contracts/services/Service';
@injectable()
export class AnalyticsService implements Service {
private readonly apiClient: AnalyticsApiClient;
constructor(@unmanaged() apiClient?: AnalyticsApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger);
this.apiClient = new AnalyticsApiClient(baseUrl, errorReporter, logger);
}
}
async recordPageView(input: { path: string; userId?: string }): Promise<RecordPageViewOutputViewModel> {
const data = await this.apiClient.recordPageView({
entityType: 'page',
entityId: input.path,
visitorType: input.userId ? 'user' : 'guest',
sessionId: 'temp-session', // Should come from a session service
...input
});
return new RecordPageViewOutputViewModel(data);
}
async recordEngagement(input: { eventType: string; userId?: string; metadata?: Record<string, any> }): Promise<RecordEngagementOutputViewModel> {
const data = await this.apiClient.recordEngagement({
action: input.eventType,
entityType: 'ui_element',
entityId: 'unknown',
actorType: input.userId ? 'user' : 'guest',
sessionId: 'temp-session', // Should come from a session service
...input
});
return new RecordEngagementOutputViewModel(data);
}
}

View File

@@ -9,38 +9,39 @@ describe('DashboardService', () => {
beforeEach(() => {
mockApiClient = {
getDashboardData: vi.fn(),
getDashboardOverview: vi.fn(),
getAnalyticsMetrics: vi.fn(),
recordPageView: vi.fn(),
recordEngagement: vi.fn(),
} as Mocked<AnalyticsApiClient>;
} as any;
service = new DashboardService(mockApiClient);
service = new DashboardService();
(service as any).apiClient = mockApiClient;
(service as any).analyticsApiClient = mockApiClient;
});
describe('getDashboardData', () => {
it('should call apiClient.getDashboardData and return AnalyticsDashboardViewModel', async () => {
describe('getDashboardOverview', () => {
it('should call apiClient.getDashboardOverview and return Result with DashboardOverviewDTO', async () => {
const dto = {
totalUsers: 100,
activeUsers: 50,
totalRaces: 20,
totalLeagues: 5,
};
mockApiClient.getDashboardData.mockResolvedValue(dto);
mockApiClient.getDashboardOverview.mockResolvedValue(dto);
const result = await service.getDashboardData();
const result = await service.getDashboardOverview();
expect(mockApiClient.getDashboardData).toHaveBeenCalled();
expect(result).toBeInstanceOf(AnalyticsDashboardViewModel);
expect(result.totalUsers).toBe(100);
expect(result.activeUsers).toBe(50);
expect(result.totalRaces).toBe(20);
expect(result.totalLeagues).toBe(5);
expect(mockApiClient.getDashboardOverview).toHaveBeenCalled();
expect(result.isOk()).toBe(true);
const value = (result as any).value;
expect(value.totalUsers).toBe(100);
expect(value.activeUsers).toBe(50);
expect(value.totalRaces).toBe(20);
expect(value.totalLeagues).toBe(5);
});
});
describe('getAnalyticsMetrics', () => {
it('should call apiClient.getAnalyticsMetrics and return AnalyticsMetricsViewModel', async () => {
it('should call apiClient.getAnalyticsMetrics and return Result with AnalyticsMetricsViewModel', async () => {
const dto = {
pageViews: 1000,
uniqueVisitors: 500,
@@ -52,11 +53,12 @@ describe('DashboardService', () => {
const result = await service.getAnalyticsMetrics();
expect(mockApiClient.getAnalyticsMetrics).toHaveBeenCalled();
expect(result).toBeInstanceOf(AnalyticsMetricsViewModel);
expect(result.pageViews).toBe(1000);
expect(result.uniqueVisitors).toBe(500);
expect(result.averageSessionDuration).toBe(300);
expect(result.bounceRate).toBe(0.25);
expect(result.isOk()).toBe(true);
const value = (result as any).value;
expect(value.pageViews).toBe(1000);
expect(value.uniqueVisitors).toBe(500);
expect(value.averageSessionDuration).toBe(300);
expect(value.bounceRate).toBe(0.25);
});
});

View File

@@ -1,6 +1,8 @@
import { injectable } from 'inversify';
import { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient';
import { AnalyticsApiClient } from '@/lib/api/analytics/AnalyticsApiClient';
import { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import { GetAnalyticsMetricsOutputDTO } from '@/lib/types/generated/GetAnalyticsMetricsOutputDTO';
import { Result } from '@/lib/contracts/Result';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
@@ -17,12 +19,14 @@ import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
@injectable()
export class DashboardService implements Service {
private apiClient: DashboardApiClient;
private analyticsApiClient: AnalyticsApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
this.apiClient = new DashboardApiClient(baseUrl, errorReporter, logger);
this.analyticsApiClient = new AnalyticsApiClient(baseUrl, errorReporter, logger);
}
async getDashboardOverview(): Promise<Result<DashboardOverviewDTO, DomainError>> {
@@ -30,29 +34,40 @@ export class DashboardService implements Service {
const dto = await this.apiClient.getDashboardOverview();
return Result.ok(dto);
} catch (error) {
// Convert ApiError to DomainError
if (error instanceof ApiError) {
switch (error.type) {
case 'NOT_FOUND':
return Result.err({ type: 'notFound', message: error.message });
case 'AUTH_ERROR':
return Result.err({ type: 'unauthorized', message: error.message });
case 'SERVER_ERROR':
return Result.err({ type: 'serverError', message: error.message });
case 'NETWORK_ERROR':
case 'TIMEOUT_ERROR':
return Result.err({ type: 'networkError', message: error.message });
default:
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: 'Dashboard fetch failed' });
return this.handleError(error, 'Dashboard fetch failed');
}
}
async getAnalyticsMetrics(): Promise<Result<GetAnalyticsMetricsOutputDTO, DomainError>> {
try {
const dto = await this.analyticsApiClient.getAnalyticsMetrics();
return Result.ok(dto);
} catch (error) {
return this.handleError(error, 'Analytics metrics fetch failed');
}
}
private handleError(error: unknown, defaultMessage: string): Result<any, DomainError> {
if (error instanceof ApiError) {
switch (error.type) {
case 'NOT_FOUND':
return Result.err({ type: 'notFound', message: error.message });
case 'AUTH_ERROR':
return Result.err({ type: 'unauthorized', message: error.message });
case 'SERVER_ERROR':
return Result.err({ type: 'serverError', message: error.message });
case 'NETWORK_ERROR':
case 'TIMEOUT_ERROR':
return Result.err({ type: 'networkError', message: error.message });
default:
return Result.err({ type: 'unknown', message: error.message });
}
}
if (error instanceof Error) {
return Result.err({ type: 'unknown', message: error.message });
}
return Result.err({ type: 'unknown', message: defaultMessage });
}
}