mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
203 lines
5.2 KiB
TypeScript
203 lines
5.2 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { clsx } from 'clsx';
|
|
import { X, CheckCircle2, AlertCircle, Info, AlertTriangle } from 'lucide-react';
|
|
import Button from './Button';
|
|
|
|
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|
|
|
export interface ToastProps {
|
|
id: string;
|
|
message: string;
|
|
description?: string;
|
|
type?: ToastType;
|
|
duration?: number;
|
|
onClose: (id: string) => void;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
};
|
|
}
|
|
|
|
const toastIcons = {
|
|
success: CheckCircle2,
|
|
error: AlertCircle,
|
|
warning: AlertTriangle,
|
|
info: Info,
|
|
};
|
|
|
|
const toastStyles = {
|
|
success: 'bg-success/10 border-success/20 text-success-foreground',
|
|
error: 'bg-destructive/10 border-destructive/20 text-destructive-foreground',
|
|
warning: 'bg-warning/10 border-warning/20 text-warning-foreground',
|
|
info: 'bg-primary/10 border-primary/20 text-primary-foreground',
|
|
};
|
|
|
|
/**
|
|
* Individual toast notification
|
|
*/
|
|
export const Toast = ({
|
|
id,
|
|
message,
|
|
description,
|
|
type = 'info',
|
|
duration = 5000,
|
|
onClose,
|
|
action,
|
|
}: ToastProps) => {
|
|
const [isVisible, setIsVisible] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (duration > 0) {
|
|
const timer = setTimeout(() => {
|
|
setIsVisible(false);
|
|
setTimeout(() => onClose(id), 300); // Wait for animation
|
|
}, duration);
|
|
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [duration, id, onClose]);
|
|
|
|
const Icon = toastIcons[type];
|
|
|
|
if (!isVisible) return null;
|
|
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
'relative flex items-start gap-3 p-4 rounded-lg border shadow-lg',
|
|
'min-w-[300px] max-w-[500px]',
|
|
'animate-in slide-in-from-top-5 fade-in-0 duration-300',
|
|
toastStyles[type]
|
|
)}
|
|
role="alert"
|
|
>
|
|
<Icon className="h-5 w-5 flex-shrink-0 mt-0.5" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium">{message}</p>
|
|
{description && <p className="text-sm mt-1 opacity-90">{description}</p>}
|
|
{action && (
|
|
<div className="mt-3">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
action.onClick();
|
|
onClose(id);
|
|
}}
|
|
>
|
|
{action.label}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setIsVisible(false);
|
|
setTimeout(() => onClose(id), 300);
|
|
}}
|
|
className="flex-shrink-0 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring transition-opacity"
|
|
aria-label="Close"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Toast container/provider
|
|
*/
|
|
export interface ToastContainerProps {
|
|
toasts: ToastProps[];
|
|
onClose: (id: string) => void;
|
|
position?:
|
|
| 'top-right'
|
|
| 'top-left'
|
|
| 'bottom-right'
|
|
| 'bottom-left'
|
|
| 'top-center'
|
|
| 'bottom-center';
|
|
}
|
|
|
|
export const ToastContainer = ({
|
|
toasts,
|
|
onClose,
|
|
position = 'top-right',
|
|
}: ToastContainerProps) => {
|
|
const positionClasses = {
|
|
'top-right': 'top-4 right-4',
|
|
'top-left': 'top-4 left-4',
|
|
'bottom-right': 'bottom-4 right-4',
|
|
'bottom-left': 'bottom-4 left-4',
|
|
'top-center': 'top-4 left-1/2 -translate-x-1/2',
|
|
'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2',
|
|
};
|
|
|
|
if (toasts.length === 0) return null;
|
|
|
|
return (
|
|
<div
|
|
className={clsx('fixed z-50 flex flex-col gap-2', positionClasses[position])}
|
|
role="region"
|
|
aria-live="polite"
|
|
aria-label="Notifications"
|
|
>
|
|
{toasts.map((toast) => (
|
|
<Toast key={toast.id} {...toast} onClose={onClose} />
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Hook for managing toasts
|
|
*/
|
|
export const useToast = () => {
|
|
const [toasts, setToasts] = useState<ToastProps[]>([]);
|
|
|
|
const showToast = (
|
|
message: string,
|
|
options?: {
|
|
description?: string;
|
|
type?: ToastType;
|
|
duration?: number;
|
|
action?: { label: string; onClick: () => void };
|
|
}
|
|
) => {
|
|
const id = `toast-${Date.now()}-${Math.random()}`;
|
|
const newToast: ToastProps = {
|
|
id,
|
|
message,
|
|
description: options?.description,
|
|
type: options?.type || 'info',
|
|
duration: options?.duration ?? 5000,
|
|
onClose: (id) => {
|
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
},
|
|
action: options?.action,
|
|
};
|
|
|
|
setToasts((prev) => [...prev, newToast]);
|
|
return id;
|
|
};
|
|
|
|
const removeToast = (id: string) => {
|
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
};
|
|
|
|
return {
|
|
toasts,
|
|
showToast,
|
|
removeToast,
|
|
success: (message: string, options?: Omit<Parameters<typeof showToast>[1], 'type'>) =>
|
|
showToast(message, { ...options, type: 'success' }),
|
|
error: (message: string, options?: Omit<Parameters<typeof showToast>[1], 'type'>) =>
|
|
showToast(message, { ...options, type: 'error' }),
|
|
warning: (message: string, options?: Omit<Parameters<typeof showToast>[1], 'type'>) =>
|
|
showToast(message, { ...options, type: 'warning' }),
|
|
info: (message: string, options?: Omit<Parameters<typeof showToast>[1], 'type'>) =>
|
|
showToast(message, { ...options, type: 'info' }),
|
|
};
|
|
};
|