'use client'; import { useState, useRef, useEffect } from 'react'; import { useNotifications } from '@/hooks/useNotifications'; import { useNavBadgesRefresh } from '@/contexts/NavBadgesContext'; import type { Notification } from '@/api/notifications'; const BELL_POSITION = { right: 24, bottom: 25 }; const PANEL_WIDTH = 360; const PANEL_MAX_HEIGHT = '70vh'; function formatDate(s: string) { try { const d = new Date(s); const now = new Date(); const sameDay = d.toDateString() === now.toDateString(); if (sameDay) return d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); } catch { return ''; } } function NotificationItem({ notification, onHover, onClick, }: { notification: Notification; onHover: () => void; onClick: () => void; }) { const hoveredRef = useRef(false); const handleMouseEnter = () => { hoveredRef.current = true; onHover(); }; return (
e.key === 'Enter' && onClick()} style={{ padding: '12px 16px', borderBottom: '1px solid var(--md-sys-color-outline-variant, #eee)', cursor: notification.action_url ? 'pointer' : 'default', backgroundColor: notification.is_read ? 'transparent' : '#bfa9ff', transition: 'background-color 0.15s ease', }} >
{notification.title || 'Уведомление'}
{notification.message}
{formatDate(notification.created_at)}
); } const SCROLL_LOAD_MORE_THRESHOLD = 80; export function NotificationBell() { const refreshNavBadges = useNavBadgesRefresh(); const { list, unreadCount, loading, loadingMore, hasMore, fetchList, fetchMore, markOneAsRead, markAllAsRead, } = useNotifications({ onNavBadgesUpdate: refreshNavBadges ?? undefined }); const [open, setOpen] = useState(false); const panelRef = useRef(null); const scrollRef = useRef(null); useEffect(() => { if (open) fetchList(); }, [open, fetchList]); const handleScroll = () => { const el = scrollRef.current; if (!el || !hasMore || loadingMore) return; const { scrollTop, scrollHeight, clientHeight } = el; if (scrollTop + clientHeight >= scrollHeight - SCROLL_LOAD_MORE_THRESHOLD) { fetchMore(); } }; // Порядок не сортируем: API уже отдаёт is_read, -created_at. Иначе при подгрузке непрочитанные из новой страницы уезжали бы наверх, а скролл остаётся внизу. useEffect(() => { if (!open) return; const handleClickOutside = (e: MouseEvent) => { if ( panelRef.current && !panelRef.current.contains(e.target as Node) && !(e.target as HTMLElement).closest('[data-notification-bell]') ) { setOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [open]); return ( <>
{/* Панель уведомлений — выезжает справа от колокольчика */} {open && (
Уведомления {unreadCount > 0 && ( )}
{loading ? (
Загрузка…
) : list.length === 0 ? (
Нет уведомлений
) : ( <> {list.map((n) => ( !n.is_read && markOneAsRead(n.id)} onClick={() => { /* Не переходим по action_url — только открыта панель уведомлений */ }} /> ))} {loadingMore && (
Загрузка…
)} )}
)} {/* Кнопка-колокольчик */}
); }