Files
gridpilot.gg/apps/website/components/errors/EnhancedFormError.tsx
2026-01-01 20:31:05 +01:00

287 lines
10 KiB
TypeScript

'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
AlertCircle,
AlertTriangle,
Wifi,
RefreshCw,
ChevronDown,
ChevronUp,
Bug,
Info,
X
} from 'lucide-react';
import { parseApiError, getErrorSeverity, isRetryable, isConnectivityError } from '@/lib/utils/errorUtils';
import { ApiError } from '@/lib/api/base/ApiError';
interface EnhancedFormErrorProps {
error: unknown;
onRetry?: () => void;
onDismiss?: () => void;
showDeveloperDetails?: boolean;
className?: string;
}
/**
* Enhanced Form Error Component
*
* Shows user-friendly error messages with optional developer details.
* Handles validation errors, network errors, and general errors.
*/
export function EnhancedFormError({
error,
onRetry,
onDismiss,
showDeveloperDetails = process.env.NODE_ENV === 'development',
className = ''
}: EnhancedFormErrorProps) {
const [showDetails, setShowDetails] = useState(false);
const parsed = parseApiError(error);
const severity = getErrorSeverity(error);
const retryable = isRetryable(error);
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" />;
};
const getColor = () => {
switch (severity) {
case 'error': return 'red';
case 'warning': return 'amber';
case 'info': return 'blue';
default: return 'gray';
}
};
const color = getColor();
return (
<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>
)}
{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>
{/* 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>
)}
{/* 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>
);
}
/**
* Form Error Summary Component
*
* Shows a compact error summary for form submission failures
*/
export function FormErrorSummary({
error,
onDismiss
}: {
error: unknown;
onDismiss?: () => void;
}) {
const parsed = parseApiError(error);
const summary = {
title: 'Submission Failed',
description: parsed.userMessage,
action: 'Please try again'
};
return (
<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>
);
}