From 30ba5bfceb3a45f77ea3c4ad8681d744b7e83f59 Mon Sep 17 00:00:00 2001 From: lizerk Date: Mon, 9 Mar 2026 09:52:49 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B0=20?= =?UTF-8?q?=D1=81=D0=BE=20=D0=B2=D1=81=D0=B5=D0=BC=D0=B8=20=D1=83=D0=B2?= =?UTF-8?q?=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Полный список всех уведомлений с прокруткой - Поиск по заголовку и содержимому - Фильтрация по статусу (все/непрочитанные/прочитанные) - Фильтрация по категориям - Кнопка "Прочитать все" для непрочитанных - Красивые карточки с иконками категорий - Клик на уведомление переходит по action_url - Responsive дизайн --- frontend/src/app/notifications/page.tsx | 244 ++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 frontend/src/app/notifications/page.tsx diff --git a/frontend/src/app/notifications/page.tsx b/frontend/src/app/notifications/page.tsx new file mode 100644 index 00000000..f67eeef2 --- /dev/null +++ b/frontend/src/app/notifications/page.tsx @@ -0,0 +1,244 @@ +'use client'; + +import { AppShell } from '@/components/AppShell'; +import { useNotifications } from '@/hooks/useApi'; +import { Bell, Check, CheckCheck, Filter, Search, Trash2 } from 'lucide-react'; +import { useState, useMemo } from 'react'; +import { formatDistanceToNow } from 'date-fns'; +import { ru } from 'date-fns/locale/ru'; +import Link from 'next/link'; + +export default function NotificationsPage() { + const { notifications: notificationsData, loading, markAsRead, markAllAsRead } = useNotifications(); + const notifications = Array.isArray(notificationsData) ? notificationsData : []; + + const [search, setSearch] = useState(''); + const [filterRead, setFilterRead] = useState<'all' | 'unread' | 'read'>('all'); + const [filterCategory, setFilterCategory] = useState(''); + const [filtersOpen, setFiltersOpen] = useState(false); + + // Уникальные категории + const categories = useMemo(() => { + const cats = new Set(); + notifications.forEach((n: any) => { + if (n.category_name) cats.add(n.category_name); + }); + return Array.from(cats).sort(); + }, [notifications]); + + // Фильтрация + const filteredNotifications = useMemo(() => { + return notifications.filter((n: any) => { + // Фильтр по прочитанности + if (filterRead === 'unread' && n.is_read) return false; + if (filterRead === 'read' && !n.is_read) return false; + + // Фильтр по категории + if (filterCategory && n.category_name !== filterCategory) return false; + + // Поиск + if (search) { + const q = search.toLowerCase(); + const title = (n.title || '').toLowerCase(); + const message = (n.message || '').toLowerCase(); + if (!title.includes(q) && !message.includes(q)) return false; + } + + return true; + }); + }, [notifications, search, filterRead, filterCategory]); + + const unreadCount = notifications.filter((n: any) => !n.is_read).length; + + const handleNotificationClick = async (notification: any) => { + if (!notification.is_read) { + await markAsRead(notification.id); + } + if (notification.action_url) { + window.location.href = notification.action_url; + } + }; + + const getCategoryIcon = (icon?: string) => { + // Можно добавить маппинг иконок по необходимости + return icon || '📋'; + }; + + return ( + +
+ {/* Заголовок */} +
+
+

Уведомления

+

+ {unreadCount > 0 ? `${unreadCount} непрочитанных` : 'Все прочитано'} +

+
+ {unreadCount > 0 && ( + + )} +
+ + {/* Поиск и фильтры */} +
+
+
+ + setSearch(e.target.value)} + placeholder="Поиск по уведомлениям..." + className="w-full rounded-lg border border-gray-200 bg-gray-50 py-2.5 pl-9 pr-3 text-sm text-gray-800 transition focus:border-sky-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-sky-100" + /> +
+ +
+ + {filtersOpen && ( +
+ + + + + {(filterRead !== 'all' || filterCategory) && ( + + )} +
+ )} +
+ + {/* Список уведомлений */} + {loading ? ( +
+
+

Загрузка уведомлений...

+
+ ) : filteredNotifications.length === 0 ? ( +
+ +

+ {search || filterRead !== 'all' || filterCategory + ? 'Нет уведомлений по заданным критериям' + : 'Нет уведомлений'} +

+

+ {search || filterRead !== 'all' || filterCategory + ? 'Попробуйте изменить фильтры' + : 'Все новые уведомления появятся здесь'} +

+
+ ) : ( +
+ {filteredNotifications.map((notification: any) => ( +
handleNotificationClick(notification)} + className={`group cursor-pointer rounded-xl border transition hover:shadow-md ${ + notification.is_read + ? 'border-gray-200 bg-white' + : 'border-sky-200 bg-sky-50/30' + }`} + > +
+ {/* Иконка */} +
+ {getCategoryIcon(notification.icon)} +
+ + {/* Содержимое */} +
+
+

+ {notification.title} +

+ {!notification.is_read && ( +
+ )} +
+ +

+ {notification.short_message || notification.message} +

+ +
+ {notification.category_name && ( + + {notification.category_name} + + )} + + {formatDistanceToNow(new Date(notification.created_at), { + addSuffix: true, + locale: ru, + })} + +
+
+
+
+ ))} +
+ )} +
+
+ ); +}