89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Bell } from 'lucide-react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { api } from '@emberclone/shared';
|
|
import type { AuditLog } from '@emberclone/shared';
|
|
|
|
export default function NotificationBell() {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
|
|
const { data: logs, isLoading } = useQuery({
|
|
queryKey: ['audit-log', 'notifications'],
|
|
queryFn: () => api.listAuditLog({ userId: 'me', limit: 10 }),
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (logs) {
|
|
const lastSeen = localStorage.getItem('lastSeenAuditAt');
|
|
if (!lastSeen) {
|
|
setUnreadCount(logs.length);
|
|
return;
|
|
}
|
|
|
|
const lastSeenDate = new Date(lastSeen).getTime();
|
|
const unread = logs.filter((log) => {
|
|
const logDate = new Date(log.createdAt).getTime();
|
|
return logDate > lastSeenDate;
|
|
}).length;
|
|
|
|
setUnreadCount(unread);
|
|
}
|
|
}, [logs]);
|
|
|
|
const handleOpen = () => {
|
|
setIsOpen(!isOpen);
|
|
if (!isOpen) {
|
|
localStorage.setItem('lastSeenAuditAt', new Date().toISOString());
|
|
setUnreadCount(0);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="relative">
|
|
<button
|
|
onClick={handleOpen}
|
|
className="relative p-2 text-slate-600 hover:text-slate-900 transition-colors rounded-full hover:bg-slate-100"
|
|
>
|
|
<Bell size={20} />
|
|
{unreadCount > 0 && (
|
|
<span className="absolute top-1.5 right-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] font-medium text-white ring-2 ring-white">
|
|
{unreadCount > 9 ? '9+' : unreadCount}
|
|
</span>
|
|
)}
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<>
|
|
<div
|
|
className="fixed inset-0 z-10"
|
|
onClick={() => setIsOpen(false)}
|
|
/>
|
|
<div className="absolute right-0 mt-2 w-80 overflow-hidden rounded-lg border border-slate-200 bg-white shadow-lg z-20">
|
|
<div className="border-b border-slate-100 bg-slate-50 px-4 py-2 text-sm font-semibold text-slate-700">
|
|
Notifications
|
|
</div>
|
|
<div className="max-h-96 overflow-y-auto">
|
|
{isLoading ? (
|
|
<div className="p-4 text-center text-sm text-slate-500">Loading...</div>
|
|
) : logs && logs.length > 0 ? (
|
|
<div className="divide-y divide-slate-100">
|
|
{logs.map((log) => (
|
|
<div key={log.id} className="p-3 text-sm hover:bg-slate-50 cursor-default">
|
|
<div className="font-medium text-slate-900">{log.action}</div>
|
|
<div className="text-xs text-slate-500 mt-1">
|
|
{new Date(log.createdAt).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="p-4 text-center text-sm text-slate-500">No notifications</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
} |