943 lines
22 KiB
Markdown
943 lines
22 KiB
Markdown
# Streamlined Error and Load State Handling Design
|
|
|
|
## Overview
|
|
|
|
This document outlines a comprehensive design for standardizing error and loading state handling across all pages in the GridPilot website app using shared, user-friendly components.
|
|
|
|
## Problem Statement
|
|
|
|
The current implementation has several inconsistencies:
|
|
- Mixed state management approaches (hook-based vs manual useState)
|
|
- Inconsistent naming (loading vs isLoading vs pending)
|
|
- Different error UIs (full-screen, inline, modal, toast)
|
|
- Various loading UIs (spinners, skeletons, text-only, full-screen)
|
|
- No shared useDataFetching() hook
|
|
- Error boundaries not used everywhere
|
|
|
|
## Solution Architecture
|
|
|
|
### 1. Unified State Management Pattern
|
|
|
|
All pages will follow this consistent pattern:
|
|
|
|
```typescript
|
|
// Standardized state interface
|
|
interface PageState<T> {
|
|
data: T | null;
|
|
isLoading: boolean;
|
|
error: ApiError | null;
|
|
retry: () => Promise<void>;
|
|
}
|
|
|
|
// Standardized hook signature
|
|
interface UseDataFetchingOptions<T> {
|
|
queryKey: string[];
|
|
queryFn: () => Promise<T>;
|
|
enabled?: boolean;
|
|
retryOnMount?: boolean;
|
|
}
|
|
|
|
// Standardized return type
|
|
interface UseDataFetchingResult<T> {
|
|
data: T | null;
|
|
isLoading: boolean;
|
|
isFetching: boolean;
|
|
error: ApiError | null;
|
|
retry: () => Promise<void>;
|
|
refetch: () => Promise<void>;
|
|
}
|
|
```
|
|
|
|
### 2. Component Architecture
|
|
|
|
```
|
|
apps/website/components/shared/
|
|
├── state/
|
|
│ ├── LoadingWrapper.tsx # Main loading component with variants
|
|
│ ├── ErrorDisplay.tsx # Standardized error display
|
|
│ ├── EmptyState.tsx # Enhanced empty state
|
|
│ └── StateContainer.tsx # Combined wrapper for all states
|
|
├── hooks/
|
|
│ └── useDataFetching.ts # Unified data fetching hook
|
|
└── types/
|
|
└── state.types.ts # TypeScript interfaces
|
|
```
|
|
|
|
### 3. Shared Component Design
|
|
|
|
#### LoadingWrapper Component
|
|
|
|
**Purpose**: Provides consistent loading states with multiple variants
|
|
|
|
**Variants**:
|
|
- `spinner` - Traditional loading spinner (default)
|
|
- `skeleton` - Skeleton screens for better UX
|
|
- `full-screen` - Centered in viewport
|
|
- `inline` - Compact inline loading
|
|
- `card` - Loading card placeholders
|
|
|
|
**Props Interface**:
|
|
```typescript
|
|
interface LoadingWrapperProps {
|
|
variant?: 'spinner' | 'skeleton' | 'full-screen' | 'inline' | 'card';
|
|
message?: string;
|
|
className?: string;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
/**
|
|
* For skeleton variant - number of skeleton items to show
|
|
*/
|
|
skeletonCount?: number;
|
|
/**
|
|
* For card variant - card layout configuration
|
|
*/
|
|
cardConfig?: {
|
|
height?: number;
|
|
count?: number;
|
|
className?: string;
|
|
};
|
|
}
|
|
```
|
|
|
|
**Usage Examples**:
|
|
```typescript
|
|
// Default spinner
|
|
<LoadingWrapper />
|
|
|
|
// Full-screen with custom message
|
|
<LoadingWrapper
|
|
variant="full-screen"
|
|
message="Loading race details..."
|
|
/>
|
|
|
|
// Skeleton for list
|
|
<LoadingWrapper
|
|
variant="skeleton"
|
|
skeletonCount={5}
|
|
/>
|
|
|
|
// Inline loading
|
|
<LoadingWrapper
|
|
variant="inline"
|
|
size="sm"
|
|
message="Saving..."
|
|
/>
|
|
```
|
|
|
|
#### ErrorDisplay Component
|
|
|
|
**Purpose**: Standardized error display with consistent behavior
|
|
|
|
**Props Interface**:
|
|
```typescript
|
|
interface ErrorDisplayProps {
|
|
error: ApiError;
|
|
onRetry?: () => void;
|
|
variant?: 'full-screen' | 'inline' | 'card' | 'toast';
|
|
/**
|
|
* Show retry button (auto-detected from error.isRetryable())
|
|
*/
|
|
showRetry?: boolean;
|
|
/**
|
|
* Show navigation buttons
|
|
*/
|
|
showNavigation?: boolean;
|
|
/**
|
|
* Additional actions
|
|
*/
|
|
actions?: Array<{
|
|
label: string;
|
|
onClick: () => void;
|
|
variant?: 'primary' | 'secondary' | 'danger';
|
|
}>;
|
|
className?: string;
|
|
/**
|
|
* Hide technical details in development
|
|
*/
|
|
hideTechnicalDetails?: boolean;
|
|
}
|
|
```
|
|
|
|
**Features**:
|
|
- Auto-detects retryable errors
|
|
- Shows user-friendly messages
|
|
- Provides navigation options (back, home)
|
|
- Displays technical details in development
|
|
- Supports custom actions
|
|
- Accessible (ARIA labels, keyboard navigation)
|
|
|
|
**Usage Examples**:
|
|
```typescript
|
|
// Full-screen error
|
|
<ErrorDisplay
|
|
error={error}
|
|
onRetry={retry}
|
|
variant="full-screen"
|
|
/>
|
|
|
|
// Inline error with custom actions
|
|
<ErrorDisplay
|
|
error={error}
|
|
variant="inline"
|
|
actions={[
|
|
{ label: 'Contact Support', onClick: () => router.push('/support') }
|
|
]}
|
|
/>
|
|
|
|
// Toast-style error
|
|
<ErrorDisplay
|
|
error={error}
|
|
variant="toast"
|
|
showNavigation={false}
|
|
/>
|
|
```
|
|
|
|
#### EmptyState Component
|
|
|
|
**Purpose**: Consistent empty/placeholder states
|
|
|
|
**Props Interface**:
|
|
```typescript
|
|
interface EmptyStateProps {
|
|
icon: LucideIcon;
|
|
title: string;
|
|
description?: string;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
icon?: LucideIcon;
|
|
};
|
|
variant?: 'default' | 'minimal' | 'full-page';
|
|
className?: string;
|
|
/**
|
|
* Show illustration instead of icon
|
|
*/
|
|
illustration?: 'racing' | 'league' | 'team' | 'sponsor';
|
|
}
|
|
```
|
|
|
|
**Usage Examples**:
|
|
```typescript
|
|
// No races
|
|
<EmptyState
|
|
icon={Calendar}
|
|
title="No upcoming races"
|
|
description="Join a league to see races here"
|
|
action={{
|
|
label: "Browse Leagues",
|
|
onClick: () => router.push('/leagues')
|
|
}}
|
|
/>
|
|
|
|
// No data with illustration
|
|
<EmptyState
|
|
illustration="racing"
|
|
title="No results found"
|
|
description="Try adjusting your filters"
|
|
variant="minimal"
|
|
/>
|
|
```
|
|
|
|
### 4. useDataFetching Hook
|
|
|
|
**Purpose**: Unified data fetching with built-in state management
|
|
|
|
**Signature**:
|
|
```typescript
|
|
function useDataFetching<T>(
|
|
options: UseDataFetchingOptions<T>
|
|
): UseDataFetchingResult<T>
|
|
```
|
|
|
|
**Implementation Features**:
|
|
- Built-in retry logic
|
|
- Error classification
|
|
- Loading state management
|
|
- Refetch capability
|
|
- Integration with error boundaries
|
|
- Automatic retry on mount for recoverable errors
|
|
|
|
**Usage Example**:
|
|
```typescript
|
|
// Dashboard page
|
|
function DashboardPage() {
|
|
const {
|
|
data,
|
|
isLoading,
|
|
error,
|
|
retry,
|
|
refetch
|
|
} = useDataFetching({
|
|
queryKey: ['dashboardOverview'],
|
|
queryFn: () => dashboardService.getDashboardOverview(),
|
|
retryOnMount: true,
|
|
});
|
|
|
|
if (isLoading) {
|
|
return <LoadingWrapper variant="full-screen" />;
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<ErrorDisplay
|
|
error={error}
|
|
onRetry={retry}
|
|
variant="full-screen"
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (!data) {
|
|
return (
|
|
<EmptyState
|
|
icon={Activity}
|
|
title="No dashboard data"
|
|
description="Try refreshing the page"
|
|
action={{ label: "Refresh", onClick: refetch }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return <DashboardContent data={data} />;
|
|
}
|
|
```
|
|
|
|
### 5. StateContainer Component
|
|
|
|
**Purpose**: Combined wrapper that handles all states automatically
|
|
|
|
**Props Interface**:
|
|
```typescript
|
|
interface StateContainerProps<T> {
|
|
data: T | null;
|
|
isLoading: boolean;
|
|
error: ApiError | null;
|
|
retry: () => Promise<void>;
|
|
children: (data: T) => React.ReactNode;
|
|
/**
|
|
* Configuration for each state
|
|
*/
|
|
config?: {
|
|
loading?: {
|
|
variant?: LoadingWrapperProps['variant'];
|
|
message?: string;
|
|
};
|
|
error?: {
|
|
variant?: ErrorDisplayProps['variant'];
|
|
actions?: ErrorDisplayProps['actions'];
|
|
};
|
|
empty?: {
|
|
icon: LucideIcon;
|
|
title: string;
|
|
description?: string;
|
|
action?: EmptyStateProps['action'];
|
|
};
|
|
};
|
|
className?: string;
|
|
}
|
|
```
|
|
|
|
**Usage Example**:
|
|
```typescript
|
|
// Simplified page implementation
|
|
function LeagueDetailPage() {
|
|
const { data, isLoading, error, retry } = useDataFetching({
|
|
queryKey: ['league', leagueId],
|
|
queryFn: () => leagueService.getLeague(leagueId),
|
|
});
|
|
|
|
return (
|
|
<StateContainer
|
|
data={data}
|
|
isLoading={isLoading}
|
|
error={error}
|
|
retry={retry}
|
|
config={{
|
|
loading: { variant: 'skeleton', message: 'Loading league...' },
|
|
error: { variant: 'full-screen' },
|
|
empty: {
|
|
icon: Trophy,
|
|
title: 'League not found',
|
|
description: 'The league may have been deleted',
|
|
action: { label: 'Back to Leagues', onClick: () => router.push('/leagues') }
|
|
}
|
|
}}
|
|
>
|
|
{(leagueData) => <LeagueDetailContent league={leagueData} />}
|
|
</StateContainer>
|
|
);
|
|
}
|
|
```
|
|
|
|
## 6. Error Boundary Integration
|
|
|
|
### EnhancedErrorBoundary Updates
|
|
|
|
The existing `EnhancedErrorBoundary` will be updated to:
|
|
1. Support the new standardized error display
|
|
2. Provide automatic retry functionality
|
|
3. Support custom fallback components
|
|
4. Integrate with the notification system
|
|
|
|
```typescript
|
|
interface EnhancedErrorBoundaryProps {
|
|
children: ReactNode;
|
|
fallback?: ReactNode;
|
|
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
onReset?: () => void;
|
|
enableDevOverlay?: boolean;
|
|
context?: Record<string, unknown>;
|
|
/**
|
|
* NEW: Auto-retry configuration
|
|
*/
|
|
autoRetry?: {
|
|
enabled: boolean;
|
|
maxAttempts?: number;
|
|
delay?: number;
|
|
};
|
|
/**
|
|
* NEW: Error display variant
|
|
*/
|
|
errorVariant?: ErrorDisplayProps['variant'];
|
|
}
|
|
```
|
|
|
|
### ApiErrorBoundary Updates
|
|
|
|
The `ApiErrorBoundary` will be enhanced to:
|
|
1. Handle both API errors and regular errors
|
|
2. Support retry functionality
|
|
3. Provide consistent UI across all API calls
|
|
|
|
## 7. Migration Strategy
|
|
|
|
### Phase 1: Foundation (Week 1)
|
|
1. Create new shared components in `components/shared/state/`
|
|
2. Create `useDataFetching` hook
|
|
3. Update TypeScript interfaces
|
|
4. Add comprehensive tests
|
|
|
|
### Phase 2: Core Pages (Week 2-3)
|
|
Update pages in priority order:
|
|
|
|
**High Priority**:
|
|
- `app/dashboard/page.tsx`
|
|
- `app/leagues/[id]/LeagueDetailInteractive.tsx`
|
|
- `app/races/[id]/RaceDetailInteractive.tsx`
|
|
|
|
**Medium Priority**:
|
|
- `app/teams/[id]/TeamDetailInteractive.tsx`
|
|
- `app/leagues/[id]/schedule/page.tsx`
|
|
- `app/races/[id]/results/RaceResultsInteractive.tsx`
|
|
- `app/races/[id]/stewarding/RaceStewardingInteractive.tsx`
|
|
|
|
**Lower Priority**:
|
|
- Sponsor pages
|
|
- Profile pages
|
|
- Other interactive components
|
|
|
|
### Phase 3: Rollout (Week 4)
|
|
1. Update remaining pages
|
|
2. Add migration codemods for automation
|
|
3. Update documentation
|
|
4. Team training
|
|
|
|
### Phase 4: Deprecation (Week 5-6)
|
|
1. Remove old components
|
|
2. Clean up legacy code
|
|
3. Performance optimization
|
|
|
|
## 8. Migration Examples
|
|
|
|
### Before (Current State)
|
|
```typescript
|
|
// Dashboard page - inconsistent patterns
|
|
function DashboardPage() {
|
|
const { data: dashboardData, isLoading, error } = useDashboardOverview();
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<main className="min-h-screen bg-deep-graphite flex items-center justify-center">
|
|
<div className="text-white">Loading dashboard...</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (error || !dashboardData) {
|
|
return (
|
|
<main className="min-h-screen bg-deep-graphite flex items-center justify-center">
|
|
<div className="text-red-400">Failed to load dashboard</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
// ... render content
|
|
}
|
|
```
|
|
|
|
### After (Standardized)
|
|
```typescript
|
|
// Dashboard page - unified pattern
|
|
function DashboardPage() {
|
|
const { data, isLoading, error, retry } = useDataFetching({
|
|
queryKey: ['dashboardOverview'],
|
|
queryFn: () => dashboardService.getDashboardOverview(),
|
|
});
|
|
|
|
if (isLoading) {
|
|
return <LoadingWrapper variant="full-screen" message="Loading dashboard..." />;
|
|
}
|
|
|
|
if (error) {
|
|
return <ErrorDisplay error={error} onRetry={retry} variant="full-screen" />;
|
|
}
|
|
|
|
if (!data) {
|
|
return (
|
|
<EmptyState
|
|
icon={Activity}
|
|
title="No dashboard data"
|
|
description="Try refreshing the page"
|
|
action={{ label: "Refresh", onClick: retry }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return <DashboardContent data={data} />;
|
|
}
|
|
```
|
|
|
|
### Before (League Detail - Manual State)
|
|
```typescript
|
|
function LeagueDetailInteractive() {
|
|
const [viewModel, setViewModel] = useState<LeagueDetailPageViewModel | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const loadLeagueData = async () => {
|
|
try {
|
|
const viewModelData = await leagueService.getLeagueDetailPageData(leagueId);
|
|
if (!viewModelData) {
|
|
setError('League not found');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
setViewModel(viewModelData);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load league');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadLeagueData();
|
|
}, [leagueId]);
|
|
|
|
if (loading) {
|
|
return <div className="text-center text-gray-400">Loading league...</div>;
|
|
}
|
|
|
|
if (error || !viewModel) {
|
|
return <div className="text-center text-warning-amber">{error || 'League not found'}</div>;
|
|
}
|
|
|
|
// ... render content
|
|
}
|
|
```
|
|
|
|
### After (League Detail - Standardized)
|
|
```typescript
|
|
function LeagueDetailInteractive() {
|
|
const params = useParams();
|
|
const leagueId = params.id as string;
|
|
|
|
const { data: viewModel, isLoading, error, retry } = useDataFetching({
|
|
queryKey: ['leagueDetailPage', leagueId],
|
|
queryFn: () => leagueService.getLeagueDetailPageData(leagueId),
|
|
});
|
|
|
|
return (
|
|
<StateContainer
|
|
data={viewModel}
|
|
isLoading={isLoading}
|
|
error={error}
|
|
retry={retry}
|
|
config={{
|
|
loading: { variant: 'skeleton', message: 'Loading league...' },
|
|
error: { variant: 'full-screen' },
|
|
empty: {
|
|
icon: Trophy,
|
|
title: 'League not found',
|
|
description: 'The league may have been deleted or you may not have access',
|
|
action: { label: 'Back to Leagues', onClick: () => router.push('/leagues') }
|
|
}
|
|
}}
|
|
>
|
|
{(leagueData) => <LeagueDetailTemplate viewModel={leagueData} leagueId={leagueId} />}
|
|
</StateContainer>
|
|
);
|
|
}
|
|
```
|
|
|
|
## 9. Testing Strategy
|
|
|
|
### Component Tests
|
|
```typescript
|
|
// LoadingWrapper tests
|
|
describe('LoadingWrapper', () => {
|
|
it('renders spinner by default', () => { /* ... */ });
|
|
it('renders skeleton variant', () => { /* ... */ });
|
|
it('shows custom message', () => { /* ... */ });
|
|
it('is accessible', () => { /* ... */ });
|
|
});
|
|
|
|
// ErrorDisplay tests
|
|
describe('ErrorDisplay', () => {
|
|
it('shows retry button for retryable errors', () => { /* ... */ });
|
|
it('hides retry for non-retryable errors', () => { /* ... */ });
|
|
it('provides navigation options', () => { /* ... */ });
|
|
it('shows technical details in dev mode', () => { /* ... */ });
|
|
});
|
|
|
|
// useDataFetching tests
|
|
describe('useDataFetching', () => {
|
|
it('manages loading state correctly', () => { /* ... */ });
|
|
it('handles errors properly', () => { /* ... */ });
|
|
it('provides retry functionality', () => { /* ... */ });
|
|
it('integrates with error boundaries', () => { /* ... */ });
|
|
});
|
|
```
|
|
|
|
### Integration Tests
|
|
```typescript
|
|
describe('Page State Handling', () => {
|
|
it('dashboard shows loading, then content', async () => { /* ... */ });
|
|
it('league page handles not found', async () => { /* ... */ });
|
|
it('race page handles network errors', async () => { /* ... */ });
|
|
});
|
|
```
|
|
|
|
## 10. Accessibility Considerations
|
|
|
|
All components will be fully accessible:
|
|
- ARIA labels for loading states
|
|
- Keyboard navigation support
|
|
- Screen reader announcements
|
|
- High contrast support
|
|
- Focus management
|
|
|
|
## 11. Performance Considerations
|
|
|
|
- Lazy loading for heavy components
|
|
- Memoization where appropriate
|
|
- Optimistic updates support
|
|
- Debounced retry logic
|
|
- Minimal re-renders
|
|
|
|
## 12. Documentation and Training
|
|
|
|
### Developer Documentation
|
|
- Component API documentation
|
|
- Migration guides
|
|
- Best practices
|
|
- Troubleshooting guide
|
|
|
|
### Team Training
|
|
- Workshop on new patterns
|
|
- Code review guidelines
|
|
- Migration walkthroughs
|
|
|
|
## 13. TypeScript Interfaces Reference
|
|
|
|
All TypeScript interfaces are defined in `components/shared/state/types.ts`. Here's a comprehensive reference:
|
|
|
|
### Core State Interfaces
|
|
|
|
```typescript
|
|
// Basic state for any data fetching
|
|
interface PageState<T> {
|
|
data: T | null;
|
|
isLoading: boolean;
|
|
error: ApiError | null;
|
|
retry: () => Promise<void>;
|
|
}
|
|
|
|
// Extended state with metadata
|
|
interface PageStateWithMeta<T> extends PageState<T> {
|
|
isFetching: boolean;
|
|
refetch: () => Promise<void>;
|
|
lastUpdated: Date | null;
|
|
isStale: boolean;
|
|
}
|
|
```
|
|
|
|
### Hook Interfaces
|
|
|
|
```typescript
|
|
// useDataFetching options
|
|
interface UseDataFetchingOptions<T> {
|
|
queryKey: string[];
|
|
queryFn: () => Promise<T>;
|
|
enabled?: boolean;
|
|
retryOnMount?: boolean;
|
|
cacheTime?: number;
|
|
staleTime?: number;
|
|
maxRetries?: number;
|
|
retryDelay?: number;
|
|
onSuccess?: (data: T) => void;
|
|
onError?: (error: ApiError) => void;
|
|
}
|
|
|
|
// useDataFetching result
|
|
interface UseDataFetchingResult<T> {
|
|
data: T | null;
|
|
isLoading: boolean;
|
|
isFetching: boolean;
|
|
error: ApiError | null;
|
|
retry: () => Promise<void>;
|
|
refetch: () => Promise<void>;
|
|
lastUpdated: Date | null;
|
|
isStale: boolean;
|
|
}
|
|
```
|
|
|
|
### Component Props
|
|
|
|
```typescript
|
|
// LoadingWrapper
|
|
interface LoadingWrapperProps {
|
|
variant?: 'spinner' | 'skeleton' | 'full-screen' | 'inline' | 'card';
|
|
message?: string;
|
|
className?: string;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
skeletonCount?: number;
|
|
cardConfig?: {
|
|
height?: number;
|
|
count?: number;
|
|
className?: string;
|
|
};
|
|
ariaLabel?: string;
|
|
}
|
|
|
|
// ErrorDisplay
|
|
interface ErrorDisplayProps {
|
|
error: ApiError;
|
|
onRetry?: () => void;
|
|
variant?: 'full-screen' | 'inline' | 'card' | 'toast';
|
|
showRetry?: boolean;
|
|
showNavigation?: boolean;
|
|
actions?: ErrorAction[];
|
|
className?: string;
|
|
hideTechnicalDetails?: boolean;
|
|
ariaLabel?: string;
|
|
}
|
|
|
|
interface ErrorAction {
|
|
label: string;
|
|
onClick: () => void;
|
|
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
|
|
icon?: LucideIcon;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
// EmptyState
|
|
interface EmptyStateProps {
|
|
icon: LucideIcon;
|
|
title: string;
|
|
description?: string;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
icon?: LucideIcon;
|
|
variant?: 'primary' | 'secondary';
|
|
};
|
|
variant?: 'default' | 'minimal' | 'full-page';
|
|
className?: string;
|
|
illustration?: 'racing' | 'league' | 'team' | 'sponsor' | 'driver';
|
|
ariaLabel?: string;
|
|
}
|
|
|
|
// StateContainer
|
|
interface StateContainerProps<T> {
|
|
data: T | null;
|
|
isLoading: boolean;
|
|
error: ApiError | null;
|
|
retry: () => Promise<void>;
|
|
children: (data: T) => ReactNode;
|
|
config?: StateContainerConfig<T>;
|
|
className?: string;
|
|
showEmpty?: boolean;
|
|
isEmpty?: (data: T) => boolean;
|
|
}
|
|
|
|
interface StateContainerConfig<T> {
|
|
loading?: {
|
|
variant?: LoadingVariant;
|
|
message?: string;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
};
|
|
error?: {
|
|
variant?: ErrorVariant;
|
|
actions?: ErrorAction[];
|
|
showRetry?: boolean;
|
|
showNavigation?: boolean;
|
|
};
|
|
empty?: {
|
|
icon: LucideIcon;
|
|
title: string;
|
|
description?: string;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
};
|
|
};
|
|
customRender?: {
|
|
loading?: () => ReactNode;
|
|
error?: (error: ApiError) => ReactNode;
|
|
empty?: () => ReactNode;
|
|
};
|
|
}
|
|
```
|
|
|
|
### Page Template Interfaces
|
|
|
|
```typescript
|
|
// Generic page template
|
|
interface PageTemplateProps<T> {
|
|
data: T | null;
|
|
isLoading: boolean;
|
|
error: ApiError | null;
|
|
retry: () => Promise<void>;
|
|
refetch: () => Promise<void>;
|
|
title?: string;
|
|
description?: string;
|
|
children: (data: T) => ReactNode;
|
|
config?: StateContainerConfig<T>;
|
|
}
|
|
|
|
// List page template
|
|
interface ListPageTemplateProps<T> extends PageTemplateProps<T[]> {
|
|
emptyConfig?: {
|
|
icon: LucideIcon;
|
|
title: string;
|
|
description?: string;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
};
|
|
};
|
|
showSkeleton?: boolean;
|
|
skeletonCount?: number;
|
|
}
|
|
|
|
// Detail page template
|
|
interface DetailPageTemplateProps<T> extends PageTemplateProps<T> {
|
|
onBack?: () => void;
|
|
onRefresh?: () => void;
|
|
}
|
|
```
|
|
|
|
### Configuration Interfaces
|
|
|
|
```typescript
|
|
// Retry configuration
|
|
interface RetryConfig {
|
|
maxAttempts?: number;
|
|
baseDelay?: number;
|
|
backoffMultiplier?: number;
|
|
retryOnMount?: boolean;
|
|
}
|
|
|
|
// Notification configuration
|
|
interface NotificationConfig {
|
|
showToastOnSuccess?: boolean;
|
|
showToastOnError?: boolean;
|
|
successMessage?: string;
|
|
errorMessage?: string;
|
|
autoDismissDelay?: number;
|
|
}
|
|
|
|
// Analytics configuration
|
|
interface StateAnalytics {
|
|
onStateChange?: (from: string, to: string, data?: unknown) => void;
|
|
onError?: (error: ApiError, context: string) => void;
|
|
onRetry?: (attempt: number, maxAttempts: number) => void;
|
|
}
|
|
|
|
// Performance metrics
|
|
interface PerformanceMetrics {
|
|
timeToFirstRender?: number;
|
|
timeToDataLoad?: number;
|
|
retryCount?: number;
|
|
cacheHit?: boolean;
|
|
}
|
|
|
|
// Advanced configuration
|
|
interface AdvancedStateConfig<T> extends StateContainerConfig<T> {
|
|
retry?: RetryConfig;
|
|
notifications?: NotificationConfig;
|
|
analytics?: StateAnalytics;
|
|
performance?: PerformanceMetrics;
|
|
}
|
|
```
|
|
|
|
### Type Aliases
|
|
|
|
```typescript
|
|
type LoadingVariant = 'spinner' | 'skeleton' | 'full-screen' | 'inline' | 'card';
|
|
type ErrorVariant = 'full-screen' | 'inline' | 'card' | 'toast';
|
|
type EmptyVariant = 'default' | 'minimal' | 'full-page';
|
|
type EmptyCheck<T> = (data: T | null) => boolean;
|
|
```
|
|
|
|
### Default Configuration
|
|
|
|
```typescript
|
|
const DEFAULT_CONFIG = {
|
|
loading: {
|
|
variant: 'spinner',
|
|
message: 'Loading...',
|
|
size: 'md',
|
|
},
|
|
error: {
|
|
variant: 'full-screen',
|
|
showRetry: true,
|
|
showNavigation: true,
|
|
},
|
|
empty: {
|
|
title: 'No data available',
|
|
description: 'There is nothing to display here',
|
|
},
|
|
retry: {
|
|
maxAttempts: 3,
|
|
baseDelay: 1000,
|
|
backoffMultiplier: 2,
|
|
retryOnMount: true,
|
|
},
|
|
notifications: {
|
|
showToastOnSuccess: false,
|
|
showToastOnError: true,
|
|
autoDismissDelay: 5000,
|
|
},
|
|
} as const;
|
|
```
|
|
|
|
## 14. Success Metrics
|
|
|
|
- 100% of pages use standardized components
|
|
- Reduced error handling code by 60%
|
|
- Consistent user experience across all pages
|
|
- Improved error recovery rates
|
|
- Faster development velocity
|
|
|
|
## 15. Rollback Plan
|
|
|
|
If issues arise:
|
|
1. Feature flags to toggle new components
|
|
2. Gradual rollout with monitoring
|
|
3. Quick revert capability
|
|
4. Comprehensive logging for debugging
|
|
|
|
---
|
|
|
|
This design provides a complete, user-friendly solution for error and load state handling that will improve both developer experience and user experience across the entire GridPilot website application. |