website refactor
This commit is contained in:
84
apps/website/lib/page-queries/page-dtos/DashboardPageDto.ts
Normal file
84
apps/website/lib/page-queries/page-dtos/DashboardPageDto.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Dashboard Page DTO
|
||||
* Contains raw JSON-serializable values only
|
||||
* Derived from DashboardOverviewDTO with ISO string timestamps
|
||||
*/
|
||||
export interface DashboardPageDto {
|
||||
currentDriver?: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
country: string;
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
rating: number;
|
||||
globalRank: number;
|
||||
consistency: number;
|
||||
};
|
||||
myUpcomingRaces: Array<{
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string; // ISO string
|
||||
status: string;
|
||||
isMyLeague: boolean;
|
||||
}>;
|
||||
otherUpcomingRaces: Array<{
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string; // ISO string
|
||||
status: string;
|
||||
isMyLeague: boolean;
|
||||
}>;
|
||||
upcomingRaces: Array<{
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string; // ISO string
|
||||
status: string;
|
||||
isMyLeague: boolean;
|
||||
}>;
|
||||
activeLeaguesCount: number;
|
||||
nextRace?: {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string; // ISO string
|
||||
status: string;
|
||||
isMyLeague: boolean;
|
||||
};
|
||||
recentResults: Array<{
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
position: number;
|
||||
date: string; // ISO string
|
||||
}>;
|
||||
leagueStandingsSummaries: Array<{
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
position: number;
|
||||
points: number;
|
||||
totalDrivers: number;
|
||||
}>;
|
||||
feedSummary: {
|
||||
notificationCount: number;
|
||||
items: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
headline: string;
|
||||
body?: string;
|
||||
timestamp: string; // ISO string
|
||||
ctaHref?: string;
|
||||
ctaLabel?: string;
|
||||
}>;
|
||||
};
|
||||
friends: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
country: string;
|
||||
}>;
|
||||
}
|
||||
@@ -1,24 +1,19 @@
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import { ContainerManager } from '@/lib/di/container';
|
||||
import { DASHBOARD_API_CLIENT_TOKEN } from '@/lib/di/tokens';
|
||||
import type { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient';
|
||||
import { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
|
||||
import type { DashboardOverviewViewModelData } from '@/lib/view-models/DashboardOverviewViewModelData';
|
||||
import type { PageQueryResult } from '@/lib/page-queries/page-query-result/PageQueryResult';
|
||||
import type { DashboardPageDto } from '@/lib/page-queries/page-dtos/DashboardPageDto';
|
||||
|
||||
interface ErrorWithStatusCode extends Error {
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* PageQueryResult discriminated union for SSR page queries
|
||||
* Transform DashboardOverviewDTO to DashboardPageDto
|
||||
* Converts Date objects to ISO strings for JSON serialization
|
||||
*/
|
||||
export type PageQueryResult<TData> =
|
||||
| { status: 'ok'; data: TData }
|
||||
| { status: 'notFound' }
|
||||
| { status: 'redirect'; destination: string }
|
||||
| { status: 'error'; error: Error };
|
||||
|
||||
/**
|
||||
* Transform DashboardOverviewDTO to DashboardOverviewViewModelData
|
||||
* Converts string dates to ISO strings for JSON serialization
|
||||
*/
|
||||
function transformDtoToViewModelData(dto: DashboardOverviewDTO): DashboardOverviewViewModelData {
|
||||
function transformDtoToPageDto(dto: DashboardOverviewDTO): DashboardPageDto {
|
||||
return {
|
||||
currentDriver: dto.currentDriver ? {
|
||||
id: dto.currentDriver.id,
|
||||
@@ -101,40 +96,54 @@ function transformDtoToViewModelData(dto: DashboardOverviewDTO): DashboardOvervi
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard page query that returns transformed ViewModelData
|
||||
* Returns a discriminated union instead of nullable data
|
||||
* Dashboard page query with manual wiring
|
||||
* Returns PageQueryResult<DashboardPageDto>
|
||||
* No DI container usage - constructs dependencies explicitly
|
||||
*/
|
||||
export class DashboardPageQuery {
|
||||
static async execute(): Promise<PageQueryResult<DashboardOverviewViewModelData>> {
|
||||
/**
|
||||
* Execute the dashboard page query
|
||||
* Constructs API client manually with required dependencies
|
||||
*/
|
||||
static async execute(): Promise<PageQueryResult<DashboardPageDto>> {
|
||||
try {
|
||||
const container = ContainerManager.getInstance().getContainer();
|
||||
const apiClient = container.get<DashboardApiClient>(DASHBOARD_API_CLIENT_TOKEN);
|
||||
// Manual wiring: construct dependencies explicitly
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
|
||||
// Construct API client with required dependencies
|
||||
// Using environment variable for base URL, fallback to empty string
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const apiClient = new DashboardApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
// Fetch data
|
||||
const dto = await apiClient.getDashboardOverview();
|
||||
|
||||
if (!dto) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
const viewModelData = transformDtoToViewModelData(dto);
|
||||
return { status: 'ok', data: viewModelData };
|
||||
// Transform to Page DTO
|
||||
const pageDto = transformDtoToPageDto(dto);
|
||||
return { status: 'ok', dto: pageDto };
|
||||
} catch (error) {
|
||||
// Handle specific error types
|
||||
if (error instanceof Error) {
|
||||
// Check if it's a not found error
|
||||
if (error.message.includes('not found') || (error as any).statusCode === 404) {
|
||||
const errorWithStatus = error as ErrorWithStatusCode;
|
||||
|
||||
if (errorWithStatus.message?.includes('not found') || errorWithStatus.statusCode === 404) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
// Check if it's a redirect error
|
||||
if (error.message.includes('redirect') || (error as any).statusCode === 302) {
|
||||
return { status: 'redirect', destination: '/' };
|
||||
if (errorWithStatus.message?.includes('redirect') || errorWithStatus.statusCode === 302) {
|
||||
return { status: 'redirect', to: '/' };
|
||||
}
|
||||
|
||||
return { status: 'error', error };
|
||||
return { status: 'error', errorId: 'DASHBOARD_FETCH_FAILED' };
|
||||
}
|
||||
|
||||
return { status: 'error', error: new Error(String(error)) };
|
||||
return { status: 'error', errorId: 'UNKNOWN_ERROR' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { SessionGateway } from '@/lib/gateways/SessionGateway';
|
||||
import { ApiClient } from '@/lib/api';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import type { PageQueryResult } from '@/lib/page-queries/page-query-result/PageQueryResult';
|
||||
|
||||
/**
|
||||
* Page DTO for Profile Leagues page
|
||||
* JSON-serializable data structure for server-to-client communication
|
||||
*/
|
||||
export interface ProfileLeaguesPageDto {
|
||||
ownedLeagues: Array<{
|
||||
leagueId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
membershipRole: 'owner' | 'admin' | 'steward' | 'member';
|
||||
}>;
|
||||
memberLeagues: Array<{
|
||||
leagueId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
membershipRole: 'owner' | 'admin' | 'steward' | 'member';
|
||||
}>;
|
||||
}
|
||||
|
||||
interface MembershipDTO {
|
||||
driverId: string;
|
||||
role: string;
|
||||
status?: 'active' | 'inactive';
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile Leagues Page Query
|
||||
*
|
||||
* Server-side composition that:
|
||||
* 1. Reads session to determine currentDriverId
|
||||
* 2. Calls API clients directly (manual wiring)
|
||||
* 3. Assembles Page DTO
|
||||
* 4. Returns PageQueryResult
|
||||
*/
|
||||
export class ProfileLeaguesPageQuery {
|
||||
static async execute(): Promise<PageQueryResult<ProfileLeaguesPageDto>> {
|
||||
try {
|
||||
// Get session server-side
|
||||
const sessionGateway = new SessionGateway();
|
||||
const session = await sessionGateway.getSession();
|
||||
|
||||
if (!session?.user?.primaryDriverId) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
const currentDriverId = session.user.primaryDriverId;
|
||||
|
||||
// Manual wiring: construct API client explicitly
|
||||
const apiBaseUrl = getWebsiteApiBaseUrl();
|
||||
const apiClient = new ApiClient(apiBaseUrl);
|
||||
|
||||
// Fetch all leagues
|
||||
const leaguesDto = await apiClient.leagues.getAllWithCapacity();
|
||||
|
||||
if (!leaguesDto?.leagues) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
// Fetch memberships for each league and categorize
|
||||
const owned: ProfileLeaguesPageDto['ownedLeagues'] = [];
|
||||
const member: ProfileLeaguesPageDto['memberLeagues'] = [];
|
||||
|
||||
for (const league of leaguesDto.leagues) {
|
||||
try {
|
||||
const membershipsDto = await apiClient.leagues.getMemberships(league.id);
|
||||
|
||||
// Handle both possible response structures with proper type checking
|
||||
let memberships: MembershipDTO[] = [];
|
||||
if (membershipsDto && typeof membershipsDto === 'object') {
|
||||
if ('members' in membershipsDto && Array.isArray((membershipsDto as { members?: unknown }).members)) {
|
||||
memberships = (membershipsDto as { members: MembershipDTO[] }).members;
|
||||
} else if ('memberships' in membershipsDto && Array.isArray((membershipsDto as { memberships?: unknown }).memberships)) {
|
||||
memberships = (membershipsDto as { memberships: MembershipDTO[] }).memberships;
|
||||
}
|
||||
}
|
||||
|
||||
const currentMembership = memberships.find(
|
||||
(m) => m.driverId === currentDriverId
|
||||
);
|
||||
|
||||
if (currentMembership && currentMembership.status === 'active') {
|
||||
const leagueData = {
|
||||
leagueId: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
membershipRole: currentMembership.role as 'owner' | 'admin' | 'steward' | 'member',
|
||||
};
|
||||
|
||||
if (currentMembership.role === 'owner') {
|
||||
owned.push(leagueData);
|
||||
} else {
|
||||
member.push(leagueData);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip leagues where membership fetch fails
|
||||
console.warn(`Failed to fetch memberships for league ${league.id}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
dto: {
|
||||
ownedLeagues: owned,
|
||||
memberLeagues: member,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('ProfileLeaguesPageQuery failed:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
// Check for specific error properties
|
||||
const errorAny = error as { statusCode?: number };
|
||||
if (error.message.includes('not found') || errorAny.statusCode === 404) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
if (error.message.includes('redirect') || errorAny.statusCode === 302) {
|
||||
return { status: 'redirect', to: '/' };
|
||||
}
|
||||
return { status: 'error', errorId: 'PROFILE_LEAGUES_FETCH_FAILED' };
|
||||
}
|
||||
|
||||
return { status: 'error', errorId: 'UNKNOWN_ERROR' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,7 @@ import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
|
||||
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { DriverService } from '@/lib/services/drivers/DriverService';
|
||||
import type { DriverProfileViewModelData } from '@/lib/view-models/DriverProfileViewModel';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
export type PageQueryResult =
|
||||
| { status: 'ok'; dto: DriverProfileViewModelData }
|
||||
| { status: 'notFound' }
|
||||
| { status: 'redirect'; to: string }
|
||||
| { status: 'error'; errorId: string };
|
||||
import type { PageQueryResult } from '@/lib/page-queries/page-query-result/PageQueryResult';
|
||||
|
||||
// ============================================================================
|
||||
// SERVER QUERY CLASS
|
||||
@@ -31,7 +22,7 @@ export class ProfilePageQuery {
|
||||
* @param driverId - The driver ID to fetch profile for
|
||||
* @returns PageQueryResult with discriminated union of states
|
||||
*/
|
||||
static async execute(driverId: string | null): Promise<PageQueryResult> {
|
||||
static async execute(driverId: string | null): Promise<PageQueryResult<DriverProfileViewModelData>> {
|
||||
// Handle missing driver ID
|
||||
if (!driverId) {
|
||||
return { status: 'notFound' };
|
||||
@@ -0,0 +1,137 @@
|
||||
import { SessionGateway } from '@/lib/gateways/SessionGateway';
|
||||
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
|
||||
import { TeamService } from '@/lib/services/teams/TeamService';
|
||||
import type { PageQueryResult } from '@/lib/page-queries/page-query-result/PageQueryResult';
|
||||
import type { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
|
||||
/**
|
||||
* TeamDetailPageDto - Raw serializable data for team detail page
|
||||
* Contains only raw data, no derived/computed properties
|
||||
*/
|
||||
export interface TeamDetailPageDto {
|
||||
team: {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description?: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
createdAt?: string;
|
||||
specialization?: string;
|
||||
region?: string;
|
||||
languages?: string[];
|
||||
category?: string;
|
||||
membership?: {
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
isActive: boolean;
|
||||
} | null;
|
||||
canManage: boolean;
|
||||
};
|
||||
memberships: Array<{
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
role: 'owner' | 'manager' | 'member';
|
||||
joinedAt: string;
|
||||
isActive: boolean;
|
||||
avatarUrl: string;
|
||||
}>;
|
||||
currentDriverId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TeamDetailPageQuery - Server-side composition for team detail page
|
||||
* Manual wiring only; no ContainerManager; no PageDataFetcher
|
||||
* Returns raw serializable DTO
|
||||
*/
|
||||
export class TeamDetailPageQuery {
|
||||
static async execute(teamId: string): Promise<PageQueryResult<TeamDetailPageDto>> {
|
||||
try {
|
||||
// Validate teamId
|
||||
if (!teamId) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
// Get session to determine current driver
|
||||
const sessionGateway = new SessionGateway();
|
||||
const session = await sessionGateway.getSession();
|
||||
|
||||
if (!session?.user?.primaryDriverId) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
const currentDriverId = session.user.primaryDriverId;
|
||||
|
||||
// Manual dependency creation
|
||||
const baseUrl = process.env.API_BASE_URL || 'http://localhost:3101';
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
|
||||
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
|
||||
const service = new TeamService(teamsApiClient);
|
||||
|
||||
// Fetch team details
|
||||
const teamData = await service.getTeamDetails(teamId, currentDriverId);
|
||||
|
||||
if (!teamData) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
// Fetch team members
|
||||
const membersData = await service.getTeamMembers(teamId, currentDriverId, teamData.ownerId);
|
||||
|
||||
// Transform to raw serializable DTO
|
||||
const dto: TeamDetailPageDto = {
|
||||
team: {
|
||||
id: teamData.id,
|
||||
name: teamData.name,
|
||||
tag: teamData.tag,
|
||||
description: teamData.description,
|
||||
ownerId: teamData.ownerId,
|
||||
leagues: teamData.leagues,
|
||||
createdAt: teamData.createdAt,
|
||||
specialization: teamData.specialization,
|
||||
region: teamData.region,
|
||||
languages: teamData.languages,
|
||||
category: teamData.category,
|
||||
membership: teamData.membership ? {
|
||||
role: teamData.membership.role,
|
||||
joinedAt: teamData.membership.joinedAt,
|
||||
isActive: teamData.membership.isActive,
|
||||
} : null,
|
||||
canManage: teamData.canManage,
|
||||
},
|
||||
memberships: membersData.map((member: TeamMemberViewModel) => ({
|
||||
driverId: member.driverId,
|
||||
driverName: member.driverName,
|
||||
role: member.role,
|
||||
joinedAt: member.joinedAt,
|
||||
isActive: member.isActive,
|
||||
avatarUrl: member.avatarUrl,
|
||||
})),
|
||||
currentDriverId,
|
||||
};
|
||||
|
||||
return { status: 'ok', dto };
|
||||
} catch (error) {
|
||||
// Handle specific error types
|
||||
if (error instanceof Error) {
|
||||
const errorAny = error as { statusCode?: number; message?: string };
|
||||
if (errorAny.message?.includes('not found') || errorAny.statusCode === 404) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
if (errorAny.message?.includes('redirect') || errorAny.statusCode === 302) {
|
||||
return { status: 'redirect', to: '/' };
|
||||
}
|
||||
return { status: 'error', errorId: 'TEAM_DETAIL_FETCH_FAILED' };
|
||||
}
|
||||
return { status: 'error', errorId: 'UNKNOWN_ERROR' };
|
||||
}
|
||||
}
|
||||
}
|
||||
98
apps/website/lib/page-queries/page-queries/TeamsPageQuery.ts
Normal file
98
apps/website/lib/page-queries/page-queries/TeamsPageQuery.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
|
||||
import { TeamService } from '@/lib/services/teams/TeamService';
|
||||
import type { PageQueryResult } from '@/lib/page-queries/page-query-result/PageQueryResult';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
|
||||
/**
|
||||
* TeamsPageDto - Raw serializable data for teams page
|
||||
* Contains only raw data, no derived/computed properties
|
||||
*/
|
||||
export interface TeamsPageDto {
|
||||
teams: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
memberCount: number;
|
||||
description?: string;
|
||||
totalWins: number;
|
||||
totalRaces: number;
|
||||
performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
isRecruiting: boolean;
|
||||
specialization?: 'endurance' | 'sprint' | 'mixed';
|
||||
region?: string;
|
||||
languages: string[];
|
||||
leagues: string[];
|
||||
logoUrl?: string;
|
||||
rating?: number;
|
||||
category?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* TeamsPageQuery - Server-side composition for teams list page
|
||||
* Manual wiring only; no ContainerManager; no PageDataFetcher
|
||||
* Returns raw serializable DTO
|
||||
*/
|
||||
export class TeamsPageQuery {
|
||||
static async execute(): Promise<PageQueryResult<TeamsPageDto>> {
|
||||
try {
|
||||
// Manual dependency creation
|
||||
const baseUrl = process.env.API_BASE_URL || 'http://localhost:3101';
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
|
||||
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
|
||||
const service = new TeamService(teamsApiClient);
|
||||
|
||||
// Fetch teams
|
||||
const teams = await service.getAllTeams();
|
||||
|
||||
if (!teams || teams.length === 0) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
|
||||
// Transform to raw serializable DTO
|
||||
const dto: TeamsPageDto = {
|
||||
teams: teams.map((team: TeamSummaryViewModel) => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
memberCount: team.memberCount,
|
||||
description: team.description,
|
||||
totalWins: team.totalWins,
|
||||
totalRaces: team.totalRaces,
|
||||
performanceLevel: team.performanceLevel,
|
||||
isRecruiting: team.isRecruiting,
|
||||
specialization: team.specialization,
|
||||
region: team.region,
|
||||
languages: team.languages,
|
||||
leagues: team.leagues,
|
||||
logoUrl: team.logoUrl,
|
||||
rating: team.rating,
|
||||
category: team.category,
|
||||
})),
|
||||
};
|
||||
|
||||
return { status: 'ok', dto };
|
||||
} catch (error) {
|
||||
// Handle specific error types
|
||||
if (error instanceof Error) {
|
||||
const errorAny = error as { statusCode?: number; message?: string };
|
||||
if (errorAny.message?.includes('not found') || errorAny.statusCode === 404) {
|
||||
return { status: 'notFound' };
|
||||
}
|
||||
if (errorAny.message?.includes('redirect') || errorAny.statusCode === 302) {
|
||||
return { status: 'redirect', to: '/' };
|
||||
}
|
||||
return { status: 'error', errorId: 'TEAMS_FETCH_FAILED' };
|
||||
}
|
||||
return { status: 'error', errorId: 'UNKNOWN_ERROR' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* PageQueryResult discriminated union
|
||||
*
|
||||
* Canonical result type for all server-side page queries.
|
||||
* Defines the explicit outcome of a page query execution.
|
||||
*
|
||||
* Based on WEBSITE_PAGE_QUERIES.md:
|
||||
* - ok with { dto }
|
||||
* - notFound
|
||||
* - redirect with { to }
|
||||
* - error with { errorId }
|
||||
*/
|
||||
|
||||
export type PageQueryResult<TPageDto> =
|
||||
| { status: 'ok'; dto: TPageDto }
|
||||
| { status: 'notFound' }
|
||||
| { status: 'redirect'; to: string }
|
||||
| { status: 'error'; errorId: string };
|
||||
Reference in New Issue
Block a user