212 lines
6.3 KiB
TypeScript
212 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { Bell } from "lucide-react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { useSession } from "@/lib/hooks/use-session";
|
|
import { NotificationDropdown } from "./NotificationDropdown";
|
|
|
|
interface Notification {
|
|
notification_id: number;
|
|
notification_type: string;
|
|
title: string;
|
|
message: string;
|
|
is_read: boolean;
|
|
related_id: number | null;
|
|
created_at: string;
|
|
}
|
|
|
|
interface NotificationBellProps {
|
|
userId?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export function NotificationBell({ userId, className }: NotificationBellProps) {
|
|
const { user } = useSession();
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [_loading, setLoading] = useState(false);
|
|
const rawUnreadRef = useRef(0);
|
|
const baselineUnreadRef = useRef(0);
|
|
const baselineLoadedRef = useRef(false);
|
|
const storageKeyRef = useRef<string | null>(null);
|
|
|
|
const effectiveUserId = userId || user?.userId;
|
|
|
|
// Buscar contagem de não lidas
|
|
const persistBaseline = (value: number) => {
|
|
baselineUnreadRef.current = value;
|
|
if (typeof window !== "undefined" && storageKeyRef.current) {
|
|
window.localStorage.setItem(storageKeyRef.current, String(value));
|
|
}
|
|
};
|
|
|
|
const loadBaselineFromStorage = () => {
|
|
if (typeof window === "undefined" || !storageKeyRef.current) {
|
|
return undefined;
|
|
}
|
|
const stored = window.localStorage.getItem(storageKeyRef.current);
|
|
if (!stored) return undefined;
|
|
const parsed = Number.parseInt(stored, 10);
|
|
if (Number.isNaN(parsed)) return undefined;
|
|
return parsed;
|
|
};
|
|
|
|
const fetchUnreadCount = async () => {
|
|
if (!effectiveUserId) return;
|
|
|
|
try {
|
|
const res = await fetch("/api/notifications/unread-count");
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
rawUnreadRef.current = data.count;
|
|
if (!baselineLoadedRef.current) {
|
|
const storedBaseline = loadBaselineFromStorage();
|
|
if (storedBaseline !== undefined) {
|
|
const normalized = Math.min(storedBaseline, data.count);
|
|
persistBaseline(normalized);
|
|
} else {
|
|
persistBaseline(data.count);
|
|
}
|
|
baselineLoadedRef.current = true;
|
|
} else if (data.count < baselineUnreadRef.current) {
|
|
persistBaseline(data.count);
|
|
}
|
|
setUnreadCount(Math.max(0, data.count - baselineUnreadRef.current));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao buscar contagem de notificações:", error);
|
|
}
|
|
};
|
|
|
|
// Buscar notificações recentes
|
|
const fetchNotifications = async () => {
|
|
if (!effectiveUserId) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/notifications?limit=10");
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
setNotifications(data.notifications || []);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao buscar notificações:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Polling automático a cada 30 segundos
|
|
useEffect(() => {
|
|
if (!effectiveUserId) {
|
|
return;
|
|
}
|
|
storageKeyRef.current = `notifications:lastBaseline:${effectiveUserId}`;
|
|
const storedBaseline = loadBaselineFromStorage();
|
|
if (storedBaseline !== undefined) {
|
|
baselineUnreadRef.current = storedBaseline;
|
|
baselineLoadedRef.current = true;
|
|
} else {
|
|
baselineUnreadRef.current = 0;
|
|
baselineLoadedRef.current = false;
|
|
}
|
|
}, [effectiveUserId, loadBaselineFromStorage]);
|
|
|
|
useEffect(() => {
|
|
if (!effectiveUserId) return;
|
|
|
|
fetchUnreadCount();
|
|
fetchNotifications();
|
|
|
|
const interval = setInterval(() => {
|
|
fetchUnreadCount();
|
|
if (isOpen) {
|
|
fetchNotifications();
|
|
}
|
|
}, 30000); // 30 segundos
|
|
|
|
return () => clearInterval(interval);
|
|
}, [effectiveUserId, isOpen, fetchNotifications, fetchUnreadCount]);
|
|
|
|
// Atualizar quando abrir popover
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
fetchNotifications();
|
|
persistBaseline(rawUnreadRef.current);
|
|
setUnreadCount(0);
|
|
}
|
|
}, [isOpen, fetchNotifications, persistBaseline]);
|
|
|
|
const handleMarkAsRead = async (notificationId: number) => {
|
|
try {
|
|
const res = await fetch(`/api/notifications/${notificationId}/read`, {
|
|
method: "POST",
|
|
});
|
|
|
|
if (res.ok) {
|
|
// Atualizar estado local
|
|
setNotifications((prev) =>
|
|
prev.map((n) =>
|
|
n.notification_id === notificationId ? { ...n, is_read: true } : n,
|
|
),
|
|
);
|
|
// Atualizar contagem
|
|
rawUnreadRef.current = Math.max(0, rawUnreadRef.current - 1);
|
|
const newBaseline = Math.min(
|
|
baselineUnreadRef.current,
|
|
rawUnreadRef.current,
|
|
);
|
|
persistBaseline(newBaseline);
|
|
setUnreadCount((prev) => Math.max(0, prev - 1));
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao marcar notificação como lida:", error);
|
|
}
|
|
};
|
|
|
|
if (!effectiveUserId) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className={`relative ${className}`}>
|
|
<Bell className="h-5 w-5" />
|
|
{unreadCount > 0 && (
|
|
<Badge
|
|
variant="destructive"
|
|
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs rounded-full animate-pulse"
|
|
>
|
|
{unreadCount > 99 ? "99+" : unreadCount}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="w-80 p-0" align="end">
|
|
<NotificationDropdown
|
|
notifications={notifications}
|
|
onMarkAsRead={handleMarkAsRead}
|
|
onViewAll={() => {
|
|
setIsOpen(false);
|
|
// Navegar para página de notificações (implementar depois)
|
|
window.location.href = "/notifications";
|
|
}}
|
|
/>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|