Spaces:
Paused
Paused
| import React, { useState, useEffect, useRef, useCallback } from 'react'; | |
| import { | |
| FaTimes, | |
| FaCheckCircle, | |
| FaExclamationCircle, | |
| FaInfoCircle, | |
| FaExclamationTriangle | |
| } from 'react-icons/fa'; | |
| import './Notification.css'; | |
| const Notification = ({ | |
| notifications = [], | |
| position = 'top-right', | |
| animation = 'slide', | |
| stackDirection = 'down', | |
| maxNotifications = 5, | |
| spacing = 10, | |
| offset = { x: 20, y: 20 }, | |
| onDismiss, | |
| onAction, | |
| autoStackCollapse = false, | |
| theme = 'light' | |
| }) => { | |
| const [internalNotifications, setInternalNotifications] = useState([]); | |
| const [collapsed, setCollapsed] = useState(false); | |
| const timersRef = useRef({}); | |
| const handleDismiss = useCallback((id) => { | |
| if (timersRef.current[id]) { | |
| clearTimeout(timersRef.current[id]); | |
| delete timersRef.current[id]; | |
| } | |
| onDismiss?.(id); | |
| }, [onDismiss]); | |
| useEffect(() => { | |
| // Update internal notifications | |
| const processedNotifications = notifications.slice( | |
| stackDirection === 'up' ? -maxNotifications : 0, | |
| stackDirection === 'up' ? undefined : maxNotifications | |
| ); | |
| setInternalNotifications(processedNotifications); | |
| // Keep track of current timer IDs for this effect | |
| const currentTimerIds = []; | |
| // Set up auto-dismiss timers | |
| processedNotifications.forEach(notification => { | |
| if (notification.autoDismiss && notification.duration && !timersRef.current[notification.id]) { | |
| const timerId = setTimeout(() => { | |
| handleDismiss(notification.id); | |
| }, notification.duration); | |
| timersRef.current[notification.id] = timerId; | |
| currentTimerIds.push(notification.id); | |
| } | |
| }); | |
| // Cleanup function | |
| return () => { | |
| // Use the captured timer IDs and current ref | |
| const timers = timersRef.current; | |
| // Clear timers for notifications that were removed | |
| Object.keys(timers).forEach(id => { | |
| if (!processedNotifications.find(n => n.id === id)) { | |
| clearTimeout(timers[id]); | |
| delete timers[id]; | |
| } | |
| }); | |
| }; | |
| }, [notifications, maxNotifications, stackDirection, handleDismiss]); | |
| const handleAction = (notificationId, actionId, actionData) => { | |
| onAction?.(notificationId, actionId, actionData); | |
| }; | |
| const getIcon = (type, customIcon) => { | |
| if (customIcon) return customIcon; | |
| switch (type) { | |
| case 'success': | |
| return <FaCheckCircle />; | |
| case 'error': | |
| return <FaExclamationCircle />; | |
| case 'warning': | |
| return <FaExclamationTriangle />; | |
| case 'info': | |
| return <FaInfoCircle />; | |
| default: | |
| return null; | |
| } | |
| }; | |
| const getPositionClasses = () => { | |
| const classes = ['notification-container']; | |
| // Position classes | |
| switch (position) { | |
| case 'top-left': | |
| classes.push('position-top-left'); | |
| break; | |
| case 'top-center': | |
| classes.push('position-top-center'); | |
| break; | |
| case 'top-right': | |
| classes.push('position-top-right'); | |
| break; | |
| case 'bottom-left': | |
| classes.push('position-bottom-left'); | |
| break; | |
| case 'bottom-center': | |
| classes.push('position-bottom-center'); | |
| break; | |
| case 'bottom-right': | |
| classes.push('position-bottom-right'); | |
| break; | |
| case 'center': | |
| classes.push('position-center'); | |
| break; | |
| default: | |
| classes.push('position-top-right'); | |
| } | |
| // Stack direction | |
| if (stackDirection === 'up') { | |
| classes.push('stack-up'); | |
| } | |
| // Theme | |
| classes.push(`theme-${theme}`); | |
| return classes.join(' '); | |
| }; | |
| const getAnimationClass = (index) => { | |
| return `animation-${animation} animation-${animation}-${index}`; | |
| }; | |
| const containerStyle = { | |
| '--spacing': `${spacing}px`, | |
| '--offset-x': `${offset.x}px`, | |
| '--offset-y': `${offset.y}px`, | |
| }; | |
| if (internalNotifications.length === 0) return null; | |
| return ( | |
| <div | |
| className={getPositionClasses()} | |
| style={containerStyle} | |
| > | |
| {autoStackCollapse && internalNotifications.length > 3 && ( | |
| <button | |
| className="notification-collapse-toggle" | |
| onClick={() => setCollapsed(!collapsed)} | |
| > | |
| {collapsed ? `Show ${internalNotifications.length} notifications` : 'Collapse'} | |
| </button> | |
| )} | |
| <div className={`notification-list ${collapsed ? 'collapsed' : ''}`}> | |
| {internalNotifications.map((notification, index) => ( | |
| <div | |
| key={notification.id} | |
| className={`notification notification-${notification.type || 'default'} ${getAnimationClass(index)} ${notification.className || ''}`} | |
| style={{ | |
| '--animation-delay': `${index * 0.05}s`, | |
| ...notification.style | |
| }} | |
| > | |
| {notification.showProgress && notification.duration && ( | |
| <div | |
| className="notification-progress" | |
| style={{ | |
| '--duration': `${notification.duration}ms` | |
| }} | |
| /> | |
| )} | |
| <div className="notification-content"> | |
| {(notification.icon !== false) && ( | |
| <div className="notification-icon"> | |
| {getIcon(notification.type, notification.icon)} | |
| </div> | |
| )} | |
| <div className="notification-body"> | |
| {notification.title && ( | |
| <div className="notification-title">{notification.title}</div> | |
| )} | |
| {notification.message && ( | |
| <div className="notification-message"> | |
| {typeof notification.message === 'string' | |
| ? notification.message | |
| : notification.message | |
| } | |
| </div> | |
| )} | |
| {notification.actions && notification.actions.length > 0 && ( | |
| <div className="notification-actions"> | |
| {notification.actions.map((action) => ( | |
| <button | |
| key={action.id} | |
| className={`notification-action ${action.className || ''}`} | |
| onClick={() => handleAction(notification.id, action.id, action.data)} | |
| style={action.style} | |
| > | |
| {action.label} | |
| </button> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| {notification.dismissible !== false && ( | |
| <button | |
| className="notification-close" | |
| onClick={() => handleDismiss(notification.id)} | |
| aria-label="Dismiss notification" | |
| > | |
| <FaTimes /> | |
| </button> | |
| )} | |
| </div> | |
| {notification.footer && ( | |
| <div className="notification-footer"> | |
| {notification.footer} | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default Notification; |