'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({ embedded }: { embedded?: boolean }) {
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 && (
Загрузка…
)}
>
)}
)}
{/* Кнопка-колокольчик: в меню — как пункт навигации, иначе — круглая */}
{embedded ? (
) : (
)}
>
);
}