feat(error): implement comprehensive error handling and loading states

- Add error boundary components with graceful fallbacks
- Implement loading states with skeleton components
- Create route-level suspense wrapper
- Add error page with recovery options
- Include error boundary demo for testing

Error handling provides resilient user experience with clear feedback
and recovery options when components fail.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-16 12:41:05 -06:00
parent 545d3ba71e
commit 28bfff42d8
9 changed files with 1993 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
import { useState } from 'react';
import { AppErrorBoundary } from './errors/AppErrorBoundary';
import { LoadingSpinner } from './loading/LoadingSpinner';
import { RouteSuspense } from './loading/RouteSuspense';
import { Skeleton } from './loading/Skeleton';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
/**
* Component that intentionally throws an error for demonstration
*/
function ErrorThrowingComponent({ shouldThrow }: { shouldThrow: boolean }) {
if (shouldThrow) {
throw new Error('This is a demonstration error for testing the error boundary');
}
return (
<Card className="p-6">
<h3 className="text-lg font-semibold text-text-primary mb-4">
Error Boundary Test Component
</h3>
<p className="text-text-secondary">
This component is working normally. Click the button below to trigger an error.
</p>
</Card>
);
}
/**
* Component that simulates async loading for demonstration
*/
function AsyncLoadingComponent({ isLoading }: { isLoading: boolean }) {
if (isLoading) {
return <LoadingSpinner size="lg" text="Loading async content..." />;
}
return (
<Card className="p-6">
<h3 className="text-lg font-semibold text-text-primary mb-4">
Async Content Loaded
</h3>
<p className="text-text-secondary">
This content was "loaded" asynchronously and is now displayed.
</p>
</Card>
);
}
/**
* Demo component showcasing error boundary and loading states
*/
export function ErrorBoundaryDemo() {
const [shouldThrowError, setShouldThrowError] = useState(false);
const [isAsyncLoading, setIsAsyncLoading] = useState(false);
const [showSkeletons, setShowSkeletons] = useState(false);
const handleThrowError = () => {
setShouldThrowError(true);
};
const handleSimulateAsyncLoad = () => {
setIsAsyncLoading(true);
setTimeout(() => {
setIsAsyncLoading(false);
}, 3000);
};
const handleToggleSkeletons = () => {
setShowSkeletons(!showSkeletons);
};
return (
<div className="max-w-4xl mx-auto p-6 space-y-8">
<div className="text-center">
<h1 className="text-3xl font-bold text-text-primary mb-4">
Error Handling & Loading States Demo
</h1>
<p className="text-text-secondary">
Demonstration of error boundaries, loading states, and skeleton components
</p>
</div>
{/* Control Panel */}
<Card className="p-6">
<h2 className="text-xl font-semibold text-text-primary mb-4">Controls</h2>
<div className="flex flex-wrap gap-4">
<Button
onClick={handleThrowError}
variant="secondary"
size="md"
>
Trigger Error
</Button>
<Button
onClick={handleSimulateAsyncLoad}
variant="secondary"
size="md"
disabled={isAsyncLoading}
>
{isAsyncLoading ? 'Loading...' : 'Simulate Async Load'}
</Button>
<Button
onClick={handleToggleSkeletons}
variant="secondary"
size="md"
>
{showSkeletons ? 'Hide' : 'Show'} Skeletons
</Button>
</div>
</Card>
{/* Error Boundary Demo */}
<AppErrorBoundary
onError={(error) => {
console.log('Error caught by boundary:', error);
}}
>
<div className="space-y-6">
<h2 className="text-xl font-semibold text-text-primary">Error Boundary Test</h2>
<ErrorThrowingComponent shouldThrow={shouldThrowError} />
</div>
</AppErrorBoundary>
{/* Loading States Demo */}
<div className="space-y-6">
<h2 className="text-xl font-semibold text-text-primary">Loading States</h2>
{/* Route Suspense Demo */}
<RouteSuspense
skeletonType="card"
loadingText="Loading route content..."
timeout={5000}
>
<AsyncLoadingComponent isLoading={isAsyncLoading} />
</RouteSuspense>
</div>
{/* Skeleton Components Demo */}
<div className="space-y-6">
<h2 className="text-xl font-semibold text-text-primary">Skeleton Components</h2>
{showSkeletons ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Card Skeleton */}
<div>
<h3 className="text-lg font-medium text-text-primary mb-4">Card Skeleton</h3>
<Skeleton.Card />
</div>
{/* List Skeleton */}
<div>
<h3 className="text-lg font-medium text-text-primary mb-4">List Skeleton</h3>
<Skeleton.List />
</div>
{/* Form Skeleton */}
<div className="md:col-span-2">
<h3 className="text-lg font-medium text-text-primary mb-4">Form Skeleton</h3>
<Skeleton.Form />
</div>
</div>
) : (
<Card className="p-6">
<p className="text-text-secondary">
Click "Show Skeletons" to see skeleton loading components in action.
</p>
</Card>
)}
</div>
{/* Loading Spinner Variants */}
<div className="space-y-6">
<h2 className="text-xl font-semibold text-text-primary">Loading Spinner Variants</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<Card className="p-6 text-center">
<h4 className="text-sm font-medium text-text-primary mb-4">Small Primary</h4>
<LoadingSpinner size="sm" variant="primary" />
</Card>
<Card className="p-6 text-center">
<h4 className="text-sm font-medium text-text-primary mb-4">Medium Accent</h4>
<LoadingSpinner size="md" variant="accent" />
</Card>
<Card className="p-6 text-center">
<h4 className="text-sm font-medium text-text-primary mb-4">Large Secondary</h4>
<LoadingSpinner size="lg" variant="secondary" />
</Card>
<Card className="p-6 text-center">
<h4 className="text-sm font-medium text-text-primary mb-4">With Text</h4>
<LoadingSpinner size="md" variant="accent" text="Loading..." />
</Card>
</div>
</div>
{/* Development Notes */}
<Card className="p-6 bg-warning-bg border-warning-border">
<h3 className="text-lg font-semibold text-warning-text mb-4">
Development Notes
</h3>
<ul className="text-sm text-warning-text space-y-2">
<li> Error boundaries will automatically catch JavaScript errors and show fallback UI</li>
<li> RouteSuspense provides timeout handling for slow-loading components</li>
<li> Skeleton components provide immediate visual feedback during loading</li>
<li> All components follow the glassmorphism design system</li>
<li> Loading states include accessibility announcements for screen readers</li>
</ul>
</Card>
</div>
);
}
export default ErrorBoundaryDemo;

