import { Component, ReactNode, ErrorInfo as ReactErrorInfo } from 'react'; import { motion } from 'framer-motion'; import { AlertTriangle, RefreshCcw, Bug, Home, WifiOff } from 'lucide-react'; import { Button } from './Button'; import { reportError } from '../../lib/error-handling'; import { classifyError, AppError } from '../../lib/error-types'; // === Types === /** Extended error info with additional metadata */ interface ExtendedErrorInfo extends ReactErrorInfo { errorName?: string; errorMessage?: string; } interface ErrorBoundaryProps { children: ReactNode; fallback?: ReactNode; onError?: (error: Error, errorInfo: ReactErrorInfo) => void; onReset?: () => void; /** Whether to show connection status indicator */ showConnectionStatus?: boolean; /** Custom error title */ errorTitle?: string; /** Custom error message */ errorMessage?: string; } interface ErrorBoundaryState { hasError: boolean; error: Error | null; errorInfo: ExtendedErrorInfo | null; appError: AppError | null; showDetails: boolean; } // === Global Error Types === type GlobalErrorType = 'unhandled-rejection' | 'error' | 'websocket' | 'network'; interface GlobalErrorEvent { type: GlobalErrorType; error: unknown; timestamp: Date; } // === Global Error Handler Registry === const globalErrorListeners = new Set<(event: GlobalErrorEvent) => void>(); export function addGlobalErrorListener(listener: (event: GlobalErrorEvent) => void): () => void { globalErrorListeners.add(listener); return () => globalErrorListeners.delete(listener); } function notifyGlobalErrorListeners(event: GlobalErrorEvent): void { globalErrorListeners.forEach(listener => { try { listener(event); } catch (e) { console.error('[GlobalErrorHandler] Listener error:', e); } }); } // === Setup Global Error Handlers === let globalHandlersSetup = false; export function setupGlobalErrorHandlers(): () => void { if (globalHandlersSetup) { return () => {}; } globalHandlersSetup = true; // Handle unhandled promise rejections const handleRejection = (event: PromiseRejectionEvent) => { console.error('[GlobalErrorHandler] Unhandled rejection:', event.reason); notifyGlobalErrorListeners({ type: 'unhandled-rejection', error: event.reason, timestamp: new Date(), }); // Prevent default browser error logging (we handle it ourselves) event.preventDefault(); }; // Handle uncaught errors const handleError = (event: ErrorEvent) => { console.error('[GlobalErrorHandler] Uncaught error:', event.error); notifyGlobalErrorListeners({ type: 'error', error: event.error, timestamp: new Date(), }); // Let the error boundary handle it if possible }; // Handle WebSocket errors globally const handleWebSocketError = (event: Event) => { if (event.target instanceof WebSocket) { console.error('[GlobalErrorHandler] WebSocket error:', event); notifyGlobalErrorListeners({ type: 'websocket', error: new Error('WebSocket connection error'), timestamp: new Date(), }); } }; window.addEventListener('unhandledrejection', handleRejection); window.addEventListener('error', handleError); window.addEventListener('error', handleWebSocketError, true); // Capture phase for WebSocket return () => { window.removeEventListener('unhandledrejection', handleRejection); window.removeEventListener('error', handleError); window.removeEventListener('error', handleWebSocketError, true); globalHandlersSetup = false; }; } /** * GlobalErrorBoundary Component * * Root-level error boundary that catches all React errors and global errors. * Displays a user-friendly error screen with recovery options. */ export class GlobalErrorBoundary extends Component { private cleanupGlobalHandlers: (() => void) | null = null; constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null, errorInfo: null, appError: null, showDetails: false, }; } static getDerivedStateFromError(error: Error): Partial { const appError = classifyError(error); return { hasError: true, error, appError, }; } componentDidMount() { // Setup global error handlers this.cleanupGlobalHandlers = setupGlobalErrorHandlers(); // Listen for global errors and update state const unsubscribe = addGlobalErrorListener((event) => { if (!this.state.hasError) { const appError = classifyError(event.error); this.setState({ hasError: true, error: event.error instanceof Error ? event.error : new Error(String(event.error)), appError, errorInfo: null, }); } }); // Store cleanup function this.cleanupGlobalHandlers = () => { unsubscribe(); }; } componentWillUnmount() { this.cleanupGlobalHandlers?.(); } componentDidCatch(error: Error, errorInfo: ReactErrorInfo) { const { onError } = this.props; // Classify the error const appError = classifyError(error); // Update state with extended error info const extendedErrorInfo: ExtendedErrorInfo = { componentStack: errorInfo.componentStack, errorName: error.name || 'Unknown Error', errorMessage: error.message || 'An unexpected error occurred', }; this.setState({ errorInfo: extendedErrorInfo, appError, }); // Call optional error handler if (onError) { onError(error, errorInfo); } // Report to error tracking reportError(error, { componentStack: errorInfo.componentStack ?? undefined, errorName: error.name, errorMessage: error.message, }); } handleReset = () => { const { onReset } = this.props; // Reset error state this.setState({ hasError: false, error: null, errorInfo: null, appError: null, showDetails: false, }); // Call optional reset handler if (onReset) { onReset(); } }; handleReload = () => { window.location.reload(); }; handleGoHome = () => { window.location.href = '/'; }; handleReport = () => { const { error, errorInfo } = this.state; if (error) { reportError(error, { componentStack: errorInfo?.componentStack ?? undefined, errorName: errorInfo?.errorName || error.name, errorMessage: errorInfo?.errorMessage || error.message, }); // Show confirmation alert('Error reported. Thank you for your feedback.'); } }; toggleDetails = () => { this.setState(prev => ({ showDetails: !prev.showDetails })); }; render() { const { children, fallback, errorTitle, errorMessage } = this.props; const { hasError, error, errorInfo, appError, showDetails } = this.state; if (hasError && error) { // Use custom fallback if provided if (fallback) { return fallback; } // Get error display info const title = errorTitle || appError?.title || 'Something went wrong'; const message = errorMessage || appError?.message || error.message || 'An unexpected error occurred'; const category = appError?.category || 'system'; const isNetworkError = category === 'network'; return (
{/* Error Header */}
{isNetworkError ? ( ) : ( )}

