// NotificationsBell — bell icon with unread badge + dropdown panel of recent notifications. // Polls /api/notifications periodically; marks all as read when the panel opens. // Dropdown shows up to DROPDOWN_LIMIT recent items + footer link to full notifications page. const { useState: useBellState, useEffect: useBellEffect, useRef: useBellRef } = React; const DROPDOWN_LIMIT = 5; function BellIcon() { return ( ); } function NotificationsBell({ pollMs = 30000, onNavigate }) { const [items, setItems] = useBellState([]); const [unread, setUnread] = useBellState(0); const [open, setOpen] = useBellState(false); const panelRef = useBellRef(null); const fetchNow = async () => { try { const data = await api.get('/api/notifications'); if (data && data.__unauthorized) return; setItems((data && data.items) || []); setUnread((data && data.unread_count) || 0); } catch (e) { /* swallow — bell is non-critical */ } }; useBellEffect(() => { fetchNow(); const t = setInterval(fetchNow, pollMs); return () => clearInterval(t); }, [pollMs]); // Close on outside click useBellEffect(() => { if (!open) return; const onDown = (e) => { if (panelRef.current && !panelRef.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', onDown); return () => document.removeEventListener('mousedown', onDown); }, [open]); const onToggle = async () => { const next = !open; setOpen(next); if (next && unread > 0) { try { await api.post('/api/notifications/mark-all-read', {}); setUnread(0); const nowIso = new Date().toISOString(); setItems(items.map(i => i.read_at ? i : { ...i, read_at: nowIso })); } catch (e) { /* swallow */ } } }; const onViewAll = () => { setOpen(false); if (typeof onNavigate === 'function') onNavigate('notifications'); }; // formatTime — глобал из /dates.js, корректно конвертирует ISO+UTC в MSK. const visible = items.slice(0, DROPDOWN_LIMIT); return (
{open && (
{visible.length === 0 ? (
Уведомлений пока нет
) : ( <>
{visible.map(n => (
{n.text}
{formatTime(n.created_at)}
))}
)}
)}
); }