241 lines
7.4 KiB
TypeScript
241 lines
7.4 KiB
TypeScript
import { ApiError } from '@/lib/gateways/api/base/ApiError';
|
|
import { Box } from '@/ui/Box';
|
|
import { Button } from '@/ui/Button';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { ErrorDisplayAction, ErrorDisplayProps } from '@/ui/state-types';
|
|
import { Surface } from '@/ui/Surface';
|
|
import { Text } from '@/ui/Text';
|
|
import { AlertCircle, ArrowLeft, Home, RefreshCw } from 'lucide-react';
|
|
|
|
export function ErrorDisplay({
|
|
error,
|
|
onRetry,
|
|
variant = 'full-screen',
|
|
actions = [],
|
|
showRetry = true,
|
|
showNavigation = true,
|
|
hideTechnicalDetails = false,
|
|
}: ErrorDisplayProps) {
|
|
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();
|
|
|
|
const defaultActions: ErrorDisplayAction[] = [
|
|
...(showRetry && onRetry ? [{ label: 'Retry', onClick: onRetry, variant: 'primary' as const, icon: RefreshCw }] : []),
|
|
...(showNavigation ? [
|
|
{ label: 'Go Back', onClick: () => window.history.back(), variant: 'secondary' as const, icon: ArrowLeft },
|
|
{ label: 'Home', onClick: () => window.location.href = '/', variant: 'secondary' as const, icon: Home },
|
|
] : []),
|
|
...actions,
|
|
];
|
|
|
|
const ErrorIcon = () => (
|
|
<Box
|
|
display="flex"
|
|
width={20}
|
|
height={20}
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
rounded="xl"
|
|
bg="rgba(227, 92, 92, 0.1)"
|
|
style={{ border: '1px solid rgba(227, 92, 92, 0.3)' }}
|
|
>
|
|
<Icon icon={AlertCircle} size={10} intent="critical" />
|
|
</Box>
|
|
);
|
|
|
|
switch (variant) {
|
|
case 'full-screen':
|
|
return (
|
|
<Box
|
|
position="fixed"
|
|
inset={0}
|
|
zIndex={100}
|
|
bg="var(--ui-color-bg-base)"
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
padding={6}
|
|
role="alert"
|
|
>
|
|
<Box maxWidth="32rem" fullWidth textAlign="center">
|
|
<Box display="flex" justifyContent="center" marginBottom={6}>
|
|
<ErrorIcon />
|
|
</Box>
|
|
|
|
<Heading level={2} marginBottom={3}>
|
|
{errorInfo.title}
|
|
</Heading>
|
|
|
|
<Text size="lg" variant="low" block marginBottom={6} leading="relaxed">
|
|
{errorInfo.message}
|
|
</Text>
|
|
|
|
{errorInfo.isApiError && errorInfo.statusCode && (
|
|
<Box marginBottom={6} display="inline-flex" alignItems="center" gap={2} paddingX={4} paddingY={2} bg="var(--ui-color-bg-surface-muted)" rounded="lg">
|
|
<Text size="sm" variant="med" font="mono">HTTP {errorInfo.statusCode}</Text>
|
|
{errorInfo.details && !hideTechnicalDetails && (
|
|
<Text size="sm" variant="low">- {errorInfo.details}</Text>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
{defaultActions.length > 0 && (
|
|
<Box display="flex" flexDirection={{ base: 'col', md: 'row' }} gap={3} justifyContent="center">
|
|
{defaultActions.map((action, index) => (
|
|
<Button
|
|
key={index}
|
|
onClick={action.onClick}
|
|
variant={action.variant === 'primary' ? 'danger' : 'secondary'}
|
|
icon={action.icon && <Icon icon={action.icon} size={4} />}
|
|
>
|
|
{action.label}
|
|
</Button>
|
|
))}
|
|
</Box>
|
|
)}
|
|
|
|
{!hideTechnicalDetails && process.env.NODE_ENV === 'development' && error.stack && (
|
|
<Box marginTop={8} textAlign="left">
|
|
<details style={{ cursor: 'pointer' }}>
|
|
<summary>
|
|
<Text as="span" size="sm" variant="low">Technical Details</Text>
|
|
</summary>
|
|
<Box marginTop={2} padding={4} bg="var(--ui-color-bg-surface-muted)" rounded="lg" style={{ overflowX: 'auto' }}>
|
|
<Text as="pre" size="xs" variant="low" font="mono">
|
|
{error.stack}
|
|
</Text>
|
|
</Box>
|
|
</details>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
|
|
case 'card':
|
|
return (
|
|
<Surface
|
|
variant="muted"
|
|
rounded="xl"
|
|
padding={6}
|
|
style={{ border: '1px solid rgba(227, 92, 92, 0.3)' }}
|
|
role="alert"
|
|
>
|
|
<Box display="flex" gap={4} alignItems="start">
|
|
<Icon icon={AlertCircle} size={6} intent="critical" />
|
|
<Box flex={1}>
|
|
<Heading level={3} marginBottom={1}>
|
|
{errorInfo.title}
|
|
</Heading>
|
|
<Text size="sm" variant="low" block marginBottom={3}>
|
|
{errorInfo.message}
|
|
</Text>
|
|
|
|
{errorInfo.isApiError && errorInfo.statusCode && (
|
|
<Text size="xs" variant="low" font="mono" block marginBottom={3}>
|
|
HTTP {errorInfo.statusCode}
|
|
{errorInfo.details && !hideTechnicalDetails && ` - ${errorInfo.details}`}
|
|
</Text>
|
|
)}
|
|
|
|
{defaultActions.length > 0 && (
|
|
<Box display="flex" gap={2}>
|
|
{defaultActions.map((action, index) => (
|
|
<Button
|
|
key={index}
|
|
onClick={action.onClick}
|
|
variant={action.variant === 'primary' ? 'danger' : 'secondary'}
|
|
size="sm"
|
|
>
|
|
{action.label}
|
|
</Button>
|
|
))}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Surface>
|
|
);
|
|
|
|
case 'inline':
|
|
return (
|
|
<Box
|
|
display="inline-flex"
|
|
alignItems="center"
|
|
gap={2}
|
|
paddingX={3}
|
|
paddingY={2}
|
|
bg="rgba(227, 92, 92, 0.1)"
|
|
rounded="lg"
|
|
style={{ border: '1px solid rgba(227, 92, 92, 0.3)' }}
|
|
role="alert"
|
|
>
|
|
<Icon icon={AlertCircle} size={4} intent="critical" />
|
|
<Text size="sm" variant="critical">{errorInfo.message}</Text>
|
|
{onRetry && showRetry && (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={onRetry}
|
|
size="sm"
|
|
style={{ marginLeft: '0.5rem', padding: 0, height: 'auto' }}
|
|
>
|
|
Retry
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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}
|
|
/>
|
|
);
|
|
}
|
|
|
|
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}
|
|
/>
|
|
);
|
|
}
|