|
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(() => { |
|
|
|
const processedNotifications = notifications.slice( |
|
stackDirection === 'up' ? -maxNotifications : 0, |
|
stackDirection === 'up' ? undefined : maxNotifications |
|
); |
|
|
|
setInternalNotifications(processedNotifications); |
|
|
|
|
|
const currentTimerIds = []; |
|
|
|
|
|
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); |
|
} |
|
}); |
|
|
|
|
|
return () => { |
|
|
|
const timers = timersRef.current; |
|
|
|
|
|
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']; |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
if (stackDirection === 'up') { |
|
classes.push('stack-up'); |
|
} |
|
|
|
|
|
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; |