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:
218
reactrebuild0825/src/components/ErrorBoundaryDemo.tsx
Normal file
218
reactrebuild0825/src/components/ErrorBoundaryDemo.tsx
Normal 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;
|
||||
484
reactrebuild0825/src/components/errors/AppErrorBoundary.tsx
Normal file
484
reactrebuild0825/src/components/errors/AppErrorBoundary.tsx
Normal 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;
|
||||
10
reactrebuild0825/src/components/errors/index.ts
Normal file
10
reactrebuild0825/src/components/errors/index.ts
Normal 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';
|
||||
169
reactrebuild0825/src/components/loading/LoadingSpinner.tsx
Normal file
169
reactrebuild0825/src/components/loading/LoadingSpinner.tsx
Normal 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;
|
||||
210
reactrebuild0825/src/components/loading/RouteSuspense.tsx
Normal file
210
reactrebuild0825/src/components/loading/RouteSuspense.tsx
Normal 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;
|
||||
423
reactrebuild0825/src/components/loading/Skeleton.tsx
Normal file
423
reactrebuild0825/src/components/loading/Skeleton.tsx
Normal 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;
|
||||
24
reactrebuild0825/src/components/loading/index.ts
Normal file
24
reactrebuild0825/src/components/loading/index.ts
Normal 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';
|
||||
357
reactrebuild0825/src/pages/ErrorPage.tsx
Normal file
357
reactrebuild0825/src/pages/ErrorPage.tsx
Normal 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;
|
||||
98
reactrebuild0825/src/types/errors.ts
Normal file
98
reactrebuild0825/src/types/errors.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user