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 (
{message}
{description && (
{description}
)}
{action && (
)}
);
};
/**
* 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 (
{toasts.map((toast) => (
))}
);
};
/**
* Hook for managing toasts
*/
export const useToast = () => {
const [toasts, setToasts] = useState([]);
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[1], 'type'>) =>
showToast(message, { ...options, type: 'success' }),
error: (message: string, options?: Omit[1], 'type'>) =>
showToast(message, { ...options, type: 'error' }),
warning: (message: string, options?: Omit[1], 'type'>) =>
showToast(message, { ...options, type: 'warning' }),
info: (message: string, options?: Omit[1], 'type'>) =>
showToast(message, { ...options, type: 'info' }),
};
};