import React, { useState, useRef, useEffect } from 'react'; import { BellIcon, CheckIcon, XMarkIcon, InformationCircleIcon, ExclamationTriangleIcon, ExclamationCircleIcon, CheckCircleIcon, } from '@heroicons/react/24/outline'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { getApiPath } from '../../config/paths'; interface Notification { id: number; title: string; message: string; level: 'info' | 'warning' | 'error' | 'success'; source: string; read_at: string | null; created_at: string; data?: { taskUid?: string; projectUid?: string; [key: string]: any; }; } interface NotificationsDropdownProps { isDarkMode: boolean; } const NotificationsDropdown: React.FC = ({ isDarkMode, }) => { const { t } = useTranslation(); const navigate = useNavigate(); const [isOpen, setIsOpen] = useState(false); const [unreadCount, setUnreadCount] = useState(0); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(false); const dropdownRef = useRef(null); const fetchUnreadCount = async () => { try { const response = await fetch( getApiPath('notifications/unread-count'), { credentials: 'include', } ); if (response.ok) { const data = await response.json(); setUnreadCount(data.count || 0); } } catch (error) { console.error('Error fetching unread count:', error); } }; const fetchNotifications = async () => { setLoading(true); try { const response = await fetch( getApiPath('notifications?limit=20&includeRead=true'), { credentials: 'include', } ); if (response.ok) { const data = await response.json(); setNotifications(data.notifications || []); } } catch (error) { console.error('Error fetching notifications:', error); } finally { setLoading(false); } }; useEffect(() => { fetchUnreadCount(); const interval = setInterval(fetchUnreadCount, 30000); return () => clearInterval(interval); }, []); useEffect(() => { if (isOpen) { fetchNotifications(); } }, [isOpen]); const handleToggle = () => { setIsOpen(!isOpen); }; const handleMarkAsRead = async (id: number) => { try { const response = await fetch( getApiPath(`notifications/${id}/read`), { method: 'POST', credentials: 'include', } ); if (response.ok) { const data = await response.json(); const updatedNotification: Notification | undefined = data.notification; if (updatedNotification) { setNotifications((prev) => prev.map((n) => n.id === id ? { ...n, read_at: updatedNotification.read_at, } : n ) ); } fetchUnreadCount(); } } catch (error) { console.error('Error marking notification as read:', error); } }; const handleMarkAllAsRead = async () => { try { const response = await fetch( getApiPath('notifications/mark-all-read'), { method: 'POST', credentials: 'include', } ); if (response.ok) { const now = new Date().toISOString(); setNotifications((prev) => prev.map((n) => ({ ...n, read_at: now })) ); setUnreadCount(0); } } catch (error) { console.error('Error marking all notifications as read:', error); } }; const handleDelete = async (id: number) => { try { const response = await fetch(getApiPath(`notifications/${id}`), { method: 'DELETE', credentials: 'include', }); if (response.ok) { setNotifications((prev) => prev.filter((n) => n.id !== id)); fetchUnreadCount(); } } catch (error) { console.error('Error deleting notification:', error); } }; const getLevelIcon = (level: string) => { switch (level) { case 'success': return ( ); case 'warning': return ( ); case 'error': return ( ); default: return ( ); } }; const formatTimestamp = (timestamp: string) => { const date = new Date(timestamp); const now = new Date(); const diff = now.getTime() - date.getTime(); const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return t('notifications.justNow', 'Just now'); if (minutes < 60) return t('notifications.minutesAgo', '{{count}} min ago', { count: minutes, }); if (hours < 24) return t('notifications.hoursAgo', '{{count}}h ago', { count: hours, }); if (days < 7) return t('notifications.daysAgo', '{{count}}d ago', { count: days, }); return date.toLocaleDateString(); }; const handleNotificationClick = (notification: Notification) => { if (notification.data?.taskUid) { setIsOpen(false); navigate(`/task/${notification.data.taskUid}`); } else if (notification.data?.projectUid) { setIsOpen(false); navigate(`/project/${notification.data.projectUid}`); } }; useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpen]); return (
{isOpen && (

{t('notifications.title', 'Notifications')}

{unreadCount > 0 && ( )}
{loading ? (

{t('notifications.loading', 'Loading...')}

) : notifications.length === 0 ? (

{t( 'notifications.noNotifications', 'No notifications yet' )}

) : ( notifications.map((notification) => (
{getLevelIcon(notification.level)}
handleNotificationClick( notification ) } >

{notification.title}

{notification.message}

{formatTimestamp( notification.created_at )}

{!notification.read_at && ( )}
)) )}
)}
); }; export default NotificationsDropdown;