|
|
'use client'; |
|
|
|
|
|
import React, { createContext, useContext, useState, useCallback } from 'react'; |
|
|
import { AlertCircle, CheckCircle, Info, X, AlertTriangle } from 'lucide-react'; |
|
|
|
|
|
type ToastType = 'success' | 'error' | 'info' | 'warning'; |
|
|
|
|
|
interface Toast { |
|
|
id: string; |
|
|
message: string; |
|
|
type: ToastType; |
|
|
title?: string; |
|
|
} |
|
|
|
|
|
interface ToastContextType { |
|
|
toasts: Toast[]; |
|
|
addToast: (message: string, type: ToastType, title?: string) => void; |
|
|
removeToast: (id: string) => void; |
|
|
} |
|
|
|
|
|
const ToastContext = createContext<ToastContextType | undefined>(undefined); |
|
|
|
|
|
export const useToast = () => { |
|
|
const context = useContext(ToastContext); |
|
|
if (!context) { |
|
|
throw new Error('useToast must be used within a ToastProvider'); |
|
|
} |
|
|
return context; |
|
|
}; |
|
|
|
|
|
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { |
|
|
const [toasts, setToasts] = useState<Toast[]>([]); |
|
|
|
|
|
const addToast = useCallback((message: string, type: ToastType, title?: string) => { |
|
|
const id = Math.random().toString(36).substring(2, 9); |
|
|
setToasts((prev) => [...prev, { id, message, type, title }]); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
removeToast(id); |
|
|
}, 5000); |
|
|
}, []); |
|
|
|
|
|
const removeToast = useCallback((id: string) => { |
|
|
setToasts((prev) => prev.filter((toast) => toast.id !== id)); |
|
|
}, []); |
|
|
|
|
|
return ( |
|
|
<ToastContext.Provider value={{ toasts, addToast, removeToast }}> |
|
|
{children} |
|
|
<ToastContainer toasts={toasts} removeToast={removeToast} /> |
|
|
</ToastContext.Provider> |
|
|
); |
|
|
}; |
|
|
|
|
|
const getToastIcon = (type: ToastType) => { |
|
|
switch (type) { |
|
|
case 'success': |
|
|
return <CheckCircle className="h-5 w-5 text-white" />; |
|
|
case 'error': |
|
|
return <AlertCircle className="h-5 w-5 text-white" />; |
|
|
case 'warning': |
|
|
return <AlertTriangle className="h-5 w-5 text-white" />; |
|
|
case 'info': |
|
|
return <Info className="h-5 w-5 text-white" />; |
|
|
} |
|
|
}; |
|
|
|
|
|
const getToastColor = (type: ToastType) => { |
|
|
switch (type) { |
|
|
case 'success': |
|
|
return 'bg-success'; |
|
|
case 'error': |
|
|
return 'bg-error'; |
|
|
case 'warning': |
|
|
return 'bg-warning'; |
|
|
case 'info': |
|
|
return 'bg-info'; |
|
|
} |
|
|
}; |
|
|
|
|
|
const ToastContainer: React.FC<{ toasts: Toast[]; removeToast: (id: string) => void }> = ({ |
|
|
toasts, |
|
|
removeToast, |
|
|
}) => { |
|
|
if (toasts.length === 0) return null; |
|
|
|
|
|
return ( |
|
|
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2 max-w-md"> |
|
|
{toasts.map((toast) => ( |
|
|
<div |
|
|
key={toast.id} |
|
|
className={`${getToastColor(toast.type)} text-white p-4 rounded-lg shadow-lg flex items-start animate-slideIn`} |
|
|
> |
|
|
<div className="mr-3 flex-shrink-0 mt-0.5"> |
|
|
{getToastIcon(toast.type)} |
|
|
</div> |
|
|
<div className="flex-1 mr-2"> |
|
|
{toast.title && <p className="font-medium">{toast.title}</p>} |
|
|
<p className="text-sm opacity-90">{toast.message}</p> |
|
|
</div> |
|
|
<button |
|
|
onClick={() => removeToast(toast.id)} |
|
|
className="text-white/80 hover:text-white transition-colors" |
|
|
> |
|
|
<X className="h-5 w-5" /> |
|
|
</button> |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|