import React, { Fragment, FC, ReactNode, useState, useCallback, useMemo } from 'react'; import { Notification } from './Notification'; import { NotificationOptions, NotificationsContext } from './NotificationsContext'; import { AnimatePresence, motion } from 'framer-motion'; import css from './Notifications.module.css'; export interface NotificationsProps { limit?: number; timeout?: number; showClose?: boolean; preventFlooding?: boolean; children?: ReactNode; } // Hacky way to track unique versions of a notification let nextId = 0; export const Notifications: FC<NotificationsProps> = ({ children, limit, timeout, showClose, preventFlooding }) => { const [notifications, setNotifications] = useState<any[]>([]); const clearNotification = useCallback( (id: number) => setNotifications(notifications.filter(n => n.id !== id)), [notifications] ); const clearAllNotifications = useCallback(() => setNotifications([]), []); const notify = useCallback( (title: string, options: NotificationOptions = {}) => { // If we are flooded with the same message over and over, // dont add more of the same type. Mainly used for error use cases. if (preventFlooding) { const has = notifications.find(n => n.title === title); if (has) { return false; } } const id = nextId++; const obj = { title, id, variant: 'default', timeout, showClose, ...options }; const sorted = [obj, ...notifications]; // Clear old notifications if we hit limit if (sorted.length >= limit) { sorted.pop(); } // Update the container instance setNotifications(sorted); return id; }, [limit, notifications, preventFlooding, showClose, timeout] ); const notifyError = useCallback( (title: string, options: NotificationOptions = {}) => notify(title, { ...options, variant: 'error' }), [notify] ); const notifyWarning = useCallback( (title: string, options: NotificationOptions = {}) => notify(title, { ...options, variant: 'warning' }), [notify] ); const notifySuccess = useCallback( (title: string, options: NotificationOptions = {}) => notify(title, { ...options, variant: 'success' }), [notify] ); const values = useMemo( () => ({ notify, notifyError, notifyWarning, notifySuccess, clearNotification, clearAllNotifications }), [ clearNotification, clearAllNotifications, notify, notifyError, notifySuccess, notifyWarning ] ); return ( <Fragment> <NotificationsContext.Provider value={values}> {children} </NotificationsContext.Provider> <div className={css.container}> <div className={css.positions}> <AnimatePresence> {!!notifications.length && ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} > {notifications.map(n => ( <Notification {...n} key={n.id} onClose={clearNotification} /> ))} </motion.div> )} </AnimatePresence> </div> </div> </Fragment> ); }; Notifications.defaultProps = { limit: 10, timeout: 4000, showClose: true, preventFlooding: true };