import { Icon, IconifyIconProps } from '@iconify/react/dist/iconify.js'; import cx from 'clsx'; import { filter, isEqual } from 'lodash-es'; import { createContext, createRef, ReactNode, RefObject, useCallback, useContext, useEffect, useState, } from 'react'; import { createPortal } from 'react-dom'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { v4 } from 'uuid'; import styles from './Notification.module.css'; export enum NotificationType { INFO, PROMPTION, SUCCESS, WARNING, ERROR, CUSTOM, } export enum ToastDuration { MANUAL = 0, SHORT = 1500, MEDIUM = 3000, LONG = 5000, } interface NotificationFunctions { addNotification( kind: NotificationType, title?: string, message?: ReactNode, icon?: IconifyIconProps['icon'], duration?: number, ): string; removeNotification(id: string): void; showToast( kind: NotificationType, message: string, icon?: IconifyIconProps['icon'], duration?: ToastDuration, ): string; } const NotificationStyleMap = { [NotificationType.INFO]: styles.kind_info, [NotificationType.PROMPTION]: styles.kind_promption, [NotificationType.SUCCESS]: styles.kind_success, [NotificationType.WARNING]: styles.kind_warning, [NotificationType.ERROR]: styles.kind_error, [NotificationType.CUSTOM]: styles.kind_custom, }; const NotificationHostContext = createContext({ addNotification: () => '', removeNotification: () => {}, showToast: () => '', }); type NotificationProps = { kind?: NotificationType; nid: string; icon?: IconifyIconProps['icon']; title?: string; message?: ReactNode; duration?: number; ref: RefObject; closeAction?: (id: string) => void; }; const Notification = ({ kind = NotificationType.INFO, nid, icon, title, message, duration = 3000, ref, closeAction, }: NotificationProps) => { useEffect(() => { if (duration > 0) { const timer = setTimeout(() => { closeAction?.(nid); }, duration); return () => clearTimeout(timer); } }, [nid, duration, closeAction]); return (
{icon && }
{title &&
{title}
} {message &&
{message}
}
{duration === 0 && ( closeAction?.(nid)} /> )}
); }; type ToastProps = { kind: NotificationType; tid: string; message?: string; icon?: string; duration?: ToastDuration; ref: RefObject; closeAction: () => void; }; const Toast = ({ kind, tid, message, icon, duration = ToastDuration.MEDIUM, ref, closeAction, }: ToastProps) => { useEffect(() => { if (duration !== ToastDuration.MANUAL) { const timer = setTimeout(() => { closeAction?.(tid); }, duration); return () => clearTimeout(timer); } }, [tid, duration, closeAction]); return (
{icon && }
{message}
{duration === ToastDuration.MANUAL && ( closeAction?.(tid)} /> )}
); }; export function useNotification() { const functions = useContext(NotificationHostContext); return functions; } type NotificationElement = { id: string; element: ReactNode; ref: RefObject; }; type NotificationsProps = { defaultDuration?: number; maxNotifications?: number; children?: ReactNode; }; export function Notifications({ defaultDuration = 3000, maxNotifications = 5, children, }: NotificationsProps) { const [notifications, setNotifications] = useState([]); const [toasts, setToasts] = useState([]); const removeNotification = useCallback((id: string) => { setNotifications((prev) => filter(prev, (n) => !isEqual(n.id, id))); }, []); const addNotification = useCallback( ( kind: NotificationType, title?: string, message?: ReactNode, icon?: IconifyIconProps['icon'], duration?: number, ) => { const id = v4(); const ref = createRef(null); const newNotify = ( ); setNotifications((prev) => [...prev, { id, element: newNotify, ref } as NotificationElement]); return id; }, [removeNotification, defaultDuration], ); const removeToast = useCallback((id: string) => { setToasts((prev) => filter(prev, (n) => !isEqual(n.id, id))); }, []); const showToast = useCallback( ( kind: NotificationType, message?: string, icon?: IconifyIconProps['icon'], duration?: ToastDuration, ) => { const id = v4(); const ref = createRef(null); const newToast = ( ); setToasts((prev) => [...prev, { id, element: newToast, ref } as NotificationElement]); return id; }, [removeToast], ); return ( '', showModalDialog: () => '', closeDialog: () => {}, showToast, }}> {children} {createPortal(
{notifications.slice(0, maxNotifications).map(({ id, element, ref }) => ( {element} ))}
, document.body, )} {createPortal(
{toasts.slice(0, 1).map(({ id, element, ref }) => ( {element} ))}
, document.body, )}
); }