generated from fahricansecer/boilerplate-fe
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { CheckCircle, AlertCircle, AlertTriangle, Info, X } from "lucide-react";
|
||||
|
||||
type ToastVariant = "success" | "error" | "warning" | "info";
|
||||
|
||||
interface Toast {
|
||||
id: string;
|
||||
variant: ToastVariant;
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface ToastContextType {
|
||||
toast: (variant: ToastVariant, message: string, duration?: number) => void;
|
||||
success: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
warning: (message: string) => void;
|
||||
info: (message: string) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextType | null>(null);
|
||||
|
||||
const icons: Record<ToastVariant, typeof CheckCircle> = {
|
||||
success: CheckCircle,
|
||||
error: AlertCircle,
|
||||
warning: AlertTriangle,
|
||||
info: Info,
|
||||
};
|
||||
|
||||
const variantStyles: Record<ToastVariant, string> = {
|
||||
success:
|
||||
"bg-emerald-500/12 border-emerald-500/30 text-emerald-300 [--toast-icon:theme(colors.emerald.400)]",
|
||||
error:
|
||||
"bg-red-500/12 border-red-500/30 text-red-300 [--toast-icon:theme(colors.red.400)]",
|
||||
warning:
|
||||
"bg-amber-500/12 border-amber-500/30 text-amber-300 [--toast-icon:theme(colors.amber.400)]",
|
||||
info: "bg-violet-500/12 border-violet-500/30 text-violet-300 [--toast-icon:theme(colors.violet.400)]",
|
||||
};
|
||||
|
||||
export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
}, []);
|
||||
|
||||
const addToast = useCallback(
|
||||
(variant: ToastVariant, message: string, duration = 4000) => {
|
||||
const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
setToasts((prev) => [...prev.slice(-4), { id, variant, message, duration }]);
|
||||
if (duration > 0) {
|
||||
setTimeout(() => removeToast(id), duration);
|
||||
}
|
||||
},
|
||||
[removeToast],
|
||||
);
|
||||
|
||||
const ctx: ToastContextType = {
|
||||
toast: addToast,
|
||||
success: (msg) => addToast("success", msg),
|
||||
error: (msg) => addToast("error", msg),
|
||||
warning: (msg) => addToast("warning", msg),
|
||||
info: (msg) => addToast("info", msg),
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={ctx}>
|
||||
{children}
|
||||
{/* Toast container — fixed bottom-right */}
|
||||
<div className="fixed bottom-20 md:bottom-6 right-4 z-[9999] flex flex-col gap-2.5 max-w-[min(400px,calc(100vw-2rem))]">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{toasts.map((t) => {
|
||||
const Icon = icons[t.variant];
|
||||
return (
|
||||
<motion.div
|
||||
key={t.id}
|
||||
layout
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||
transition={{ type: "spring", bounce: 0.25, duration: 0.4 }}
|
||||
className={`flex items-start gap-3 px-4 py-3 rounded-xl border backdrop-blur-xl shadow-2xl ${variantStyles[t.variant]}`}
|
||||
>
|
||||
<Icon
|
||||
size={18}
|
||||
className="shrink-0 mt-0.5"
|
||||
style={{ color: "var(--toast-icon)" }}
|
||||
/>
|
||||
<p className="text-sm font-medium flex-1 leading-snug">{t.message}</p>
|
||||
<button
|
||||
onClick={() => removeToast(t.id)}
|
||||
className="shrink-0 p-0.5 rounded-md hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<X size={14} className="opacity-60" />
|
||||
</button>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useToast(): ToastContextType {
|
||||
const ctx = useContext(ToastContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useToast must be used within <ToastProvider>");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user