72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
||
|
|
|
||
|
|
type ToastType = 'success' | 'error' | 'info' | 'warning';
|
||
|
|
|
||
|
|
interface Toast {
|
||
|
|
id: number;
|
||
|
|
type: ToastType;
|
||
|
|
message: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ToastContextValue {
|
||
|
|
showToast: (message: string, type?: ToastType) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
const ToastContext = createContext<ToastContextValue | null>(null);
|
||
|
|
|
||
|
|
const toastColors: Record<ToastType, string> = {
|
||
|
|
success: 'bg-success text-white',
|
||
|
|
error: 'bg-error text-white',
|
||
|
|
info: 'bg-primary text-white',
|
||
|
|
warning: 'bg-secondary text-white',
|
||
|
|
};
|
||
|
|
|
||
|
|
export function ToastProvider({ children }: { children: ReactNode }) {
|
||
|
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
||
|
|
|
||
|
|
const showToast = useCallback((message: string, type: ToastType = 'info') => {
|
||
|
|
const id = Date.now() + Math.random();
|
||
|
|
setToasts((prev) => [...prev, { id, type, message }]);
|
||
|
|
setTimeout(() => {
|
||
|
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||
|
|
}, 5000);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const removeToast = useCallback((id: number) => {
|
||
|
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ToastContext.Provider value={{ showToast }}>
|
||
|
|
{children}
|
||
|
|
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2" data-testid="toast-container">
|
||
|
|
{toasts.map((toast) => (
|
||
|
|
<div
|
||
|
|
key={toast.id}
|
||
|
|
className={`${toastColors[toast.type]} px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 min-w-[280px]`}
|
||
|
|
data-testid={`toast-${toast.id}`}
|
||
|
|
role="alert"
|
||
|
|
>
|
||
|
|
<span className="flex-1">{toast.message}</span>
|
||
|
|
<button onClick={() => removeToast(toast.id)} className="text-white/80 hover:text-white" aria-label="Dismiss">
|
||
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</ToastContext.Provider>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useToast(): ToastContextValue {
|
||
|
|
const ctx = useContext(ToastContext);
|
||
|
|
if (!ctx) {
|
||
|
|
throw new Error('useToast must be used within ToastProvider');
|
||
|
|
}
|
||
|
|
return ctx;
|
||
|
|
}
|