{title}

{message}

{/* Error Details */}
{/* Category Badge */} {appError && (
{category.charAt(0).toUpperCase() + category.slice(1)} Error {appError.recoverable && ( Recoverable )}
)} {/* Recovery Steps */} {appError?.recoverySteps && appError.recoverySteps.length > 0 && (

Suggested Actions:

    {appError.recoverySteps.slice(0, 3).map((step, index) => (
  • {index + 1}. {step.description}
  • ))}
)} {/* Technical Details Toggle */} {/* Technical Details */} {showDetails && (
                    {errorInfo?.errorName || error.name}: {errorInfo?.errorMessage || error.message}
                    {errorInfo?.componentStack && `\n\nComponent Stack:${errorInfo.componentStack}`}
                  
)} {/* Actions */}
); } return children; } } /** * ErrorBoundary Component * * A simpler error boundary for wrapping individual components or sections. * Use GlobalErrorBoundary for the root level. */ export class ErrorBoundary extends Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null, errorInfo: null, appError: null, showDetails: false, }; } static getDerivedStateFromError(error: Error): Partial { const appError = classifyError(error); return { hasError: true, error, appError, }; } componentDidCatch(error: Error, errorInfo: ReactErrorInfo) { const { onError } = this.props; // Update state with extended error info const extendedErrorInfo: ExtendedErrorInfo = { componentStack: errorInfo.componentStack, errorName: error.name || 'Unknown Error', errorMessage: error.message || 'An unexpected error occurred', }; this.setState({ errorInfo: extendedErrorInfo, }); // Call optional error handler if (onError) { onError(error, errorInfo); } // Report error reportError(error, { componentStack: errorInfo.componentStack ?? undefined, errorName: error.name, errorMessage: error.message, }); } handleReset = () => { const { onReset } = this.props; this.setState({ hasError: false, error: null, errorInfo: null, appError: null, showDetails: false, }); if (onReset) { onReset(); } }; render() { const { children, fallback } = this.props; const { hasError, error, appError } = this.state; if (hasError && error) { if (fallback) { return fallback; } // Compact error UI for nested boundaries return (

{appError?.title || 'Error'}

{appError?.message || error.message}

); } return children; } } // === Re-export for convenience === export { GlobalErrorBoundary as RootErrorBoundary };