252 lines
8.3 KiB
TypeScript
252 lines
8.3 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { AlertCircle, RefreshCw, ArrowLeft, Home, Bug } from 'lucide-react';
|
|
import { ErrorDisplayProps } from './types';
|
|
import { ApiError } from '@/lib/api/base/ApiError';
|
|
|
|
/**
|
|
* ErrorDisplay Component
|
|
*
|
|
* Provides consistent error state handling with multiple variants:
|
|
* - full-screen: Centered error page with full viewport
|
|
* - card: Compact card-style error display
|
|
* - inline: Small inline error message
|
|
*
|
|
* Features:
|
|
* - Automatic error message extraction
|
|
* - Retry functionality
|
|
* - Navigation options (back, home)
|
|
* - Technical details toggle
|
|
* - API error specific handling
|
|
*/
|
|
export function ErrorDisplay({
|
|
error,
|
|
onRetry,
|
|
variant = 'full-screen',
|
|
actions = [],
|
|
showRetry = true,
|
|
showNavigation = true,
|
|
hideTechnicalDetails = false,
|
|
className = '',
|
|
}: ErrorDisplayProps) {
|
|
// Extract error information
|
|
const getErrorInfo = () => {
|
|
const isApiError = error instanceof ApiError;
|
|
|
|
return {
|
|
title: isApiError ? 'API Error' : 'Unexpected Error',
|
|
message: error.message || 'Something went wrong',
|
|
statusCode: isApiError ? error.context.statusCode : undefined,
|
|
details: isApiError ? error.context.responseText : undefined,
|
|
isApiError,
|
|
};
|
|
};
|
|
|
|
const errorInfo = getErrorInfo();
|
|
|
|
// Default actions
|
|
const defaultActions = [
|
|
...(showRetry && onRetry ? [{ label: 'Retry', onClick: onRetry, variant: 'primary' as const }] : []),
|
|
...(showNavigation ? [
|
|
{ label: 'Go Back', onClick: () => window.history.back(), variant: 'secondary' as const },
|
|
{ label: 'Home', onClick: () => window.location.href = '/', variant: 'outline' as const },
|
|
] : []),
|
|
...actions,
|
|
];
|
|
|
|
// Render different variants
|
|
switch (variant) {
|
|
case 'full-screen':
|
|
return (
|
|
<div
|
|
className={`fixed inset-0 z-50 bg-deep-graphite flex items-center justify-center p-6 ${className}`}
|
|
role="alert"
|
|
aria-live="assertive"
|
|
>
|
|
<div className="max-w-lg w-full text-center">
|
|
{/* Icon */}
|
|
<div className="flex justify-center mb-6">
|
|
<div className="flex h-20 w-20 items-center justify-center rounded-3xl bg-red-500/10 border border-red-500/30">
|
|
<AlertCircle className="w-10 h-10 text-red-500" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Title */}
|
|
<h2 className="text-3xl font-bold text-white mb-3">
|
|
{errorInfo.title}
|
|
</h2>
|
|
|
|
{/* Message */}
|
|
<p className="text-gray-400 text-lg mb-6 leading-relaxed">
|
|
{errorInfo.message}
|
|
</p>
|
|
|
|
{/* API Error Details */}
|
|
{errorInfo.isApiError && errorInfo.statusCode && (
|
|
<div className="mb-6 inline-flex items-center gap-2 px-4 py-2 bg-iron-gray/40 rounded-lg text-sm text-gray-300">
|
|
<span className="font-mono">HTTP {errorInfo.statusCode}</span>
|
|
{errorInfo.details && !hideTechnicalDetails && (
|
|
<span className="text-gray-500">- {errorInfo.details}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
{defaultActions.length > 0 && (
|
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
|
{defaultActions.map((action, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={action.onClick}
|
|
className={`
|
|
px-6 py-3 rounded-lg font-medium transition-all
|
|
${action.variant === 'primary' ? 'bg-red-500 hover:bg-red-600 text-white' : ''}
|
|
${action.variant === 'secondary' ? 'bg-iron-gray hover:bg-iron-gray/80 text-white' : ''}
|
|
${action.variant === 'outline' ? 'border border-gray-600 hover:border-gray-500 text-gray-300' : ''}
|
|
${action.variant === 'ghost' ? 'text-gray-400 hover:text-gray-300' : ''}
|
|
`}
|
|
>
|
|
{action.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Technical Details Toggle (for development) */}
|
|
{!hideTechnicalDetails && process.env.NODE_ENV === 'development' && error.stack && (
|
|
<details className="mt-8 text-left">
|
|
<summary className="cursor-pointer text-sm text-gray-500 hover:text-gray-400">
|
|
Technical Details
|
|
</summary>
|
|
<pre className="mt-2 p-4 bg-black/50 rounded-lg text-xs text-gray-400 overflow-x-auto">
|
|
{error.stack}
|
|
</pre>
|
|
</details>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 'card':
|
|
return (
|
|
<div
|
|
className={`bg-iron-gray/40 border border-red-500/30 rounded-xl p-6 ${className}`}
|
|
role="alert"
|
|
aria-live="assertive"
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
{/* Icon */}
|
|
<div className="flex-shrink-0">
|
|
<AlertCircle className="w-6 h-6 text-red-500" />
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold text-white mb-1">
|
|
{errorInfo.title}
|
|
</h3>
|
|
<p className="text-sm text-gray-400 mb-3">
|
|
{errorInfo.message}
|
|
</p>
|
|
|
|
{/* API Error Details */}
|
|
{errorInfo.isApiError && errorInfo.statusCode && (
|
|
<div className="text-xs font-mono text-gray-500 mb-3">
|
|
HTTP {errorInfo.statusCode}
|
|
{errorInfo.details && !hideTechnicalDetails && ` - ${errorInfo.details}`}
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
{defaultActions.length > 0 && (
|
|
<div className="flex gap-2">
|
|
{defaultActions.map((action, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={action.onClick}
|
|
className={`
|
|
px-3 py-1.5 rounded text-sm font-medium
|
|
${action.variant === 'primary' ? 'bg-red-500 hover:bg-red-600 text-white' : ''}
|
|
${action.variant === 'secondary' ? 'bg-deep-graphite hover:bg-black/60 text-white' : ''}
|
|
${action.variant === 'outline' ? 'border border-gray-600 hover:border-gray-500 text-gray-300' : ''}
|
|
${action.variant === 'ghost' ? 'text-gray-400 hover:text-gray-300' : ''}
|
|
`}
|
|
>
|
|
{action.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 'inline':
|
|
return (
|
|
<div
|
|
className={`inline-flex items-center gap-2 px-3 py-2 bg-red-500/10 border border-red-500/30 rounded-lg ${className}`}
|
|
role="alert"
|
|
aria-live="assertive"
|
|
>
|
|
<AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
|
|
<span className="text-sm text-red-400">{errorInfo.message}</span>
|
|
{onRetry && showRetry && (
|
|
<button
|
|
onClick={onRetry}
|
|
className="ml-2 text-xs text-red-300 hover:text-red-200 underline"
|
|
>
|
|
Retry
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience component for API error display
|
|
*/
|
|
export function ApiErrorDisplay({
|
|
error,
|
|
onRetry,
|
|
variant = 'full-screen',
|
|
hideTechnicalDetails = false,
|
|
}: {
|
|
error: ApiError;
|
|
onRetry?: () => void;
|
|
variant?: 'full-screen' | 'card' | 'inline';
|
|
hideTechnicalDetails?: boolean;
|
|
}) {
|
|
return (
|
|
<ErrorDisplay
|
|
error={error}
|
|
onRetry={onRetry}
|
|
variant={variant}
|
|
hideTechnicalDetails={hideTechnicalDetails}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Convenience component for network error display
|
|
*/
|
|
export function NetworkErrorDisplay({
|
|
onRetry,
|
|
variant = 'full-screen',
|
|
}: {
|
|
onRetry?: () => void;
|
|
variant?: 'full-screen' | 'card' | 'inline';
|
|
}) {
|
|
return (
|
|
<ErrorDisplay
|
|
error={new Error('Network connection failed. Please check your internet connection.')}
|
|
onRetry={onRetry}
|
|
variant={variant}
|
|
/>
|
|
);
|
|
} |