Spaces:
Running
Running
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; |