import React, { useState, useCallback } from 'react'; import { FiAlertTriangle, FiRefreshCw, FiX, FiInfo, FiWifi } from 'react-icons/fi'; import Typography from '../ui/Typography'; import { Button } from '../ui/Button'; import { Alert, AlertTitle, AlertDescription } from '../ui/alert'; import { cn } from '../../lib/utils'; export interface ErrorInfo { category: string; severity: string; message: string; userMessage: string; code: string; recoverable: boolean; retryAfter?: number; context?: Record; } export interface RecoveryAction { type: 'retry' | 'refresh' | 'redirect' | 'manual' | 'none'; label: string; description: string; autoExecute?: boolean; delayMs?: number; } interface ErrorDisplayProps { error: ErrorInfo; recoveryActions?: RecoveryAction[]; onRetry?: () => void; onDismiss?: () => void; className?: string; } /** * Enhanced error display component with recovery actions and user guidance * * @param error - Error information object * @param recoveryActions - Available recovery actions * @param onRetry - Callback for retry action * @param onDismiss - Callback for dismissing the error * @param className - Additional CSS classes */ const ErrorDisplay: React.FC = ({ error, recoveryActions = [], onRetry, onDismiss, className, }) => { const [isRetrying, setIsRetrying] = useState(false); const [countdown, setCountdown] = useState(null); const getSeverityStyles = useCallback((severity: string) => { switch (severity.toLowerCase()) { case 'low': return { container: 'bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800', icon: 'text-yellow-600 dark:text-yellow-400', text: 'text-yellow-800 dark:text-yellow-200', }; case 'medium': return { container: 'bg-orange-50 border-orange-200 dark:bg-orange-900/20 dark:border-orange-800', icon: 'text-orange-600 dark:text-orange-400', text: 'text-orange-800 dark:text-orange-200', }; case 'high': case 'critical': return { container: 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800', icon: 'text-red-600 dark:text-red-400', text: 'text-red-800 dark:text-red-200', }; default: return { container: 'bg-gray-50 border-gray-200 dark:bg-gray-900/20 dark:border-gray-800', icon: 'text-gray-600 dark:text-gray-400', text: 'text-gray-800 dark:text-gray-200', }; } }, []); const getErrorIcon = useCallback((category: string) => { switch (category.toLowerCase()) { case 'network': return FiWifi; case 'validation': return FiInfo; default: return FiAlertTriangle; } }, []); const handleRetry = useCallback(async () => { if (isRetrying || !onRetry) return; setIsRetrying(true); try { await onRetry(); } finally { setIsRetrying(false); } }, [isRetrying, onRetry]); const handleAutoRetry = useCallback((action: RecoveryAction) => { if (!action.autoExecute || !action.delayMs) return; let timeLeft = Math.ceil(action.delayMs / 1000); setCountdown(timeLeft); const timer = setInterval(() => { timeLeft -= 1; setCountdown(timeLeft); if (timeLeft <= 0) { clearInterval(timer); setCountdown(null); handleRetry(); } }, 1000); return () => clearInterval(timer); }, [handleRetry]); const styles = getSeverityStyles(error.severity); const ErrorIcon = getErrorIcon(error.category); const getActionButton = useCallback((action: RecoveryAction, index: number) => { const isRetryAction = action.type === 'retry'; const isDisabled = isRetrying || (countdown !== null && countdown > 0); if (action.autoExecute && action.delayMs && countdown !== null) { return ( ); } return ( ); }, [isRetrying, countdown, handleRetry]); return (
{error.retryAfter && countdown === null && (
Automatic retry available in {Math.ceil(error.retryAfter / 1000)} seconds
)}
); }; export { ErrorDisplay }; export type { ErrorDisplayProps };