2026-06-01 16:42:36 +07:00
|
|
|
// ─── Toast notification system ──────────────────────────────────────────────
|
2026-06-01 21:44:29 +07:00
|
|
|
import {
|
|
|
|
|
createContext,
|
|
|
|
|
type ReactNode,
|
|
|
|
|
useCallback,
|
|
|
|
|
useContext,
|
|
|
|
|
useState,
|
|
|
|
|
} from "react";
|
2026-06-01 16:42:36 +07:00
|
|
|
|
|
|
|
|
interface Toast {
|
|
|
|
|
id: string;
|
|
|
|
|
message: string;
|
|
|
|
|
type: "info" | "success" | "error" | "warning";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ToastContextType {
|
|
|
|
|
toasts: Toast[];
|
|
|
|
|
addToast: (message: string, type?: Toast["type"]) => void;
|
|
|
|
|
removeToast: (id: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ToastContext = createContext<ToastContextType>({
|
|
|
|
|
toasts: [],
|
|
|
|
|
addToast: () => {},
|
|
|
|
|
removeToast: () => {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export function ToastProvider({ children }: { children: ReactNode }) {
|
|
|
|
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
|
|
|
|
2026-06-01 21:44:29 +07:00
|
|
|
const addToast = useCallback(
|
|
|
|
|
(message: string, type: Toast["type"] = "info") => {
|
|
|
|
|
const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
|
|
|
setToasts((prev) => [...prev, { id, message, type }]);
|
|
|
|
|
setTimeout(
|
|
|
|
|
() => setToasts((prev) => prev.filter((t) => t.id !== id)),
|
|
|
|
|
4000,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
2026-06-01 16:42:36 +07:00
|
|
|
|
|
|
|
|
const removeToast = useCallback((id: string) => {
|
|
|
|
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ToastContext.Provider value={{ toasts, addToast, removeToast }}>
|
|
|
|
|
{children}
|
|
|
|
|
<ToastContainer />
|
|
|
|
|
</ToastContext.Provider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useToast() {
|
|
|
|
|
return useContext(ToastContext);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ToastContainer() {
|
|
|
|
|
const { toasts, removeToast } = useContext(ToastContext);
|
|
|
|
|
|
|
|
|
|
if (toasts.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
|
|
|
|
{toasts.map((toast) => (
|
|
|
|
|
<div
|
|
|
|
|
key={toast.id}
|
|
|
|
|
className={`rounded-lg border px-4 py-3 text-sm shadow-lg backdrop-blur-xl cursor-pointer transition-all hover:scale-[1.02] ${
|
|
|
|
|
toast.type === "error"
|
|
|
|
|
? "border-destructive/30 bg-destructive/20 text-destructive"
|
|
|
|
|
: toast.type === "success"
|
2026-06-01 21:44:29 +07:00
|
|
|
? "border-green-500/30 bg-green-500/10 text-green-300"
|
|
|
|
|
: toast.type === "warning"
|
|
|
|
|
? "border-yellow-500/30 bg-yellow-500/10 text-yellow-300"
|
|
|
|
|
: "border-border/30 bg-card/80 text-foreground"
|
2026-06-01 16:42:36 +07:00
|
|
|
}`}
|
|
|
|
|
onClick={() => removeToast(toast.id)}
|
|
|
|
|
>
|
|
|
|
|
{toast.message}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|