website refactor
This commit is contained in:
248
apps/website/ui/ErrorDisplay.tsx
Normal file
248
apps/website/ui/ErrorDisplay.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
|
||||
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { AlertCircle, ArrowLeft, Home, RefreshCw } from 'lucide-react';
|
||||
import { Box } from './Box';
|
||||
import { Button } from './Button';
|
||||
import { Heading } from './Heading';
|
||||
import { Icon } from './Icon';
|
||||
import { Stack } from './Stack';
|
||||
import { ErrorDisplayAction, ErrorDisplayProps } from './state-types';
|
||||
import { Surface } from './Surface';
|
||||
import { Text } from './Text';
|
||||
|
||||
export function ErrorDisplay({
|
||||
error,
|
||||
onRetry,
|
||||
variant = 'full-screen',
|
||||
actions = [],
|
||||
showRetry = true,
|
||||
showNavigation = true,
|
||||
hideTechnicalDetails = false,
|
||||
className = '',
|
||||
}: 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,
|
||||
];
|
||||
|
||||
switch (variant) {
|
||||
case 'full-screen':
|
||||
return (
|
||||
<Box
|
||||
position="fixed"
|
||||
inset="0"
|
||||
zIndex={50}
|
||||
bg="bg-deep-graphite"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
p={6}
|
||||
className={className}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
>
|
||||
<Box maxWidth="lg" fullWidth textAlign="center">
|
||||
<Box display="flex" justifyContent="center" mb={6}>
|
||||
<Box
|
||||
display="flex"
|
||||
h="20"
|
||||
w="20"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
rounded="3xl"
|
||||
bg="bg-red-500/10"
|
||||
border={true}
|
||||
borderColor="border-red-500/30"
|
||||
>
|
||||
<Icon icon={AlertCircle} size={10} color="text-red-500" />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Heading level={2} mb={3}>
|
||||
{errorInfo.title}
|
||||
</Heading>
|
||||
|
||||
<Text size="lg" color="text-gray-400" block mb={6} style={{ lineHeight: 1.625 }}>
|
||||
{errorInfo.message}
|
||||
</Text>
|
||||
|
||||
{errorInfo.isApiError && errorInfo.statusCode && (
|
||||
<Box mb={6} display="inline-flex" alignItems="center" gap={2} px={4} py={2} bg="bg-iron-gray/40" rounded="lg">
|
||||
<Text size="sm" color="text-gray-300" font="mono">HTTP {errorInfo.statusCode}</Text>
|
||||
{errorInfo.details && !hideTechnicalDetails && (
|
||||
<Text size="sm" color="text-gray-500">- {errorInfo.details}</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{defaultActions.length > 0 && (
|
||||
<Stack direction={{ base: 'col', md: 'row' }} gap={3} justify="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} />}
|
||||
className="px-6 py-3"
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{!hideTechnicalDetails && process.env.NODE_ENV === 'development' && error.stack && (
|
||||
<Box mt={8} textAlign="left">
|
||||
<details className="cursor-pointer">
|
||||
<summary className="text-sm text-gray-500 hover:text-gray-400">
|
||||
Technical Details
|
||||
</summary>
|
||||
<Box as="pre" mt={2} p={4} bg="bg-black/50" rounded="lg" color="text-gray-400" style={{ fontSize: '0.75rem', overflowX: 'auto' }}>
|
||||
{error.stack}
|
||||
</Box>
|
||||
</details>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 'card':
|
||||
return (
|
||||
<Surface
|
||||
variant="muted"
|
||||
border={true}
|
||||
borderColor="border-red-500/30"
|
||||
rounded="xl"
|
||||
p={6}
|
||||
className={className}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
>
|
||||
<Stack direction="row" gap={4} align="start">
|
||||
<Icon icon={AlertCircle} size={6} color="text-red-500" />
|
||||
<Box flexGrow={1}>
|
||||
<Heading level={3} mb={1}>
|
||||
{errorInfo.title}
|
||||
</Heading>
|
||||
<Text size="sm" color="text-gray-400" block mb={3}>
|
||||
{errorInfo.message}
|
||||
</Text>
|
||||
|
||||
{errorInfo.isApiError && errorInfo.statusCode && (
|
||||
<Text size="xs" font="mono" color="text-gray-500" block mb={3}>
|
||||
HTTP {errorInfo.statusCode}
|
||||
{errorInfo.details && !hideTechnicalDetails && ` - ${errorInfo.details}`}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{defaultActions.length > 0 && (
|
||||
<Stack direction="row" gap={2}>
|
||||
{defaultActions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onClick={action.onClick}
|
||||
variant={action.variant === 'primary' ? 'danger' : 'secondary'}
|
||||
size="sm"
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
case 'inline':
|
||||
return (
|
||||
<Box
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
px={3}
|
||||
py={2}
|
||||
bg="bg-red-500/10"
|
||||
border={true}
|
||||
borderColor="border-red-500/30"
|
||||
rounded="lg"
|
||||
className={className}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
>
|
||||
<Icon icon={AlertCircle} size={4} color="text-red-500" />
|
||||
<Text size="sm" color="text-red-400">{errorInfo.message}</Text>
|
||||
{onRetry && showRetry && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onRetry}
|
||||
size="sm"
|
||||
className="ml-2 text-xs text-red-300 hover:text-red-200 underline p-0 h-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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user