UNPKG

reactjs-toaster

Version:

The React JS Toaster (reactjs-toaster) is lightweight JavaScript/TypeScript based Toast message library for showing Error , Success , Warning and Info message in UI End.

379 lines (361 loc) 20.3 kB
import * as React from 'react'; import { useRef, useEffect, useState } from 'react'; import * as ReactDOM from 'react-dom'; const useProgressBarHook = ({ duration, type, startWidth = "100%", endWidth = "0%" }) => { const progressRef = useRef(null); useEffect(() => { if (progressRef.current) { progressRef.current.style.transition = `width ${duration}ms linear`; progressRef.current.style.width = startWidth; progressRef.current.getBoundingClientRect(); window.requestAnimationFrame(() => { if (progressRef.current) { progressRef.current.style.width = endWidth; } }); } }, [duration, type, startWidth, endWidth]); return progressRef; }; // Create the custom hook const useStyleHook = () => { // Initialize state with the provided classname const [className, setClassName] = useState(); const [styles, setStyles] = useState(); // Define background styles based on the className props const backgroundColors = { success: className?.successBgClass || `bg-emerald-100 dark:bg-emerald-800`, error: className?.errorBgClass || `bg-red-100 dark:bg-red-800`, warning: className?.warningBgClass || `bg-yellow-100 dark:bg-yellow-800`, info: className?.infoBgClass || `bg-blue-100 dark:bg-blue-800`, loading: className?.loadingBgClass || `bg-purple-100 dark:bg-purple-800`, }; // Define text colors based on the className props const textColors = { success: className?.successTextClass || 'text-emerald-500', error: className?.errorTextClass || 'text-red-500', warning: className?.warningTextClass || 'text-yellow-500', info: className?.infoTextClass || 'text-blue-500', loading: className?.loadingTextClass || 'text-purple-500', }; // Define button styles based on the className props const progressColors = { success: className?.successProgClass || 'bg-emerald-900 hover:bg-emerald-600', error: className?.errorProgClass || 'bg-red-900 hover:bg-red-600', warning: className?.warningProgClass || 'bg-yellow-900 hover:bg-yellow-600', info: className?.infoProgClass || 'bg-blue-900 hover:bg-blue-600', loading: className?.loadingProgClass || 'bg-purple-900 hover:bg-purple-600', }; let style = { body: styles?.body, contentBody: styles?.contentBody, iconBody: styles?.iconBody, icon: styles?.icon, messageBody: styles?.messageBody, message: styles?.message, closeBtnBody: styles?.closeBtnBody, closeBtnIcon: styles?.closeBtnIcon, closeBtn: styles?.closeBtn, progressBody: styles?.progressBody, progress: styles?.progress }; // Return the styles return { backgroundColors, textColors, progressColors, style, setStyles, setClassName }; }; const AlertIcon = ({ size = 24, color = 'currentColor', bgColor = 'transparent', strokeWidth = 2, className = '', }) => { return (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className }, React.createElement("circle", { cx: "12", cy: "12", r: "11", fill: bgColor, stroke: color, strokeWidth: strokeWidth }), React.createElement("path", { d: "M12 8v5M12 16h.01", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" }))); }; const SuccessIcon = ({ size = 24, color = 'currentColor', bgColor = 'transparent', strokeWidth = 2, className = '', }) => { return (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className }, React.createElement("circle", { cx: "12", cy: "12", r: "11", fill: bgColor, stroke: color, strokeWidth: strokeWidth }), React.createElement("path", { d: "M7 13l3 3 7-7", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" }))); }; const WarningIcon = ({ size = 24, color = 'currentColor', bgColor = 'transparent', strokeWidth = 2, className = '', }) => { return (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className }, React.createElement("path", { d: "M12 3L2 21h20L12 3z", fill: bgColor, stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" }), React.createElement("path", { d: "M12 9v4M12 17h.01", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" }))); }; const InfoIcon = ({ size = 24, color = 'currentColor', bgColor = 'transparent', strokeWidth = 2, className = '', }) => { return (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className }, React.createElement("circle", { cx: "12", cy: "12", r: "11", fill: bgColor, stroke: color, strokeWidth: strokeWidth }), React.createElement("path", { d: "M12 8v8M12 7h.01", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" }))); }; const CircularCrossIcon = ({ size = 24, color = 'currentColor', bgColor = 'transparent', strokeWidth = 2, className = '', }) => { return (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className }, React.createElement("circle", { cx: "12", cy: "12", r: "10", fill: bgColor, stroke: color, strokeWidth: strokeWidth }), React.createElement("path", { d: "M15 9l-6 6M9 9l6 6", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" }))); }; const CrossIcon = ({ size = 24, color = 'currentColor', strokeWidth = 2, className = '', }) => { return (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: className }, React.createElement("path", { d: "M18 6L6 18M6 6l12 12", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" }))); }; const LoaderIcon = ({ size = 24, color = 'currentColor', bgColor = 'transparent', strokeWidth = 2, className = '', }) => { return (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: `animate-spin ${className}` }, React.createElement("circle", { cx: "12", cy: "12", r: "10", stroke: bgColor, strokeWidth: strokeWidth }), React.createElement("path", { d: "M12 2a10 10 0 0 1 10 10", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round" }))); }; function getPositionClasses(position) { switch (position) { case 'top-left': return 'top-4 left-4'; case 'top-center': return 'top-4 left-1/2 transform -translate-x-1/2'; case 'top-right': return 'top-4 right-4'; case 'middle-left': return 'top-1/2 left-4 transform -translate-y-1/2'; case 'middle-center': return 'top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2'; case 'middle-right': return 'top-1/2 right-4 transform -translate-y-1/2'; case 'bottom-left': return 'bottom-4 left-4'; case 'bottom-center': return 'bottom-4 left-1/2 transform -translate-x-1/2'; case 'bottom-right': return 'bottom-4 right-4'; } } const icons$2 = { success: SuccessIcon, error: AlertIcon, warning: WarningIcon, info: InfoIcon, loading: LoaderIcon }; function Toast({ type, message, duration = 5000, onClose, styles, className, }) { const { backgroundColors, textColors, progressColors, style, setStyles, setClassName } = useStyleHook(); const Icon = icons$2[type]; React.useEffect(() => { setClassName(className); setStyles(styles); }, [className, styles]); const progressRef = useProgressBarHook({ duration, type }); return (React.createElement("div", { className: `relative w-full max-w-sm overflow-hidden rounded-lg shadow-lg duration-300 bg-white border border-white`, style: style.body }, React.createElement("div", { className: "flex items-start gap-3 p-4", style: style.contentBody }, React.createElement("div", { className: `flex h-8 w-8 items-center justify-center rounded-full ${backgroundColors[type]}`, style: style.iconBody }, React.createElement(Icon, { className: ` h-4 w-4 ${progressColors[type], type === 'loading' && "animate-spin"} `, style: style.iconBody })), React.createElement("div", { className: "flex-1 min-w-0", style: style.messageBody }, React.createElement("p", { className: `mt-1 text-sm max-w-48 text-wrap ${textColors[type]}`, style: style.message }, message)), React.createElement("button", { onClick: onClose, className: `text-gray-400 hover:text-gray-600`, style: style.closeBtnBody }, React.createElement(CrossIcon, { className: "h-5 w-5", style: style.closeBtnIcon }), React.createElement("span", { className: "sr-only", style: style.closeBtn }, "Close"))), React.createElement("div", { className: `h-1 w-full`, style: style.progressBody }, React.createElement("div", { ref: progressRef, className: `h-full w-full ${progressColors[type]}`, style: style.progress })))); } const icons$1 = { success: SuccessIcon, error: AlertIcon, warning: WarningIcon, info: InfoIcon, loading: LoaderIcon }; function ToastModal({ type, message, onClose, duration = 5000, styles, className, }) { const toastRef = React.useRef(null); const { backgroundColors, textColors, progressColors, style, setStyles, setClassName } = useStyleHook(); const Icon = icons$1[type]; let startWidth = "0%"; let endWidth = "100%"; React.useEffect(() => { setClassName(className); setStyles(styles); }, [className]); const progressRef = useProgressBarHook({ duration, type, startWidth, endWidth }); const buttonText = { success: 'OKAY', error: 'TRY AGAIN', warning: 'ACKNOWLEDGE', info: 'GOT IT', loading: 'PLEASE WAIT ...' }; return (React.createElement("div", { className: `fixed inset-0 flex items-center justify-center z-50 bg-black/50 animate-in fade-in duration-200 ${backgroundColors[type]}`, style: style.body }, React.createElement("div", { ref: toastRef, className: `min-w-96 rounded-lg bg-white p-4 shadow-lg flex flex-col items-center text-center gap-2 animate-in slide-in-from-bottom-4 duration-300`, style: style.contentBody }, React.createElement("div", { className: `flex h-12 w-12 items-center justify-center rounded-full `, style: style.iconBody }, React.createElement(Icon, { className: `h-6 w-6 ${textColors[type]} ${type === 'loading' && "animate-spin"} `, style: style.icon })), React.createElement("div", { style: style.messageBody }, React.createElement("p", { className: `text-sm text-gray-600 ${textColors[type]}`, style: style.message }, message)), React.createElement("div", { className: 'w-full', style: style.closeBtnBody }, React.createElement("button", { onClick: onClose, className: `mt-2 w-full px-4 py-2 rounded-md text-white transition-colors ${progressColors[type]}`, disabled: type === 'loading', style: style.closeBtn }, buttonText[type])), React.createElement("div", { className: `h-1 w-full ${backgroundColors[type]}`, style: style.progressBody }, React.createElement("div", { ref: progressRef, className: `h-full w-full ${progressColors[type]}`, style: style.progress }))))); } const icons = { success: SuccessIcon, error: AlertIcon, warning: WarningIcon, info: InfoIcon, loading: LoaderIcon }; function Toaster({ type = 'success', message, className, styles }) { // const [isVisible, setIsVisible] = useState(true); const { backgroundColors, textColors, style, setStyles, setClassName } = useStyleHook(); const Icon = icons[type]; useEffect(() => { setClassName(className); setStyles(styles); }, [className, styles]); // useEffect(() => { // const timer = setTimeout(() => { // setIsVisible(false) // }, 2900) // Slightly less than the removal time to allow for fade out // return () => clearTimeout(timer) // }, []) // if (!isVisible) return null return (React.createElement("div", { className: `text-wrap text-sm max-w-72 rounded-lg px-3 py-2 shadow z-50 animate-enter mb-2 ${backgroundColors[type]}`, style: style.body }, React.createElement("div", { className: `flex items-center gap-2`, style: style.contentBody }, React.createElement(Icon, { className: `h-5 w-5`, style: style.icon }), React.createElement("p", { className: `text-sm ${textColors[type]} `, style: style.message }, message)))); } function ToastContainer({ toasts, toaster, removeToast, toastModal, styles, className }) { const groupedToasts = toasts?.reduce((acc, toast) => { if (!acc[toast?.position]) { acc[toast?.position] = []; } acc[toast?.position].push(toast); return acc; }, {}); return (React.createElement(React.Fragment, null, ReactDOM.createPortal(React.createElement("div", { className: 'fixed top-0 left-0 right-0 z-50' }, ['top-left', 'top-center', 'top-right', 'middle-left', 'middle-center', 'middle-right', 'bottom-left', 'bottom-center', 'bottom-right'].map((position) => (React.createElement("div", { key: position, className: `fixed z-50 bg-transparent ${getPositionClasses(position)}` }, toaster?.filter((toast) => toast.position === position) ?.map((toast) => (React.createElement(Toaster, { key: toast.id, type: toast.type, message: toast.message, className: className, styles: styles })))))), !toastModal ? Object.entries(groupedToasts).map(([position, positionToasts]) => (React.createElement("div", { key: position, className: `fixed z-50 flex flex-col gap-2 ${getPositionClasses(position)}` }, positionToasts?.map((toast) => (React.createElement(Toast, { key: toast.id, id: toast.id, type: toast.type, message: toast.message, duration: toast.duration, onClose: () => removeToast(toast.id), styles: styles, className: className })))))) : React.createElement("div", { className: "fixed top-4 right-4 z-50 flex flex-col gap-2" }, toasts.map((toast) => (React.createElement(ToastModal, { key: toast.id, id: toast.id, type: toast.type, message: toast.message, duration: toast.duration, onClose: () => removeToast(toast.id), styles: styles, className: className }))))), document.body))); } const ToastContext = React.createContext(undefined); const MAX_TOASTS = 12; function ToastProvider({ children }) { const [toasts, setToasts] = React.useState([]); const [toasters, setToasters] = React.useState([]); const [toastModal, setToastModal] = React.useState(false); const [className, setClassName] = React.useState({}); const [styles, setStyles] = React.useState({}); const toastTimers = React.useRef(new Map()); const removeToast = React.useCallback((id) => { setToasts(prev => prev.filter(toast => toast.id !== id)); const timer = toastTimers.current.get(id); if (timer) { clearTimeout(timer); toastTimers.current.delete(id); } }, []); const removeToaster = React.useCallback((id) => { setToasters(prev => prev.filter(toast => toast.id !== id)); const timer = toastTimers.current.get(id); if (timer) { clearTimeout(timer); toastTimers.current.delete(id); } }, []); const addToast = React.useCallback(({ type, message, duration = 3000, position = 'top-center', toastModal = false, }) => { const id = Math.random()?.toString(36)?.substring(2, 9); const newToast = { id, type, message, duration, position }; setToasts(prev => { const updatedToasts = prev.length >= MAX_TOASTS ? [...prev.slice(1), newToast] : [...prev, newToast]; return updatedToasts; }); if (toastModal) { setToastModal(true); } else { setToastModal(false); } const timer = setTimeout(() => removeToast(id), duration); toastTimers.current.set(id, timer); return id; }, [removeToast]); const showToast = React.useCallback((type, message, duration = 5000, toastModal = false, position) => { const id = Math.random().toString(36).substring(2, 9); const newToast = { id, type, message, duration, position, createdAt: Date.now(), }; if (toastModal) { setToastModal(true); } else { setToastModal(false); } setToasts(prev => { const updatedToasts = prev.length >= MAX_TOASTS ? [...prev.slice(1), newToast] : [...prev, newToast]; return updatedToasts; }); const timer = setTimeout(() => { removeToast(id); }, duration); toastTimers.current.set(id, timer); }, [removeToast]); const addToaster = React.useCallback(({ message, type, position, duration = 5000 }) => { const id = Math.random().toString(36).substring(2, 9); let newToast = { id, message, type, position }; setToasters(prev => { const updatedToasts = prev.length >= MAX_TOASTS ? [...prev.slice(1), newToast] : [...prev, newToast]; return updatedToasts; }); const timer = setTimeout(() => removeToaster(id), duration); toastTimers.current.set(id, timer); return id; }, [removeToast]); React.useEffect(() => { return () => { toastTimers.current.forEach(clearTimeout); toastTimers.current.clear(); }; }, []); return (React.createElement(ToastContext.Provider, { value: { addToaster, addToast, showToast, removeToast, setClassName, setStyles } }, children, React.createElement(ToastContainer, { toasts: toasts, toaster: toasters, removeToast: removeToast, toastModal: toastModal, className: className, styles: styles }))); } const useToastHook = () => { const context = React.useContext(ToastContext); if (!context) { throw new Error('useToast must be used within a ToastProvider'); } return context; }; let globalAddToast = null; let globalAddToaster = null; function setGlobalAddToast(addToast) { globalAddToast = addToast; } function setGlobalAddToaster(addToaster) { globalAddToaster = addToaster; } function toast(type, message, duration, position = 'top-center') { if (typeof window !== 'undefined' && globalAddToast) { globalAddToast({ type, message, duration, position }); } else { console.error('Toast function called before initialization or on server-side or Please configure setAddToast Initializer'); } } function toaster(type, message, duration, position = 'top-center') { if (typeof window !== 'undefined' && globalAddToaster) { globalAddToaster({ type, message, duration, position }); } else { console.error('Toast function called before initialization or on server-side or Please configure setAddToaster Initializer'); } } export { AlertIcon, CircularCrossIcon, CrossIcon, InfoIcon, LoaderIcon, SuccessIcon, ToastContext, ToastProvider, WarningIcon, setGlobalAddToast, setGlobalAddToaster, toast, toaster, useProgressBarHook, useToastHook };