/
dmitriysturov
/
workspaces_platform
Обзор
Документация
Войти
/
dmitriysturov
/
workspaces_platform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
frontend/src/components/NotificationsMenu.tsx
150 строк
5 KB
dmitriysturov
vkr
04 июн 2026, 22:24
04 июн 2026, 22:24
971dfdc
Код
Авторство
О чём код?
import { useEffect, useRef, useState } from 'react' import { notificationsApi } from '../api/notificationsApi' import { navigate } from '../router/router' import type { NotificationItem } from '../types' import { formatDateTime } from '../utils/format' type NotificationsMenuProps = { token: string | null } export function NotificationsMenu({ token }: NotificationsMenuProps) { const [isOpen, setIsOpen] = useState(false) const [items, setItems] = useState<NotificationItem[]>([]) const [unreadCount, setUnreadCount] = useState(0) const [isLoading, setIsLoading] = useState(false) const [errorMessage, setErrorMessage] = useState<string | null>(null) const menuRef = useRef<HTMLDivElement | null>(null) async function refresh() { if (!token) { setItems([]) setUnreadCount(0) setErrorMessage(null) return } try { const [nextItems, nextCount] = await Promise.all([notificationsApi.getMyNotifications(token), notificationsApi.getUnreadCount(token)]) setItems(nextItems) setUnreadCount(nextCount.count) setErrorMessage(null) } catch (error) { console.warn('Failed to refresh notifications', error) setItems([]) setUnreadCount(0) setErrorMessage('Уведомления временно недоступны.') } } useEffect(() => { void refresh() }, [token]) useEffect(() => { if (!isOpen) { return } const handleClick = (event: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setIsOpen(false) } } document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) }, [isOpen]) async function openMenu() { setIsOpen((current) => !current) if (!isOpen && token) { setIsLoading(true) try { await refresh() } finally { setIsLoading(false) } } } async function handleNotificationClick(item: NotificationItem) { if (!token) { return } try { if (!item.readAt) { await notificationsApi.markNotificationRead(token, item.id) await refresh() } } catch (error) { console.warn('Failed to mark notification as read', error) setErrorMessage('Уведомления временно недоступны.') } setIsOpen(false) if (item.link) { navigate(item.link) } } async function markAllRead() { if (!token) { return } try { await notificationsApi.markAllNotificationsRead(token) await refresh() } catch (error) { console.warn('Failed to mark notifications as read', error) setErrorMessage('Уведомления временно недоступны.') } } async function deleteItem(item: NotificationItem) { if (!token) { return } try { await notificationsApi.deleteNotification(token, item.id) await refresh() } catch (error) { console.warn('Failed to delete notification', error) setErrorMessage('Уведомления временно недоступны.') } } return ( <div className="notifications-menu" ref={menuRef}> <button type="button" className="notifications-button" onClick={openMenu} aria-label="Уведомления"> <span aria-hidden="true">!</span> {unreadCount > 0 && <span className="notifications-badge">{unreadCount > 99 ? '99+' : unreadCount}</span>} </button> {isOpen && ( <div className="notifications-dropdown"> <header> <strong>Уведомления</strong> <button type="button" onClick={markAllRead} disabled={unreadCount === 0}> Прочитать все </button> </header> {isLoading && <p className="compact-empty">Загрузка...</p>} {!isLoading && errorMessage && <p className="compact-empty">{errorMessage}</p>} {!isLoading && !errorMessage && items.length === 0 && <p className="compact-empty">Новых уведомлений нет.</p>} {!isLoading && items.length > 0 && ( <ul> {items.slice(0, 10).map((item) => ( <li key={item.id} className={item.readAt ? '' : 'is-unread'}> <button type="button" className="notification-content" onClick={() => void handleNotificationClick(item)}> <strong>{item.title}</strong> {item.message && <span>{item.message}</span>} <small>{formatDateTime(item.createdAt)}</small> </button> <button type="button" className="notification-delete" onClick={() => void deleteItem(item)} aria-label="Удалить уведомление"> x </button> </li> ))} </ul> )} </div> )} </div> ) }