224 lines
5.7 KiB
TypeScript
224 lines
5.7 KiB
TypeScript
import React from "react";
|
|
import { cn } from "~/lib/utils";
|
|
import { AlertCircle, X, RefreshCw } from "lucide-react";
|
|
import { Button } from "./button";
|
|
|
|
interface ErrorAlertProps {
|
|
title?: string;
|
|
message: string;
|
|
variant?: "error" | "warning" | "info";
|
|
dismissible?: boolean;
|
|
onDismiss?: () => void;
|
|
onRetry?: () => void;
|
|
retryLabel?: string;
|
|
className?: string;
|
|
icon?: React.ReactNode;
|
|
actions?: React.ReactNode;
|
|
}
|
|
|
|
export function ErrorAlert({
|
|
title,
|
|
message,
|
|
variant = "error",
|
|
dismissible = false,
|
|
onDismiss,
|
|
onRetry,
|
|
retryLabel = "تلاش مجدد",
|
|
className,
|
|
icon,
|
|
actions,
|
|
}: ErrorAlertProps) {
|
|
const variants = {
|
|
error: {
|
|
container:
|
|
"bg-red-50 border-red-200 dark:bg-red-950/20 dark:border-red-800/30",
|
|
icon: "text-red-500 dark:text-red-400",
|
|
title: "text-red-800 dark:text-red-200",
|
|
message: "text-red-700 dark:text-red-300",
|
|
button:
|
|
"text-red-700 hover:text-red-800 dark:text-red-300 dark:hover:text-red-200",
|
|
},
|
|
warning: {
|
|
container:
|
|
"bg-yellow-50 border-yellow-200 dark:bg-yellow-950/20 dark:border-yellow-800/30",
|
|
icon: "text-yellow-500 dark:text-yellow-400",
|
|
title: "text-yellow-800 dark:text-yellow-200",
|
|
message: "text-yellow-700 dark:text-yellow-300",
|
|
button:
|
|
"text-yellow-700 hover:text-yellow-800 dark:text-yellow-300 dark:hover:text-yellow-200",
|
|
},
|
|
info: {
|
|
container:
|
|
"bg-blue-50 border-blue-200 dark:bg-blue-950/20 dark:border-blue-800/30",
|
|
icon: "text-blue-500 dark:text-blue-400",
|
|
title: "text-blue-800 dark:text-blue-200",
|
|
message: "text-blue-700 dark:text-blue-300",
|
|
button:
|
|
"text-blue-700 hover:text-blue-800 dark:text-blue-300 dark:hover:text-blue-200",
|
|
},
|
|
};
|
|
|
|
const variantStyles = variants[variant];
|
|
const defaultIcon = <AlertCircle className="h-5 w-5" />;
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"relative rounded-lg border p-4 transition-all duration-200 animate-slide-down",
|
|
variantStyles.container,
|
|
className,
|
|
)}
|
|
role="alert"
|
|
aria-live="polite"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
{/* Icon */}
|
|
<div className={cn("flex-shrink-0 mt-0.5", variantStyles.icon)}>
|
|
{icon || defaultIcon}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 min-w-0">
|
|
{title && (
|
|
<h3
|
|
className={cn(
|
|
"text-sm font-medium font-persian mb-1",
|
|
variantStyles.title,
|
|
)}
|
|
>
|
|
{title}
|
|
</h3>
|
|
)}
|
|
<div
|
|
className={cn(
|
|
"text-sm font-persian leading-relaxed",
|
|
variantStyles.message,
|
|
)}
|
|
>
|
|
{message}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
{(onRetry || actions) && (
|
|
<div className="mt-3 flex items-center gap-2">
|
|
{onRetry && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onRetry}
|
|
className={cn(
|
|
"h-8 px-3 font-persian text-xs",
|
|
variantStyles.button,
|
|
)}
|
|
>
|
|
<RefreshCw className="h-3 w-3 ml-1" />
|
|
{retryLabel}
|
|
</Button>
|
|
)}
|
|
{actions}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Dismiss Button */}
|
|
{dismissible && onDismiss && (
|
|
<button
|
|
onClick={onDismiss}
|
|
className={cn(
|
|
"flex-shrink-0 rounded-md p-1.5 transition-colors duration-200 hover:bg-black/5 dark:hover:bg-white/5",
|
|
variantStyles.button,
|
|
)}
|
|
aria-label="بستن پیام"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface InlineErrorProps {
|
|
message: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function InlineError({ message, className }: InlineErrorProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex items-center gap-2 text-sm text-destructive font-persian",
|
|
className,
|
|
)}
|
|
role="alert"
|
|
aria-live="polite"
|
|
>
|
|
<AlertCircle className="h-4 w-4 flex-shrink-0" />
|
|
<span>{message}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface FormErrorProps {
|
|
title?: string;
|
|
errors: string[];
|
|
className?: string;
|
|
onRetry?: () => void;
|
|
retryLabel?: string;
|
|
}
|
|
|
|
export function FormError({
|
|
title = "خطا در ارسال فرم",
|
|
errors,
|
|
className,
|
|
onRetry,
|
|
retryLabel = "تلاش مجدد",
|
|
}: FormErrorProps) {
|
|
if (errors.length === 0) return null;
|
|
|
|
const message =
|
|
errors.length === 1
|
|
? errors[0]
|
|
: errors.map((error, index) => `• ${error}`).join("\n");
|
|
|
|
return (
|
|
<ErrorAlert
|
|
title={title}
|
|
message={message}
|
|
variant="error"
|
|
onRetry={onRetry}
|
|
retryLabel={retryLabel}
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|
|
|
|
interface ConnectionErrorProps {
|
|
onRetry?: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function ConnectionError({ onRetry, className }: ConnectionErrorProps) {
|
|
return (
|
|
<ErrorAlert
|
|
title="خطا در اتصال"
|
|
message="خطا در برقراری ارتباط با سرور. لطفاً اتصال اینترنت خود را بررسی کنید."
|
|
variant="error"
|
|
onRetry={onRetry}
|
|
retryLabel="تلاش مجدد"
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|
|
|
|
interface ValidationErrorProps {
|
|
message: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function ValidationError({ message, className }: ValidationErrorProps) {
|
|
return (
|
|
<ErrorAlert message={message} variant="warning" className={className} />
|
|
);
|
|
}
|