Files
gridpilot.gg/apps/website/components/errors/ErrorDisplay.tsx
2025-12-31 21:24:42 +01:00

146 lines
5.0 KiB
TypeScript

'use client';
import { ApiError } from '@/lib/api/base/ApiError';
import { AlertTriangle, Wifi, RefreshCw, ArrowLeft } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
interface ErrorDisplayProps {
error: ApiError;
onRetry?: () => void;
}
/**
* User-friendly error display for production environments
*/
export function ErrorDisplay({ error, onRetry }: ErrorDisplayProps) {
const router = useRouter();
const [isRetrying, setIsRetrying] = useState(false);
const userMessage = error.getUserMessage();
const isConnectivity = error.isConnectivityIssue();
const handleRetry = async () => {
if (onRetry) {
setIsRetrying(true);
try {
onRetry();
} finally {
setIsRetrying(false);
}
}
};
const handleGoBack = () => {
router.back();
};
const handleGoHome = () => {
router.push('/');
};
return (
<div className="min-h-screen bg-deep-graphite flex items-center justify-center p-4">
<div className="max-w-md w-full bg-iron-gray border border-charcoal-outline rounded-2xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="bg-red-500/10 border-b border-red-500/20 p-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-red-500/20 rounded-lg">
{isConnectivity ? (
<Wifi className="w-6 h-6 text-red-400" />
) : (
<AlertTriangle className="w-6 h-6 text-red-400" />
)}
</div>
<div>
<h1 className="text-xl font-bold text-white">
{isConnectivity ? 'Connection Issue' : 'Something Went Wrong'}
</h1>
<p className="text-sm text-gray-400">Error {error.context.statusCode || 'N/A'}</p>
</div>
</div>
</div>
{/* Body */}
<div className="p-6 space-y-4">
<p className="text-gray-300 leading-relaxed">{userMessage}</p>
{/* Details for debugging (collapsed by default) */}
<details className="text-xs text-gray-500 font-mono bg-deep-graphite p-3 rounded border border-charcoal-outline">
<summary className="cursor-pointer hover:text-gray-300">Technical Details</summary>
<div className="mt-2 space-y-1">
<div>Type: {error.type}</div>
<div>Endpoint: {error.context.endpoint || 'N/A'}</div>
{error.context.statusCode && <div>Status: {error.context.statusCode}</div>}
{error.context.retryCount !== undefined && (
<div>Retries: {error.context.retryCount}</div>
)}
</div>
</details>
{/* Action Buttons */}
<div className="flex flex-col gap-2 pt-2">
{error.isRetryable() && (
<button
onClick={handleRetry}
disabled={isRetrying}
className="flex items-center justify-center gap-2 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg font-medium transition-colors disabled:opacity-50"
>
{isRetrying ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
Retrying...
</>
) : (
<>
<RefreshCw className="w-4 h-4" />
Try Again
</>
)}
</button>
)}
<div className="flex gap-2">
<button
onClick={handleGoBack}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded-lg font-medium transition-colors border border-charcoal-outline"
>
<ArrowLeft className="w-4 h-4" />
Go Back
</button>
<button
onClick={handleGoHome}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded-lg font-medium transition-colors border border-charcoal-outline"
>
Home
</button>
</div>
</div>
</div>
{/* Footer */}
<div className="bg-iron-gray/50 border-t border-charcoal-outline p-4 text-xs text-gray-500 text-center">
If this persists, please contact support at{' '}
<a
href="mailto:support@gridpilot.com"
className="text-primary-blue hover:underline"
>
support@gridpilot.com
</a>
</div>
</div>
</div>
);
}
/**
* Full-screen error display with more context
*/
export function FullScreenError({ error, onRetry }: ErrorDisplayProps) {
return (
<div className="fixed inset-0 z-50 bg-deep-graphite flex items-center justify-center p-4">
<ErrorDisplay error={error} onRetry={onRetry} />
</div>
);
}