react-toastly
Version:
A beautiful React toast notification library with animations, gradients, sounds, and premium features for free.
142 lines (125 loc) • 4.22 kB
JSX
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from "react";
const ToastlyContext = createContext();
const soundMap = {
success: "/sounds/success.mp3",
error: "/sounds/error.mp3",
warning: "/sounds/warning.mp3",
info: "/sounds/info.mp3",
};
export function ToastlyProvider({ children }) {
const [toasts, setToasts] = useState([]);
const timers = useRef(new Map());
const playSound = (type) => {
const soundPath = soundMap[type];
if (soundPath) {
const audio = new Audio(soundPath);
audio.play().catch((e) => console.warn("Sound play failed:", e));
}
};
const removeToast = useCallback((id) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
timers.current.delete(id);
}, []);
const addToast = useCallback(({ type, message, content, duration = 3000 }) => {
const id = Date.now() + Math.random();
setToasts((prev) => [...prev, { id, type, message, content }]);
playSound(type);
const timerId = setTimeout(() => {
removeToast(id);
}, duration);
timers.current.set(id, timerId);
}, [removeToast]);
// Clear timers on unmount to avoid leaks
useEffect(() => {
return () => {
timers.current.forEach((timerId) => clearTimeout(timerId));
timers.current.clear();
};
}, []);
const success = (msg, opts = {}) => addToast({ type: "success", message: msg, ...opts });
const error = (msg, opts = {}) => addToast({ type: "error", message: msg, ...opts });
const warning = (msg, opts = {}) => addToast({ type: "warning", message: msg, ...opts });
const info = (msg, opts = {}) => addToast({ type: "info", message: msg, ...opts });
const custom = (content, opts = {}) => addToast({ type: "custom", content, ...opts });
// Promise-based toast helper
const promise = (promise, { loading, success: successMsg, error: errorMsg }) => {
const id = Date.now() + Math.random();
setToasts((prev) => [...prev, { id, type: "info", message: loading }]);
playSound("info");
return promise
.then((res) => {
removeToast(id);
addToast({ type: "success", message: successMsg });
return res;
})
.catch((err) => {
removeToast(id);
addToast({ type: "error", message: errorMsg });
throw err;
});
};
return (
<ToastlyContext.Provider value={{ success, error, warning, info, custom, promise }}>
{children}
<div
style={{
position: "fixed",
top: 10,
right: 10,
display: "flex",
flexDirection: "column",
gap: 10,
zIndex: 9999,
maxWidth: 320,
}}
>
{toasts.map(({ id, type, message, content }) => (
<div
key={id}
role="alert"
aria-live="assertive"
aria-atomic="true"
style={{
padding: "12px 20px",
borderRadius: 8,
color: "white",
fontWeight: "bold",
backgroundColor:
type === "success"
? "#4caf50"
: type === "error"
? "#f44336"
: type === "warning"
? "#ff9800"
: type === "info"
? "#2196f3"
: "#555",
boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
opacity: 0.9,
animation: "slideIn 0.3s ease forwards",
}}
>
{content || message}
</div>
))}
</div>
<style>{`
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 0.9;
transform: translateX(0);
}
}
`}</style>
</ToastlyContext.Provider>
);
}
export function useToastly() {
const context = useContext(ToastlyContext);
if (!context) throw new Error("useToastly must be used within ToastlyProvider");
return context;
}