146 lines
5.0 KiB
TypeScript
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>
|
|
);
|
|
} |