View File

@@ -0,0 +1,484 @@
import type { ReactNode } from 'react';
import React, { Component } from 'react';
import { Alert } from '../ui/Alert';
import { Button } from '../ui/Button';
import { Card } from '../ui/Card';
import type { BoundaryError, ErrorSeverity, ErrorType, RecoveryStrategy } from '../../types/errors';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: React.ErrorInfo | null;
errorId: string;
retryCount: number;
}
interface AppErrorBoundaryProps {
children: ReactNode;
fallback?: React.ComponentType<ErrorFallbackProps>;
onError?: (error: BoundaryError) => void;
maxRetries?: number;
resetKeys?: (string | number)[];
resetOnPropsChange?: boolean;
isolate?: boolean;
}
export interface ErrorFallbackProps {
error: Error;
errorInfo: React.ErrorInfo;
retry: () => void;
resetErrorBoundary: () => void;
errorId: string;
retryCount: number;
}
/**
* Determines error type based on error message and properties
*/
function getErrorType(error: Error): ErrorType {
const message = error.message.toLowerCase();
if (message.includes('network') || message.includes('fetch')) {
return 'network';
}
if (message.includes('unauthorized') || message.includes('forbidden')) {
return 'auth';
}
if (message.includes('permission') || message.includes('access denied')) {
return 'permission';
}
if (message.includes('timeout')) {
return 'timeout';
}
if (message.includes('rate limit')) {
return 'rate_limit';
}
if (message.includes('validation') || message.includes('invalid')) {
return 'validation';
}
return 'generic';
}
/**
* Determines error severity based on error type and context
*/
function getErrorSeverity(errorType: ErrorType, error: Error): ErrorSeverity {
// Critical errors that break the entire app
if (error.name === 'ChunkLoadError' || error.message.includes('Loading chunk')) {
return 'critical';
}
switch (errorType) {
case 'network':
return 'medium';
case 'auth':
return 'high';
case 'permission':
return 'medium';
case 'timeout':
return 'low';
case 'rate_limit':
return 'low';
case 'validation':
return 'low';
default:
return 'medium';
}
}
/**
* Default error fallback component with glassmorphism styling
*/
function DefaultErrorFallback({
error,
errorInfo,
retry,
resetErrorBoundary,
errorId,
retryCount
}: ErrorFallbackProps) {
const errorType = getErrorType(error);
const severity = getErrorSeverity(errorType, error);
const getErrorTitle = (type: ErrorType): string => {
switch (type) {
case 'network':
return 'Connection Error';
case 'auth':
return 'Authentication Required';
case 'permission':
return 'Access Denied';
case 'timeout':
return 'Request Timeout';
case 'rate_limit':
return 'Rate Limit Exceeded';
case 'validation':
return 'Validation Error';
default:
return 'Something went wrong';
}
};
const getErrorDescription = (type: ErrorType): string => {
switch (type) {
case 'network':
return 'Unable to connect to the server. Please check your internet connection and try again.';
case 'auth':
return 'Your session has expired. Please log in again to continue.';
case 'permission':
return "You don't have permission to access this resource.";
case 'timeout':
return 'The request took too long to complete. Please try again.';
case 'rate_limit':
return 'Too many requests. Please wait a moment before trying again.';
case 'validation':
return 'There was an issue with the provided data. Please check your input and try again.';
default:
return 'An unexpected error occurred. Our team has been notified.';
}
};
const getRecoveryStrategy = (type: ErrorType): RecoveryStrategy => {
switch (type) {
case 'network':
case 'timeout':
return 'retry';
case 'auth':
return 'redirect';
case 'permission':
return 'fallback';
default:
return 'reload';
}
};
const strategy = getRecoveryStrategy(errorType);
const title = getErrorTitle(errorType);
const description = getErrorDescription(errorType);
const handleAuthRedirect = () => {
window.location.href = '/login';
};
const handleReload = () => {
window.location.reload();
};
const canRetry = retryCount < 3 && (strategy === 'retry' || errorType === 'generic');
return (
<div className="min-h-screen bg-gradient-to-br from-bg-primary to-bg-secondary flex items-center justify-center p-lg">
<Card className="max-w-md w-full mx-auto">
<div className="text-center space-y-lg">
{/* Error Icon */}
<div className="mx-auto w-16 h-16 rounded-full bg-error-bg border border-error-border flex items-center justify-center">
<svg
className="w-8 h-8 text-error-accent"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
{/* Error Title */}
<h1 className="text-2xl font-bold text-text-primary">
{title}
</h1>
{/* Error Description */}
<p className="text-text-secondary">
{description}
</p>
{/* Error Details (Development Mode) */}
{process.env.NODE_ENV === 'development' && (
<details className="text-left">
<summary className="text-sm text-text-muted cursor-pointer mb-sm">
Technical Details
</summary>
<div className="bg-bg-tertiary rounded-md p-sm border border-border-default">
<p className="text-xs font-mono text-text-muted mb-xs">
Error ID: {errorId}
</p>
<p className="text-xs font-mono text-text-muted mb-xs">
Type: {errorType} | Severity: {severity}
</p>
<p className="text-xs font-mono text-error-text break-all">
{error.message}
</p>
{errorInfo.componentStack && (
<details className="mt-xs">
<summary className="text-xs text-text-muted cursor-pointer">
Component Stack
</summary>
<pre className="text-xs text-text-muted mt-xs overflow-auto">
{errorInfo.componentStack}
</pre>
</details>
)}
</div>
</details>
)}
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-sm justify-center">
{canRetry && (
<Button
onClick={retry}
variant="primary"
size="md"
className="order-1"
>
Try Again
{retryCount > 0 && ` (${retryCount}/3)`}
</Button>
)}
{errorType === 'auth' && (
<Button
onClick={handleAuthRedirect}
variant="primary"
size="md"
className="order-1"
>
Sign In
</Button>
)}
{(strategy === 'reload' || !canRetry) && errorType !== 'auth' && (
<Button
onClick={handleReload}
variant="primary"
size="md"
className="order-1"
>
Reload Page
</Button>
)}
<Button
onClick={resetErrorBoundary}
variant="secondary"
size="md"
className="order-2"
>
Reset
</Button>
</div>
{/* Support Information */}
<div className="pt-lg border-t border-border-muted">
<p className="text-sm text-text-muted">
If this problem persists, please{' '}
<a
href="mailto:support@blackcanyontickets.com"
className="text-gold-text hover:text-gold-400 transition-colors"
>
contact support
</a>
{' '}with error ID: <code className="font-mono">{errorId}</code>
</p>
</div>
</div>
</Card>
</div>
);
}
/**
* Global error boundary component for catching and handling React errors
*/
export class AppErrorBoundary extends Component<AppErrorBoundaryProps, ErrorBoundaryState> {
private resetTimeoutId: number | null = null;
constructor(props: AppErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
errorId: '',
retryCount: 0
};
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
// Generate unique error ID
const errorId = `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
return {
hasError: true,
error,
errorId
};
}
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
const errorType = getErrorType(error);
const severity = getErrorSeverity(errorType, error);
const boundaryError: BoundaryError = {
error,
errorInfo,
errorType,
severity,
timestamp: new Date(),
userAgent: navigator.userAgent,
url: window.location.href,
// userId would come from auth context in real app
};
// Update state with error info
this.setState({ errorInfo });
// Call error handler if provided
this.props.onError?.(boundaryError);
// Log error to console in development
if (process.env.NODE_ENV === 'development') {
console.group('🚨 Error Boundary Caught Error');
console.error('Error:', error);
console.error('Error Info:', errorInfo);
console.error('Boundary Error:', boundaryError);
console.groupEnd();
}
// Report error to monitoring service (mock in this implementation)
this.reportError(boundaryError);
}
override componentDidUpdate(prevProps: AppErrorBoundaryProps) {
const { resetKeys, resetOnPropsChange } = this.props;
const { hasError } = this.state;
// Reset error boundary if resetKeys change
if (hasError && resetKeys && prevProps.resetKeys) {
const hasResetKeyChanged = resetKeys.some(
(key, index) => key !== prevProps.resetKeys![index]
);
if (hasResetKeyChanged) {
this.resetErrorBoundary();
}
}
// Reset error boundary if props change and resetOnPropsChange is true
if (hasError && resetOnPropsChange) {
this.resetErrorBoundary();
}
}
private readonly reportError = (boundaryError: BoundaryError) => {
// Mock error reporting - in production, integrate with Sentry, LogRocket, etc.
try {
const errorReport = {
...boundaryError,
// Serialize error objects
error: {
name: boundaryError.error.name,
message: boundaryError.error.message,
stack: boundaryError.error.stack
},
errorInfo: {
componentStack: boundaryError.errorInfo.componentStack
}
};
// In development, just log to console
if (process.env.NODE_ENV === 'development') {
console.log('Error Report (would be sent to monitoring service):', errorReport);
}
// In production, send to your error reporting service
// Example: Sentry.captureException(boundaryError.error, { extra: errorReport });
} catch (reportingError) {
console.error('Failed to report error:', reportingError);
}
};
private readonly resetErrorBoundary = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
errorId: '',
retryCount: 0
});
};
private readonly retry = () => {
const { maxRetries = 3 } = this.props;
const { retryCount } = this.state;
if (retryCount < maxRetries) {
this.setState(prevState => ({
...prevState,
hasError: false,
error: null,
errorInfo: null,
retryCount: prevState.retryCount + 1
}));
// Auto-reset retry count after 30 seconds
if (this.resetTimeoutId) {
clearTimeout(this.resetTimeoutId);
}
this.resetTimeoutId = window.setTimeout(() => {
this.setState({ retryCount: 0 });
}, 30000);
}
};
override componentWillUnmount() {
if (this.resetTimeoutId) {
clearTimeout(this.resetTimeoutId);
}
}
override render() {
const { hasError, error, errorInfo, errorId, retryCount } = this.state;
const { children, fallback: CustomFallback, isolate } = this.props;
if (hasError && error && errorInfo) {
const FallbackComponent = CustomFallback || DefaultErrorFallback;
const errorFallbackProps: ErrorFallbackProps = {
error,
errorInfo,
retry: this.retry,
resetErrorBoundary: this.resetErrorBoundary,
errorId,
retryCount
};
// If isolate is true, render error in a smaller container
if (isolate) {
return (
<div className="bg-error-bg border border-error-border rounded-lg p-lg">
<Alert variant="error" title="Component Error">
<FallbackComponent {...errorFallbackProps} />
</Alert>
</div>
);
}
return <FallbackComponent {...errorFallbackProps} />;
}
return children;
}
}
export default AppErrorBoundary;

View File

@@ -0,0 +1,10 @@
/**
* Error Handling Components
*
* Comprehensive error boundary and error display components
* with glassmorphism styling and recovery mechanisms.
*/
// Error boundary
export { AppErrorBoundary } from './AppErrorBoundary';
export type { ErrorFallbackProps } from './AppErrorBoundary';

View File

@@ -0,0 +1,169 @@
import React from 'react';
import { clsx } from 'clsx';
export interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg' | 'xl';
variant?: 'primary' | 'secondary' | 'accent' | 'muted';
overlay?: boolean;
text?: string;
className?: string;
}
/**
* Reusable loading spinner with glassmorphism styling
* Provides smooth animations and multiple size/variant options
*/
export function LoadingSpinner({
size = 'md',
variant = 'primary',
overlay = false,
text,
className
}: LoadingSpinnerProps) {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8',
xl: 'w-12 h-12'
};
const variantClasses = {
primary: 'text-primary-500',
secondary: 'text-secondary-500',
accent: 'text-gold-500',
muted: 'text-text-muted'
};
const textSizeClasses = {
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg',
xl: 'text-xl'
};
const spinnerElement = (
<div className="flex flex-col items-center justify-center gap-md">
{/* Spinner SVG */}
<svg
className={clsx(
'animate-spin',
sizeClasses[size],
variantClasses[variant],
className
)}
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
{/* Loading Text */}
{text && (
<p className={clsx(
'text-text-secondary animate-pulse',
textSizeClasses[size]
)}>
{text}
</p>
)}
</div>
);
// Render as overlay if specified
if (overlay) {
return (
<div className="fixed inset-0 bg-bg-overlay backdrop-blur-sm z-50 flex items-center justify-center">
<div className="bg-glass-bg border border-glass-border rounded-lg p-6xl shadow-glass-lg">
{spinnerElement}
</div>
</div>
);
}
return spinnerElement;
}
/**
* Pulse animation component for skeleton loading states
*/
export function PulseLoader({ className }: { className?: string }) {
return (
<div className={clsx('animate-pulse bg-glass-bg rounded', className)} />
);
}
/**
* Shimmer effect component for advanced skeleton loading
*/
export function ShimmerLoader({
className,
children
}: {
className?: string;
children?: React.ReactNode;
}) {
return (
<div className={clsx('relative overflow-hidden', className)}>
{children}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer" />
</div>
);
}
/**
* Dots loading animation
*/
export function DotsLoader({
size = 'md',
variant = 'primary',
className
}: Pick<LoadingSpinnerProps, 'size' | 'variant' | 'className'>) {
const dotSizeClasses = {
sm: 'w-1 h-1',
md: 'w-2 h-2',
lg: 'w-3 h-3',
xl: 'w-4 h-4'
};
const variantClasses = {
primary: 'bg-primary-500',
secondary: 'bg-secondary-500',
accent: 'bg-gold-500',
muted: 'bg-text-muted'
};
return (
<div className={clsx('flex items-center space-x-1', className)}>
{[0, 1, 2].map((index) => (
<div
key={index}
className={clsx(
'rounded-full animate-bounce',
dotSizeClasses[size],
variantClasses[variant]
)}
style={{
animationDelay: `${index * 0.1}s`,
animationDuration: '0.6s'
}}
/>
))}
</div>
);
}
export default LoadingSpinner;

View File

@@ -0,0 +1,210 @@
import React, { Suspense, useEffect, useState } from 'react';
import { clsx } from 'clsx';
import { Button } from '../ui/Button';
import { Card } from '../ui/Card';
import { LoadingSpinner } from './LoadingSpinner';
import { Skeleton } from './Skeleton';
export interface RouteSuspenseProps {
children: React.ReactNode;
fallback?: React.ReactNode;
timeout?: number;
skeletonType?: 'page' | 'card' | 'list' | 'table' | 'custom';
loadingText?: string;
className?: string;
onTimeout?: () => void;
enableRetry?: boolean;
}
/**
* Enhanced Suspense wrapper for route-level code splitting with timeout handling
* Provides progressive loading states and graceful error handling
*/
export function RouteSuspense({
children,
fallback,
timeout = 10000, // 10 seconds default timeout
skeletonType = 'page',
loadingText = 'Loading...',
className,
onTimeout,
enableRetry = true
}: RouteSuspenseProps) {
const [hasTimedOut, setHasTimedOut] = useState(false);
const [retryKey, setRetryKey] = useState(0);
useEffect(() => {
if (timeout <= 0) {return;}
const timeoutId = setTimeout(() => {
setHasTimedOut(true);
onTimeout?.();
}, timeout);
return () => clearTimeout(timeoutId);
}, [timeout, onTimeout, retryKey]);
const handleRetry = () => {
setHasTimedOut(false);
setRetryKey(prev => prev + 1);
};
// Show timeout error if loading takes too long
if (hasTimedOut) {
return (
<div className={clsx('min-h-screen bg-gradient-to-br from-background-primary to-background-secondary flex items-center justify-center p-lg', className)}>
<Card className="max-w-md w-full mx-auto">
<div className="text-center space-y-lg">
{/* Timeout Icon */}
<div className="mx-auto w-16 h-16 rounded-full bg-warning-bg border border-warning-border flex items-center justify-center">
<svg
className="w-8 h-8 text-warning-accent"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h1 className="text-2xl font-bold text-text-primary">
Loading Timeout
</h1>
<p className="text-text-secondary">
The page is taking longer than expected to load. This might be due to a slow connection or server issues.
</p>
<div className="flex flex-col sm:flex-row gap-sm justify-center">
{enableRetry && (
<Button
onClick={handleRetry}
variant="primary"
size="md"
className="order-1"
>
Try Again
</Button>
)}
<Button
onClick={() => window.location.reload()}
variant="secondary"
size="md"
className="order-2"
>
Reload Page
</Button>
</div>
<div className="pt-lg border-t border-border-muted">
<p className="text-sm text-text-muted">
If this problem persists, please check your internet connection or{' '}
<a
href="mailto:support@blackcanyontickets.com"
className="text-gold-text hover:text-gold-400 transition-colors"
>
contact support
</a>
</p>
</div>
</div>
</Card>
</div>
);
}
// Create default fallback based on skeleton type
const createDefaultFallback = () => {
switch (skeletonType) {
case 'page':
return <Skeleton.Page loadingText={loadingText} />;
case 'card':
return <Skeleton.Card loadingText={loadingText} />;
case 'list':
return <Skeleton.List loadingText={loadingText} />;
case 'table':
return <Skeleton.Table loadingText={loadingText} />;
case 'custom':
default:
return (
<div className={clsx('flex items-center justify-center min-h-96', className)}>
<LoadingSpinner
size="lg"
variant="accent"
text={loadingText}
/>
</div>
);
}
};
const defaultFallback = fallback || createDefaultFallback();
return (
<div
key={retryKey}
className={className}
role="main"
aria-live="polite"
aria-label="Loading content"
>
<Suspense fallback={defaultFallback}>
{children}
</Suspense>
</div>
);
}
/**
* Higher-order component for wrapping routes with suspense
*/
export function withRouteSuspense<P extends object>(
Component: React.ComponentType<P>,
suspenseProps?: Omit<RouteSuspenseProps, 'children'>
) {
const WrappedComponent = (props: P) => (
<RouteSuspense {...suspenseProps}>
<Component {...props} />
</RouteSuspense>
);
WrappedComponent.displayName = `withRouteSuspense(${Component.displayName || Component.name})`;
return WrappedComponent;
}
/**
* Hook for programmatic timeout handling
*/
export function useLoadingTimeout(
timeout: number = 10000,
onTimeout?: () => void
) {
const [hasTimedOut, setHasTimedOut] = useState(false);
useEffect(() => {
if (timeout <= 0) {return;}
const timeoutId = setTimeout(() => {
setHasTimedOut(true);
onTimeout?.();
}, timeout);
return () => clearTimeout(timeoutId);
}, [timeout, onTimeout]);
const resetTimeout = () => setHasTimedOut(false);
return { hasTimedOut, resetTimeout };
}
export default RouteSuspense;

View File

@@ -0,0 +1,423 @@
import { clsx } from 'clsx';
import { Card } from '../ui/Card';
import { LoadingSpinner } from './LoadingSpinner';
export interface SkeletonProps {
className?: string;
rounded?: boolean;
animate?: boolean;
}
export interface SkeletonLayoutProps {
loadingText?: string;
className?: string;
}
/**
* Base skeleton component with glassmorphism styling
*/
export function BaseSkeleton({
className,
rounded = true,
animate = true
}: SkeletonProps) {
return (
<div
className={clsx(
'bg-glass-bg border border-glass-border',
{
'animate-pulse': animate,
rounded,
'rounded-lg': rounded
},
className
)}
role="status"
aria-label="Loading..."
/>
);
}
/**
* Skeleton for text content
*/
export function TextSkeleton({
lines = 1,
className,
animate = true
}: {
lines?: number;
className?: string;
animate?: boolean;
}) {
return (
<div className={clsx('space-y-2', className)}>
{Array.from({ length: lines }).map((_, index) => (
<BaseSkeleton
key={index}
className={clsx(
'h-4',
index === lines - 1 ? 'w-3/4' : 'w-full'
)}
animate={animate}
/>
))}
</div>
);
}
/**
* Skeleton for avatars and circular elements
*/
export function AvatarSkeleton({
size = 'md',
className
}: {
size?: 'sm' | 'md' | 'lg' | 'xl';
className?: string;
}) {
const sizeClasses = {
sm: 'w-8 h-8',
md: 'w-12 h-12',
lg: 'w-16 h-16',
xl: 'w-24 h-24'
};
return (
<BaseSkeleton
className={clsx('rounded-full', sizeClasses[size], className)}
/>
);
}
/**
* Skeleton for buttons
*/
export function ButtonSkeleton({
size = 'md',
className
}: {
size?: 'sm' | 'md' | 'lg';
className?: string;
}) {
const sizeClasses = {
sm: 'h-8 w-20',
md: 'h-10 w-24',
lg: 'h-12 w-32'
};
return (
<BaseSkeleton
className={clsx('rounded-lg', sizeClasses[size], className)}
/>
);
}
/**
* Skeleton for cards
*/
function CardSkeleton({ loadingText, className }: SkeletonLayoutProps) {
return (
<div className={clsx('space-y-6', className)}>
{/* Loading indicator */}
{loadingText && (
<div className="text-center mb-8">
<LoadingSpinner size="md" variant="accent" text={loadingText} />
</div>
)}
{/* Card skeletons */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, index) => (
<Card key={index} className="p-6">
<div className="space-y-4">
{/* Header */}
<div className="flex items-center space-x-3">
<AvatarSkeleton size="md" />
<div className="flex-1 space-y-2">
<BaseSkeleton className="h-5 w-3/4" />
<BaseSkeleton className="h-3 w-1/2" />
</div>
</div>
{/* Content */}
<div className="space-y-2">
<BaseSkeleton className="h-4 w-full" />
<BaseSkeleton className="h-4 w-5/6" />
<BaseSkeleton className="h-4 w-4/6" />
</div>
{/* Footer */}
<div className="flex justify-between items-center pt-4">
<BaseSkeleton className="h-4 w-1/4" />
<ButtonSkeleton size="sm" />
</div>
</div>
</Card>
))}
</div>
</div>
);
}
/**
* Skeleton for list items
*/
function ListSkeleton({ loadingText, className }: SkeletonLayoutProps) {
return (
<div className={clsx('space-y-4', className)}>
{/* Loading indicator */}
{loadingText && (
<div className="text-center mb-8">
<LoadingSpinner size="md" variant="accent" text={loadingText} />
</div>
)}
{/* List items */}
{Array.from({ length: 8 }).map((_, index) => (
<div key={index} className="flex items-center space-x-4 p-4 bg-glass-bg border border-glass-border rounded-lg">
<AvatarSkeleton size="md" />
<div className="flex-1 space-y-2">
<BaseSkeleton className="h-5 w-3/4" />
<BaseSkeleton className="h-3 w-1/2" />
</div>
<div className="flex space-x-2">
<ButtonSkeleton size="sm" />
<ButtonSkeleton size="sm" />
</div>
</div>
))}
</div>
);
}
/**
* Skeleton for tables
*/
function TableSkeleton({ loadingText, className }: SkeletonLayoutProps) {
return (
<div className={clsx('space-y-4', className)}>
{/* Loading indicator */}
{loadingText && (
<div className="text-center mb-8">
<LoadingSpinner size="md" variant="accent" text={loadingText} />
</div>
)}
<Card className="overflow-hidden">
{/* Table header */}
<div className="bg-glass-bg border-b border-glass-border px-6 py-4">
<div className="grid grid-cols-5 gap-4">
{Array.from({ length: 5 }).map((_, index) => (
<BaseSkeleton key={index} className="h-4 w-3/4" />
))}
</div>
</div>
{/* Table rows */}
<div className="divide-y divide-glass-border">
{Array.from({ length: 10 }).map((_, rowIndex) => (
<div key={rowIndex} className="px-6 py-4">
<div className="grid grid-cols-5 gap-4 items-center">
{Array.from({ length: 5 }).map((_, colIndex) => {
if (colIndex === 0) {
return (
<div key={colIndex} className="flex items-center space-x-3">
<AvatarSkeleton size="sm" />
<BaseSkeleton className="h-4 w-20" />
</div>
);
}
if (colIndex === 4) {
return (
<div key={colIndex} className="flex space-x-2">
<ButtonSkeleton size="sm" />
</div>
);
}
return <BaseSkeleton key={colIndex} className="h-4 w-16" />;
})}
</div>
</div>
))}
</div>
</Card>
</div>
);
}
/**
* Skeleton for full page layouts
*/
function PageSkeleton({ loadingText, className }: SkeletonLayoutProps) {
return (
<div className={clsx('min-h-screen bg-gradient-to-br from-background-primary to-background-secondary', className)}>
{/* Header skeleton */}
<div className="bg-glass-bg border-b border-glass-border px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<BaseSkeleton className="h-8 w-32" />
<div className="hidden md:flex space-x-6">
{Array.from({ length: 4 }).map((_, index) => (
<BaseSkeleton key={index} className="h-4 w-16" />
))}
</div>
</div>
<div className="flex items-center space-x-4">
<AvatarSkeleton size="sm" />
<ButtonSkeleton size="sm" />
</div>
</div>
</div>
{/* Main content area */}
<div className="container mx-auto px-6 py-8">
{/* Loading indicator */}
{loadingText && (
<div className="text-center mb-12">
<LoadingSpinner size="lg" variant="accent" text={loadingText} />
</div>
)}
{/* Page title and actions */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
<div className="space-y-2">
<BaseSkeleton className="h-8 w-64" />
<BaseSkeleton className="h-4 w-96" />
</div>
<div className="mt-4 sm:mt-0 flex space-x-3">
<ButtonSkeleton size="md" />
<ButtonSkeleton size="md" />
</div>
</div>
{/* Stats cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
{Array.from({ length: 4 }).map((_, index) => (
<Card key={index} className="p-6">
<div className="space-y-3">
<BaseSkeleton className="h-4 w-16" />
<BaseSkeleton className="h-8 w-20" />
<BaseSkeleton className="h-3 w-24" />
</div>
</Card>
))}
</div>
{/* Main content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Primary content */}
<div className="lg:col-span-2 space-y-6">
<Card className="p-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<BaseSkeleton className="h-6 w-32" />
<ButtonSkeleton size="sm" />
</div>
<div className="space-y-3">
{Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="flex items-center space-x-4 p-3 bg-glass-bg rounded border border-glass-border">
<AvatarSkeleton size="sm" />
<div className="flex-1 space-y-1">
<BaseSkeleton className="h-4 w-3/4" />
<BaseSkeleton className="h-3 w-1/2" />
</div>
<BaseSkeleton className="h-6 w-16" />
</div>
))}
</div>
</div>
</Card>
</div>
{/* Sidebar */}
<div className="space-y-6">
<Card className="p-6">
<div className="space-y-4">
<BaseSkeleton className="h-5 w-24" />
<div className="space-y-3">
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="flex items-center justify-between">
<BaseSkeleton className="h-4 w-20" />
<BaseSkeleton className="h-4 w-12" />
</div>
))}
</div>
</div>
</Card>
<Card className="p-6">
<div className="space-y-4">
<BaseSkeleton className="h-5 w-28" />
<BaseSkeleton className="h-32 w-full" />
<ButtonSkeleton size="md" className="w-full" />
</div>
</Card>
</div>
</div>
</div>
</div>
);
}
/**
* Skeleton for form layouts
*/
function FormSkeleton({ loadingText, className }: SkeletonLayoutProps) {
return (
<div className={clsx('space-y-6', className)}>
{/* Loading indicator */}
{loadingText && (
<div className="text-center mb-8">
<LoadingSpinner size="md" variant="accent" text={loadingText} />
</div>
)}
<Card className="p-6">
<div className="space-y-6">
{/* Form title */}
<BaseSkeleton className="h-6 w-48" />
{/* Form fields */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="space-y-2">
<BaseSkeleton className="h-4 w-24" />
<BaseSkeleton className="h-10 w-full" />
</div>
))}
</div>
{/* Text area */}
<div className="space-y-2">
<BaseSkeleton className="h-4 w-32" />
<BaseSkeleton className="h-24 w-full" />
</div>
{/* Form actions */}
<div className="flex justify-end space-x-3 pt-6 border-t border-glass-border">
<ButtonSkeleton size="md" />
<ButtonSkeleton size="md" />
</div>
</div>
</Card>
</div>
);
}
// Export all skeleton components
export const Skeleton = {
Base: BaseSkeleton,
Text: TextSkeleton,
Avatar: AvatarSkeleton,
Button: ButtonSkeleton,
Card: CardSkeleton,
List: ListSkeleton,
Table: TableSkeleton,
Page: PageSkeleton,
Form: FormSkeleton
};
export default Skeleton;

View File

@@ -0,0 +1,24 @@
/**
* Loading Components
*
* Comprehensive collection of loading states and skeleton components
* with glassmorphism styling and accessibility features.
*/
// Main loading components
export { LoadingSpinner, PulseLoader, ShimmerLoader, DotsLoader } from './LoadingSpinner';
export type { LoadingSpinnerProps } from './LoadingSpinner';
// Suspense and route loading
export { RouteSuspense, withRouteSuspense, useLoadingTimeout } from './RouteSuspense';
export type { RouteSuspenseProps } from './RouteSuspense';
// Skeleton components
export {
BaseSkeleton,
TextSkeleton,
AvatarSkeleton,
ButtonSkeleton,
Skeleton
} from './Skeleton';
export type { SkeletonProps, SkeletonLayoutProps } from './Skeleton';

View File

@@ -0,0 +1,357 @@
import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { clsx } from 'clsx';
import { AppLayout } from '../components/layout/AppLayout';
import { Alert } from '../components/ui/Alert';
import { Button } from '../components/ui/Button';
import { Card } from '../components/ui/Card';
export type ErrorPageType = '404' | '403' | '500' | 'network' | 'timeout' | 'maintenance';
export interface ErrorPageProps {
type?: ErrorPageType;
title?: string;
message?: string;
details?: string;
showRetry?: boolean;
showHome?: boolean;
showBack?: boolean;
customActions?: React.ReactNode;
className?: string;
}
/**
* Get error configuration based on error type
*/
interface ErrorConfig {
title: string;
message: string;
icon: React.ReactNode;
showRetry?: boolean;
showHome?: boolean;
showBack?: boolean;
}
function getErrorConfig(type: ErrorPageType): ErrorConfig {
const configs: Record<ErrorPageType, ErrorConfig> = {
'404': {
title: 'Page Not Found',
message: 'The page you\'re looking for doesn\'t exist or has been moved.',
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6 4h6M12 3v9.172a4 4 0 00-1.172 2.828L12 16l1.172-1.172A4 4 0 0012 12.172V3z"
/>
),
showHome: true,
showBack: true
},
'403': {
title: 'Access Denied',
message: 'You don\'t have permission to access this resource.',
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
),
showHome: true,
showBack: true
},
'500': {
title: 'Server Error',
message: 'Something went wrong on our end. We\'re working to fix it.',
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
),
showRetry: true,
showHome: true
},
'network': {
title: 'Connection Error',
message: 'Unable to connect to the server. Please check your internet connection.',
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"
/>
),
showRetry: true,
showHome: true
},
'timeout': {
title: 'Request Timeout',
message: 'The request took too long to complete. Please try again.',
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
),
showRetry: true,
showHome: true
},
'maintenance': {
title: 'Under Maintenance',
message: 'We\'re currently performing scheduled maintenance. Please check back soon.',
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
),
showHome: false
}
};
return configs[type] || configs['500'];
}
/**
* Comprehensive error page component with glassmorphism styling
*/
export function ErrorPage({
type = '500',
title,
message,
details,
showRetry,
showHome,
showBack,
customActions,
className
}: ErrorPageProps) {
const navigate = useNavigate();
const location = useLocation();
const config = getErrorConfig(type);
const errorTitle = title || config.title;
const errorMessage = message || config.message;
const shouldShowRetry = showRetry !== undefined ? showRetry : config.showRetry;
const shouldShowHome = showHome !== undefined ? showHome : config.showHome;
const shouldShowBack = showBack !== undefined ? showBack : config.showBack;
const handleRetry = () => {
window.location.reload();
};
const handleGoHome = () => {
navigate('/');
};
const handleGoBack = () => {
if (window.history.length > 1) {
navigate(-1);
} else {
navigate('/');
}
};
const getErrorColor = (errorType: ErrorPageType) => {
switch (errorType) {
case '404':
return 'text-info-accent';
case '403':
return 'text-warning-accent';
case '500':
case 'network':
case 'timeout':
return 'text-error-accent';
case 'maintenance':
return 'text-gold-500';
default:
return 'text-error-accent';
}
};
const getErrorBg = (errorType: ErrorPageType) => {
switch (errorType) {
case '404':
return 'bg-info-bg border-info-border';
case '403':
return 'bg-warning-bg border-warning-border';
case '500':
case 'network':
case 'timeout':
return 'bg-error-bg border-error-border';
case 'maintenance':
return 'bg-gold-bg border-gold-border';
default:
return 'bg-error-bg border-error-border';
}
};
return (
<AppLayout>
<div className={clsx('min-h-screen bg-gradient-to-br from-background-primary to-background-secondary flex items-center justify-center p-lg', className)}>
<Card className="max-w-lg w-full mx-auto">
<div className="text-center space-y-lg">
{/* Error Icon */}
<div className={clsx(
'mx-auto w-20 h-20 rounded-full flex items-center justify-center',
getErrorBg(type)
)}>
<svg
className={clsx('w-10 h-10', getErrorColor(type))}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
{config.icon}
</svg>
</div>
{/* Error Code */}
{(type === '404' || type === '403' || type === '500') && (
<div className="text-6xl font-bold text-text-muted opacity-50">
{type}
</div>
)}
{/* Error Title */}
<h1 className="text-3xl font-bold text-text-primary">
{errorTitle}
</h1>
{/* Error Message */}
<p className="text-lg text-text-secondary max-w-md mx-auto">
{errorMessage}
</p>
{/* Error Details */}
{details && (
<Alert variant="info" title="Additional Information">
<p className="text-sm">{details}</p>
</Alert>
)}
{/* Development Details */}
{process.env.NODE_ENV === 'development' && (
<details className="text-left">
<summary className="text-sm text-text-muted cursor-pointer mb-sm">
Development Details
</summary>
<div className="bg-bg-tertiary rounded-md p-sm border border-border-default">
<p className="text-xs font-mono text-text-muted mb-xs">
Current URL: {location.pathname + location.search}
</p>
<p className="text-xs font-mono text-text-muted mb-xs">
Error Type: {type}
</p>
<p className="text-xs font-mono text-text-muted">
Timestamp: {new Date().toISOString()}
</p>
</div>
</details>
)}
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-sm justify-center pt-lg">
{shouldShowRetry && (
<Button
onClick={handleRetry}
variant="primary"
size="md"
className="order-1"
>
Try Again
</Button>
)}
{shouldShowHome && (
<Button
onClick={handleGoHome}
variant="primary"
size="md"
className="order-1"
>
Go Home
</Button>
)}
{shouldShowBack && (
<Button
onClick={handleGoBack}
variant="secondary"
size="md"
className="order-2"
>
Go Back
</Button>
)}
{customActions}
</div>
{/* Support Information */}
<div className="pt-lg border-t border-border-muted">
<p className="text-sm text-text-muted">
Need help?{' '}
<a
href="mailto:support@blackcanyontickets.com"
className="text-gold-text hover:text-gold-400 transition-colors"
>
Contact Support
</a>
{' '}or check our{' '}
<a
href="/help"
className="text-gold-text hover:text-gold-400 transition-colors"
>
Help Center
</a>
</p>
</div>
</div>
</Card>
</div>
</AppLayout>
);
}
/**
* Specific error page components for common scenarios
*/
export function NotFoundPage(props: Omit<ErrorPageProps, 'type'>) {
return <ErrorPage type="404" {...props} />;
}
export function UnauthorizedPage(props: Omit<ErrorPageProps, 'type'>) {
return <ErrorPage type="403" {...props} />;
}
export function ServerErrorPage(props: Omit<ErrorPageProps, 'type'>) {
return <ErrorPage type="500" {...props} />;
}
export function NetworkErrorPage(props: Omit<ErrorPageProps, 'type'>) {
return <ErrorPage type="network" {...props} />;
}
export function TimeoutErrorPage(props: Omit<ErrorPageProps, 'type'>) {
return <ErrorPage type="timeout" {...props} />;
}
export function MaintenancePage(props: Omit<ErrorPageProps, 'type'>) {
return <ErrorPage type="maintenance" {...props} />;
}
export default ErrorPage;

View File

@@ -0,0 +1,98 @@
/**
* Error types and interfaces for comprehensive error handling
*/
export type ErrorType = 'network' | 'auth' | 'permission' | 'validation' | 'generic' | 'timeout' | 'rate_limit';
export interface ErrorInfo {
type: ErrorType;
message: string;
code?: string;
details?: string;
recoverable: boolean;
retryAction?: () => void;
timestamp?: Date;
componentStack?: string;
errorBoundary?: string;
}
export interface NetworkError extends ErrorInfo {
type: 'network';
status?: number;
endpoint?: string;
}
export interface AuthError extends ErrorInfo {
type: 'auth';
redirectTo?: string;
}
export interface PermissionError extends ErrorInfo {
type: 'permission';
requiredPermission?: string;
currentPermissions?: string[];
}
export interface ValidationError extends ErrorInfo {
type: 'validation';
field?: string;
value?: unknown;
}
export interface TimeoutError extends ErrorInfo {
type: 'timeout';
operation?: string;
timeoutMs?: number;
}
export interface RateLimitError extends ErrorInfo {
type: 'rate_limit';
retryAfter?: number;
limit?: number;
}
export type AppError = NetworkError | AuthError | PermissionError | ValidationError | TimeoutError | RateLimitError | ErrorInfo;
/**
* Error severity levels for different handling strategies
*/
export type ErrorSeverity = 'low' | 'medium' | 'high' | 'critical';
/**
* Enhanced error interface for error boundaries
*/
export interface BoundaryError {
error: Error;
errorInfo: React.ErrorInfo;
errorType: ErrorType;
severity: ErrorSeverity;
timestamp: Date;
userAgent?: string;
url?: string;
userId?: string;
}
/**
* Error recovery strategies
*/
export type RecoveryStrategy = 'retry' | 'reload' | 'redirect' | 'fallback' | 'none';
export interface ErrorRecovery {
strategy: RecoveryStrategy;
action?: () => void;
fallbackComponent?: React.ComponentType;
maxRetries?: number;
retryDelay?: number;
}
/**
* Error reporting configuration
*/
export interface ErrorReportConfig {
enabled: boolean;
includeUserAgent: boolean;
includeUrl: boolean;
includeUserId: boolean;
includeBreadcrumbs: boolean;
endpoint?: string;
}