| // 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.