feat: add NotificationCenter and SystemStatus components for improved user notifications and system updates
This commit is contained in:
118
frontend/src/components/NotificationCenter.tsx
Normal file
118
frontend/src/components/NotificationCenter.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Bell, X, Info, AlertTriangle, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import { getNotifications, markNotificationRead, markAllNotificationsRead } from '../api/system';
|
||||
|
||||
const NotificationCenter: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: notifications = [] } = useQuery({
|
||||
queryKey: ['notifications'],
|
||||
queryFn: () => getNotifications(true),
|
||||
refetchInterval: 30000, // Poll every 30s
|
||||
});
|
||||
|
||||
const markReadMutation = useMutation({
|
||||
mutationFn: markNotificationRead,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['notifications'] });
|
||||
},
|
||||
});
|
||||
|
||||
const markAllReadMutation = useMutation({
|
||||
mutationFn: markAllNotificationsRead,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['notifications'] });
|
||||
},
|
||||
});
|
||||
|
||||
const unreadCount = notifications.length;
|
||||
|
||||
const getIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'success': return <CheckCircle className="w-5 h-5 text-green-500" />;
|
||||
case 'warning': return <AlertTriangle className="w-5 h-5 text-yellow-500" />;
|
||||
case 'error': return <AlertCircle className="w-5 h-5 text-red-500" />;
|
||||
default: return <Info className="w-5 h-5 text-blue-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="relative p-2 text-gray-400 hover:text-white focus:outline-none"
|
||||
>
|
||||
<Bell className="w-6 h-6" />
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute top-0 right-0 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-100 transform translate-x-1/4 -translate-y-1/4 bg-red-600 rounded-full">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setIsOpen(false)}
|
||||
></div>
|
||||
<div className="absolute right-0 z-20 w-80 mt-2 overflow-hidden bg-white rounded-md shadow-lg dark:bg-gray-800 ring-1 ring-black ring-opacity-5">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b dark:border-gray-700">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white">Notifications</h3>
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
onClick={() => markAllReadMutation.mutate()}
|
||||
className="text-xs text-blue-600 hover:text-blue-500 dark:text-blue-400"
|
||||
>
|
||||
Mark all read
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
No new notifications
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className="flex items-start px-4 py-3 border-b dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{getIcon(notification.type)}
|
||||
</div>
|
||||
<div className="ml-3 w-0 flex-1">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{notification.title}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{notification.message}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
{new Date(notification.created_at).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 flex-shrink-0 flex">
|
||||
<button
|
||||
onClick={() => markReadMutation.mutate(notification.id)}
|
||||
className="bg-white dark:bg-gray-800 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationCenter;
|
||||
Reference in New Issue
Block a user