Files
2025-11-11 03:08:36 -03:00

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>
);
}