tahamajs/IE / Projects /front /src /components /common /ErrorMessage.tsx
tahamajs's picture
download
raw
11.5 kB
// src/components/common/ErrorMessage.tsx
import { AlertCircle, RefreshCw, Home, ArrowLeft, Bug, Copy, CheckCircle, XCircle } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
interface ErrorMessageProps {
/** Main error title */
title?: string;
/** Friendly error message */
message?: string;
/** Error object or string for detailed view */
error?: Error | string | null;
/** Show technical error details (useful for debugging) */
showDetails?: boolean;
/** Show refresh button */
showRefresh?: boolean;
/** Show go back button */
showBack?: boolean;
/** Show home button */
showHome?: boolean;
/** Custom retry handler */
onRetry?: () => void;
/** Additional custom action button */
action?: {
label: string;
onClick: () => void;
};
/** Additional CSS classes */
className?: string;
/** Show copy error button */
showCopy?: boolean;
/** Show error code */
errorCode?: string;
/** Variant of error message */
variant?: "default" | "inline" | "toast" | "banner";
/** on close handler for banner variant */
onClose?: () => void;
}
export function ErrorMessage({
title = "Something went wrong",
message = "We encountered an error while loading your content. Please try again.",
error,
showDetails = false,
showRefresh = true,
showBack = true,
showHome = true,
onRetry,
action,
className = "",
showCopy = true,
errorCode,
variant = "default",
onClose,
}: ErrorMessageProps) {
const navigate = useNavigate();
const [copied, setCopied] = useState(false);
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
const errorMessage = error instanceof Error ? error.message : typeof error === "string" ? error : null;
const errorStack = error instanceof Error ? error.stack : null;
const handleRefresh = () => {
if (onRetry) {
onRetry();
} else {
window.dispatchEvent(new CustomEvent("app:soft-reload"));
if (typeof navigator !== "undefined" && !navigator.onLine) {
return;
}
setTimeout(() => {
window.location.reload();
}, 500);
}
};
const handleCopyError = async () => {
const errorText = `Error: ${title}\nMessage: ${message}\n${errorMessage ? `Details: ${errorMessage}` : ""}\n${errorCode ? `Error Code: ${errorCode}` : ""}\nTimestamp: ${new Date().toISOString()}\nURL: ${window.location.href}`;
await navigator.clipboard.writeText(errorText);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
// Inline variant (for small containers)
if (variant === "inline") {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`flex items-center gap-3 rounded-lg bg-red-50 dark:bg-red-900/10 p-3 text-sm ${className}`}
>
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0" />
<div className="flex-1">
<p className="text-red-700 dark:text-red-400">{message}</p>
</div>
{showRefresh && (
<button onClick={handleRefresh} className="text-red-600 hover:text-red-700 transition">
<RefreshCw className="h-4 w-4" />
</button>
)}
</motion.div>
);
}
// Banner variant (dismissible)
if (variant === "banner") {
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`flex items-center justify-between gap-3 bg-red-50 dark:bg-red-900/20 px-4 py-3 shadow-sm ${className}`}
>
<div className="flex items-center gap-3">
<AlertCircle className="h-5 w-5 text-red-500" />
<div>
<p className="text-sm font-medium text-red-800 dark:text-red-200">{title}</p>
<p className="text-xs text-red-600 dark:text-red-300">{message}</p>
</div>
</div>
<div className="flex gap-2">
{showRefresh && (
<button onClick={handleRefresh} className="p-1 text-red-600 hover:bg-red-100 dark:hover:bg-red-900/30 rounded">
<RefreshCw className="h-4 w-4" />
</button>
)}
{onClose && (
<button onClick={onClose} className="p-1 text-red-600 hover:bg-red-100 dark:hover:bg-red-900/30 rounded">
<XCircle className="h-4 w-4" />
</button>
)}
</div>
</motion.div>
</AnimatePresence>
);
}
// Toast variant (compact)
if (variant === "toast") {
return (
<div className={`flex items-center gap-2 bg-red-500 text-white px-3 py-2 rounded-lg shadow-lg ${className}`}>
<AlertCircle className="h-4 w-4" />
<p className="text-sm">{message}</p>
<button onClick={handleRefresh} className="ml-auto hover:bg-white/20 rounded p-0.5">
<RefreshCw className="h-3 w-3" />
</button>
</div>
);
}
// Default variant (full error page)
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className={`flex flex-col items-center justify-center rounded-2xl bg-gradient-to-b from-red-50 to-white dark:from-gray-900 dark:to-gray-950 p-8 text-center ${className}`}
>
{/* Animated Icon */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 200, damping: 15 }}
className="mb-4 rounded-full bg-red-100 dark:bg-red-900/30 p-4"
>
<AlertCircle className="h-10 w-10 text-red-500" />
</motion.div>
{/* Error Code */}
{errorCode && (
<span className="mb-2 text-xs font-mono text-red-400 bg-red-50 dark:bg-red-900/20 px-2 py-0.5 rounded-full">
Error {errorCode}
</span>
)}
{/* Title */}
<h3 className="mb-2 text-xl font-bold text-gray-900 dark:text-white">
{title}
</h3>
{/* Message */}
<p className="mb-4 max-w-md text-sm text-gray-600 dark:text-gray-400">
{message}
</p>
{/* Technical details */}
{showDetails && (errorMessage || errorStack) && (
<details className="mb-4 w-full max-w-md rounded-xl bg-gray-100 dark:bg-gray-800/50 p-3 text-left">
<summary className="cursor-pointer text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
<Bug className="h-3 w-3" />
Technical details
</summary>
{errorMessage && (
<p className="mt-2 text-xs font-mono text-red-600 dark:text-red-400 break-all">
<strong>Error:</strong> {errorMessage}
</p>
)}
{errorStack && (
<pre className="mt-2 text-xs font-mono text-gray-700 dark:text-gray-300 overflow-x-auto whitespace-pre-wrap max-h-40">
{errorStack}
</pre>
)}
{showCopy && (
<button
onClick={handleCopyError}
className="mt-2 inline-flex items-center gap-1 text-xs text-primary hover:underline transition"
>
{copied ? <CheckCircle className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
{copied ? "Copied!" : "Copy error"}
</button>
)}
</details>
)}
{/* Action buttons */}
<div className="flex flex-wrap items-center justify-center gap-3">
{showRefresh && (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={handleRefresh}
className="inline-flex items-center gap-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-medium text-white shadow-lg shadow-primary/20 transition hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-900"
>
<RefreshCw className="h-4 w-4" />
Try Again
</motion.button>
)}
{showBack && (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => navigate(-1)}
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 px-5 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 shadow-sm transition hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400"
>
<ArrowLeft className="h-4 w-4" />
Go Back
</motion.button>
)}
{showHome && (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => navigate("/")}
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 px-5 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 shadow-sm transition hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400"
>
<Home className="h-4 w-4" />
Home
</motion.button>
)}
{action && (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={action.onClick}
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 px-5 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 shadow-sm transition hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400"
>
{action.label}
</motion.button>
)}
</div>
{/* Support Link */}
<div className="mt-6 text-center">
<p className="text-xs text-gray-400">
Need help?{" "}
<a href="/contact" className="text-primary hover:underline">
Contact Support
</a>
</p>
</div>
</motion.div>
);
}
// Export also a small version for inline errors
export function InlineErrorMessage({ message, onRetry }: { message: string; onRetry?: () => void }) {
return (
<div className="flex items-center gap-2 text-sm text-red-600 dark:text-red-400">
<AlertCircle className="h-4 w-4" />
<span>{message}</span>
{onRetry && (
<button onClick={onRetry} className="text-primary hover:underline text-xs">
Retry
</button>
)}
</div>
);
}
// Export a banner error for page headers
export function BannerErrorMessage({ message, onClose }: { message: string; onClose?: () => void }) {
return (
<div className="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500 p-3 mb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-500" />
<p className="text-sm text-red-700 dark:text-red-300">{message}</p>
</div>
{onClose && (
<button onClick={onClose} className="text-red-500 hover:text-red-700">
<XCircle className="h-4 w-4" />
</button>
)}
</div>
</div>
);
}

Xet Storage Details

Size:
11.5 kB
·
Xet hash:
494a562748f9b06c68ef753ea376ca5115b33b91aea665e8fbdb8469e49f117c

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.