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
JavaScript
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 };