Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | 1x 17x 17x 2x 15x 15x 15x 7x 7x 7x 1x 15x 1x 15x 8x 1x | "use client";
import { AnimatePresence, motion } from "framer-motion";
import { CheckCircle, Info, X, XCircle } from "lucide-react";
import {
createContext,
ReactNode,
useCallback,
useContext,
useState,
} from "react";
export type ToastType = "success" | "error" | "info";
interface ToastMessage {
id: string;
message: string;
type: ToastType;
}
interface ToastContextType {
showToast: (message: string, type: ToastType) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export function useToast() {
const context = useContext(ToastContext);
if (!context) {
throw new Error("useToast must be used within a ToastProvider");
}
return context;
}
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<ToastMessage[]>([]);
const showToast = useCallback((message: string, type: ToastType) => {
const id = Math.random().toString(36).substring(2, 9);
setToasts((prev) => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, 5000);
}, []);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);
return (
<ToastContext.Provider value={{ showToast }}>
{children}
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 pointer-events-none">
<AnimatePresence>
{toasts.map((toast) => (
<motion.div
key={toast.id}
initial={{ opacity: 0, y: 50, scale: 0.3 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.5, transition: { duration: 0.2 } }}
layout="position"
className={`pointer-events-auto flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg text-white min-w-[300px] max-w-sm ${
toast.type === "success"
? "bg-green-600"
: toast.type === "error"
? "bg-red-600"
: "bg-blue-600"
}`}
>
{toast.type === "success" ? (
<CheckCircle className="w-5 h-5 flex-shrink-0" />
) : toast.type === "error" ? (
<XCircle className="w-5 h-5 flex-shrink-0" />
) : (
<Info className="w-5 h-5 flex-shrink-0" />
)}
<p className="flex-1 text-sm font-medium">{toast.message}</p>
<button
onClick={() => removeToast(toast.id)}
className="p-1 hover:bg-white/20 rounded-full transition-colors"
aria-label="Close"
>
<X className="w-4 h-4" />
</button>
</motion.div>
))}
</AnimatePresence>
</div>
</ToastContext.Provider>
);
}
|