Files
pagodera/components/notifications/NotificationBell.tsx
T
Evandro Fonseca Junior ab37b477df feat: implement market page with QueryProvider and dynamic listings
- wrap root layout with QueryProvider for React Query
- replace market UnderConstruction placeholder with MarketClient and real data fetching
- implement search, pagination, and sort via search params using getActiveListings
- add necessary client components and queries
- update dependencies (e.g., prisma, pkg-types)
2025-11-05 01:58:03 -03:00

155 lines
4.1 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { Bell } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { NotificationDropdown } from "./NotificationDropdown";
import { useSession } from "@/lib/hooks/use-session";
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 effectiveUserId = userId || user?.userId;
// Buscar contagem de não lidas
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) {
setUnreadCount(data.count);
}
}
} 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;
fetchUnreadCount();
fetchNotifications();
const interval = setInterval(() => {
fetchUnreadCount();
if (isOpen) {
fetchNotifications();
}
}, 30000); // 30 segundos
return () => clearInterval(interval);
}, [effectiveUserId, isOpen]);
// Atualizar quando abrir popover
useEffect(() => {
if (isOpen) {
fetchNotifications();
}
}, [isOpen]);
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
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>
);
}