From f1b2e47df86cd67302bc51f9b822123c69fc4b2e Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 02:54:16 +0300 Subject: [PATCH] feat(notifications): add in-app notification center panel - add notifications API client - add notifications modal in chat page header - show recent notification events with timestamps and count badge --- web/src/api/notifications.ts | 14 +++++++++ web/src/pages/ChatsPage.tsx | 60 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 web/src/api/notifications.ts diff --git a/web/src/api/notifications.ts b/web/src/api/notifications.ts new file mode 100644 index 0000000..924eb9b --- /dev/null +++ b/web/src/api/notifications.ts @@ -0,0 +1,14 @@ +import { http } from "./http"; + +export interface NotificationItem { + id: number; + user_id: number; + event_type: string; + payload: string; + created_at: string; +} + +export async function getNotifications(limit = 30): Promise { + const { data } = await http.get("/notifications", { params: { limit } }); + return data; +} diff --git a/web/src/pages/ChatsPage.tsx b/web/src/pages/ChatsPage.tsx index 1e60744..015236b 100644 --- a/web/src/pages/ChatsPage.tsx +++ b/web/src/pages/ChatsPage.tsx @@ -3,6 +3,7 @@ import { ChatList } from "../components/ChatList"; import { ChatInfoPanel } from "../components/ChatInfoPanel"; import { MessageComposer } from "../components/MessageComposer"; import { MessageList } from "../components/MessageList"; +import { getNotifications, type NotificationItem } from "../api/notifications"; import { searchMessages } from "../api/chats"; import type { Message } from "../chat/types"; import { useRealtime } from "../hooks/useRealtime"; @@ -26,6 +27,9 @@ export function ChatsPage() { const [searchQuery, setSearchQuery] = useState(""); const [searchLoading, setSearchLoading] = useState(false); const [searchResults, setSearchResults] = useState([]); + const [notificationsOpen, setNotificationsOpen] = useState(false); + const [notifications, setNotifications] = useState([]); + const [loadingNotifications, setLoadingNotifications] = useState(false); useRealtime(); @@ -72,6 +76,33 @@ export function ChatsPage() { }; }, [searchOpen, searchQuery, activeChatId]); + useEffect(() => { + if (!notificationsOpen) { + return; + } + let cancelled = false; + setLoadingNotifications(true); + void (async () => { + try { + const items = await getNotifications(30); + if (!cancelled) { + setNotifications(items); + } + } catch { + if (!cancelled) { + setNotifications([]); + } + } finally { + if (!cancelled) { + setLoadingNotifications(false); + } + } + })(); + return () => { + cancelled = true; + }; + }, [notificationsOpen]); + return (
@@ -96,6 +127,15 @@ export function ChatsPage() {
+ @@ -161,6 +201,26 @@ export function ChatsPage() {
) : null} + {notificationsOpen ? ( +
setNotificationsOpen(false)}> +
e.stopPropagation()}> +
+

Notifications

+ +
+ {loadingNotifications ?

Loading...

: null} + {!loadingNotifications && notifications.length === 0 ?

No notifications

: null} +
+ {notifications.map((item) => ( +
+

{item.event_type}

+

{new Date(item.created_at).toLocaleString()}

+
+ ))} +
+
+
+ ) : null}
); }