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