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)
This commit is contained in:
1 parent
55eebde0ba
commit
ab37b477df
106 files changed
+15261
-301
No files matched your search
@@ -0,0 +1,154 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user