feat: add useNotifications hook and NotificationBell component

This commit is contained in:
2026-07-05 23:18:18 +08:00
parent adb875976a
commit f47b7aaf87
2 changed files with 286 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
import { useState, useEffect, useCallback } from 'react';
import api from '../api';
interface Notification {
id: number;
type: string;
title: string;
content: string;
link: string | null;
createdAt: string;
}
export function useNotifications() {
const [unreadCount, setUnreadCount] = useState(0);
const [latestNotification, setLatestNotification] = useState<Notification | null>(null);
const fetchUnreadCount = useCallback(async () => {
try {
const data = await api.get('/notifications/unread-count') as unknown as { count: number };
setUnreadCount(data.count);
} catch {
// silent
}
}, []);
useEffect(() => {
fetchUnreadCount();
const token = localStorage.getItem('token');
if (!token) return;
const es = new EventSource(`/api/notifications/stream?token=${encodeURIComponent(token)}`);
es.onmessage = (event) => {
try {
const notification = JSON.parse(event.data) as Notification;
setUnreadCount((c) => c + 1);
setLatestNotification(notification);
} catch {
// ignore parse errors
}
};
es.onerror = () => {
es.close();
const interval = setInterval(() => {
fetchUnreadCount();
}, 60_000);
return () => clearInterval(interval);
};
return () => {
es.close();
};
}, [fetchUnreadCount]);
const markAsRead = useCallback(async (id: number) => {
try {
await api.put(`/notifications/${id}/read`);
setUnreadCount((c) => Math.max(0, c - 1));
} catch {
// silent
}
}, []);
const markAllAsRead = useCallback(async () => {
try {
await api.put('/notifications/read-all');
setUnreadCount(0);
} catch {
// silent
}
}, []);
return { unreadCount, latestNotification, markAsRead, markAllAsRead };
}