website refactor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -15,13 +15,18 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { parseApiError, getErrorSeverity, isRetryable, isConnectivityError } from '@/lib/utils/errorUtils';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Button } from '@/ui/Button';
|
||||
|
||||
interface EnhancedFormErrorProps {
|
||||
error: unknown;
|
||||
onRetry?: () => void;
|
||||
onDismiss?: () => void;
|
||||
showDeveloperDetails?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,7 +40,6 @@ export function EnhancedFormError({
|
||||
onRetry,
|
||||
onDismiss,
|
||||
showDeveloperDetails = process.env.NODE_ENV === 'development',
|
||||
className = ''
|
||||
}: EnhancedFormErrorProps) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const parsed = parseApiError(error);
|
||||
@@ -44,10 +48,10 @@ export function EnhancedFormError({
|
||||
const connectivity = isConnectivityError(error);
|
||||
|
||||
const getIcon = () => {
|
||||
if (connectivity) return <Wifi className="w-5 h-5" />;
|
||||
if (severity === 'error') return <AlertTriangle className="w-5 h-5" />;
|
||||
if (severity === 'warning') return <AlertCircle className="w-5 h-5" />;
|
||||
return <Info className="w-5 h-5" />;
|
||||
if (connectivity) return Wifi;
|
||||
if (severity === 'error') return AlertTriangle;
|
||||
if (severity === 'warning') return AlertCircle;
|
||||
return Info;
|
||||
};
|
||||
|
||||
const getColor = () => {
|
||||
@@ -62,179 +66,165 @@ export function EnhancedFormError({
|
||||
const color = getColor();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<Box
|
||||
as={motion.div}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className={`bg-${color}-500/10 border-${color}-500/30 rounded-lg overflow-hidden ${className}`}
|
||||
>
|
||||
{/* Main Error Message */}
|
||||
<div className="p-4 flex items-start gap-3">
|
||||
<div className={`text-${color}-400 flex-shrink-0 mt-0.5`}>
|
||||
{getIcon()}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className={`text-sm font-medium text-${color}-200`}>
|
||||
{parsed.userMessage}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{retryable && onRetry && (
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="p-1.5 hover:bg-white/5 rounded transition-colors"
|
||||
title="Retry"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
)}
|
||||
<Box
|
||||
bg={`bg-${color}-500/10`}
|
||||
border
|
||||
borderColor={`border-${color}-500/30`}
|
||||
rounded="lg"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* Main Error Message */}
|
||||
<Box p={4} display="flex" alignItems="start" gap={3}>
|
||||
<Box color={`text-${color}-400`} flexShrink={0} mt={0.5}>
|
||||
<Icon icon={getIcon()} size={5} />
|
||||
</Box>
|
||||
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Box display="flex" alignItems="center" justifyContent="between" gap={2}>
|
||||
<Text size="sm" weight="medium" color={`text-${color}-200`}>
|
||||
{parsed.userMessage}
|
||||
</Text>
|
||||
|
||||
{onDismiss && (
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="p-1.5 hover:bg-white/5 rounded transition-colors"
|
||||
title="Dismiss"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showDeveloperDetails && (
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="p-1.5 hover:bg-white/5 rounded transition-colors"
|
||||
title="Toggle technical details"
|
||||
>
|
||||
{showDetails ? (
|
||||
<ChevronUp className="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
{retryable && onRetry && (
|
||||
<IconButton
|
||||
icon={RefreshCw}
|
||||
onClick={onRetry}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Retry"
|
||||
/>
|
||||
)}
|
||||
|
||||
{onDismiss && (
|
||||
<IconButton
|
||||
icon={X}
|
||||
onClick={onDismiss}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Dismiss"
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDeveloperDetails && (
|
||||
<IconButton
|
||||
icon={showDetails ? ChevronUp : ChevronDown}
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Toggle technical details"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Validation Errors List */}
|
||||
{parsed.isValidationError && parsed.validationErrors.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{parsed.validationErrors.map((validationError, index) => (
|
||||
<div key={index} className="text-xs text-${color}-300/80">
|
||||
• {validationError.field}: {validationError.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Validation Errors List */}
|
||||
{parsed.isValidationError && parsed.validationErrors.length > 0 && (
|
||||
<Stack gap={1} mt={2}>
|
||||
{parsed.validationErrors.map((validationError, index) => (
|
||||
<Text key={index} size="xs" color={`text-${color}-300/80`} block>
|
||||
• {validationError.field}: {validationError.message}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Action Hint */}
|
||||
<Box mt={2}>
|
||||
<Text size="xs" color="text-gray-400">
|
||||
{connectivity && "Check your internet connection and try again"}
|
||||
{parsed.isValidationError && "Please review your input and try again"}
|
||||
{retryable && !connectivity && !parsed.isValidationError && "Please try again in a moment"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Developer Details */}
|
||||
<AnimatePresence>
|
||||
{showDetails && (
|
||||
<Box
|
||||
as={motion.div}
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
>
|
||||
<Box borderTop borderColor={`border-${color}-500/20`} bg="bg-black/20" p={4}>
|
||||
<Stack gap={3} fontSize="0.75rem">
|
||||
<Box display="flex" alignItems="center" gap={2} color="text-gray-400">
|
||||
<Icon icon={Bug} size={3} />
|
||||
<Text weight="semibold">Developer Details</Text>
|
||||
</Box>
|
||||
|
||||
<Stack gap={1}>
|
||||
<Text color="text-gray-500">Error Type:</Text>
|
||||
<Text color="text-white">{error instanceof ApiError ? error.type : 'Unknown'}</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={1}>
|
||||
<Text color="text-gray-500">Developer Message:</Text>
|
||||
<Text color="text-white" transform="break-all">{parsed.developerMessage}</Text>
|
||||
</Stack>
|
||||
|
||||
{error instanceof ApiError && error.context.endpoint && (
|
||||
<Stack gap={1}>
|
||||
<Text color="text-gray-500">Endpoint:</Text>
|
||||
<Text color="text-white">{error.context.method} {error.context.endpoint}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{error instanceof ApiError && error.context.statusCode && (
|
||||
<Stack gap={1}>
|
||||
<Text color="text-gray-500">Status Code:</Text>
|
||||
<Text color="text-white">{error.context.statusCode}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Box pt={2} borderTop borderColor="border-charcoal-outline/50">
|
||||
<Text color="text-gray-500" block mb={1}>Quick Actions:</Text>
|
||||
<Box display="flex" gap={2}>
|
||||
{retryable && onRetry && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onRetry}
|
||||
size="sm"
|
||||
bg="bg-blue-600/20"
|
||||
color="text-primary-blue"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
if (error instanceof Error) {
|
||||
console.error('Full error details:', error);
|
||||
if (error.stack) {
|
||||
console.log('Stack trace:', error.stack);
|
||||
}
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
bg="bg-purple-600/20"
|
||||
color="text-purple-400"
|
||||
>
|
||||
Log to Console
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Action Hint */}
|
||||
<div className="mt-2 text-xs text-gray-400">
|
||||
{connectivity && "Check your internet connection and try again"}
|
||||
{parsed.isValidationError && "Please review your input and try again"}
|
||||
{retryable && !connectivity && !parsed.isValidationError && "Please try again in a moment"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Developer Details */}
|
||||
<AnimatePresence>
|
||||
{showDeveloperDetails && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-t border-${color}-500/20 bg-black/20"
|
||||
>
|
||||
<div className="p-4 space-y-3 text-xs font-mono">
|
||||
<div className="flex items-center gap-2 text-gray-400">
|
||||
<Bug className="w-3 h-3" />
|
||||
<span className="font-semibold">Developer Details</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Error Type:</div>
|
||||
<div className="text-white">{error instanceof ApiError ? error.type : 'Unknown'}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Developer Message:</div>
|
||||
<div className="text-white break-all">{parsed.developerMessage}</div>
|
||||
</div>
|
||||
|
||||
{error instanceof ApiError && error.context.endpoint && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Endpoint:</div>
|
||||
<div className="text-white">{error.context.method} {error.context.endpoint}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error instanceof ApiError && error.context.statusCode && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Status Code:</div>
|
||||
<div className="text-white">{error.context.statusCode}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error instanceof ApiError && error.context.retryCount !== undefined && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Retry Count:</div>
|
||||
<div className="text-white">{error.context.retryCount}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error instanceof ApiError && error.context.timestamp && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Timestamp:</div>
|
||||
<div className="text-white">{error.context.timestamp}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error instanceof ApiError && error.context.troubleshooting && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Troubleshooting:</div>
|
||||
<div className="text-yellow-400">{error.context.troubleshooting}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{parsed.validationErrors.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-500">Validation Errors:</div>
|
||||
<div className="text-white">{JSON.stringify(parsed.validationErrors, null, 2)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2 border-t border-gray-700/50">
|
||||
<div className="text-gray-500 mb-1">Quick Actions:</div>
|
||||
<div className="flex gap-2">
|
||||
{retryable && onRetry && (
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="px-2 py-1 bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 rounded transition-colors"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (error instanceof Error) {
|
||||
console.error('Full error details:', error);
|
||||
if (error.stack) {
|
||||
console.log('Stack trace:', error.stack);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="px-2 py-1 bg-purple-600/20 hover:bg-purple-600/30 text-purple-400 rounded transition-colors"
|
||||
>
|
||||
Log to Console
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -258,30 +248,33 @@ export function FormErrorSummary({
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<Box
|
||||
as={motion.div}
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="bg-red-500/10 border border-red-500/30 rounded-lg p-3 flex items-start gap-2"
|
||||
>
|
||||
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-red-200">{summary.title}</div>
|
||||
<div className="text-xs text-red-300/80 mt-0.5">{summary.description}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{summary.action}</div>
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="p-1 hover:bg-red-500/10 rounded transition-colors"
|
||||
>
|
||||
<X className="w-3.5 h-3.5 text-red-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
<Box bg="bg-red-500/10" border borderColor="border-red-500/30" rounded="lg" p={3} display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={AlertCircle} size={4} color="rgb(239, 68, 68)" mt={0.5} />
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Box display="flex" alignItems="center" justifyContent="between" gap={2}>
|
||||
<Box>
|
||||
<Text size="sm" weight="medium" color="text-red-200" block>{summary.title}</Text>
|
||||
<Text size="xs" color="text-red-300/80" block mt={0.5}>{summary.description}</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1}>{summary.action}</Text>
|
||||
</Box>
|
||||
{onDismiss && (
|
||||
<IconButton
|
||||
icon={X}
|
||||
onClick={onDismiss}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="rgb(239, 68, 68)"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user