error and load state
This commit is contained in:
943
apps/website/docs/STREAMLINED_STATE_HANDLING_DESIGN.md
Normal file
943
apps/website/docs/STREAMLINED_STATE_HANDLING_DESIGN.md
Normal file
@@ -0,0 +1,943 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user