From 6adc8d96e88758d57cda7c19018773414134faf3 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Tue, 11 Nov 2025 03:20:01 -0300 Subject: [PATCH] refactor: reorganize hooks and add safety checks fix(api): add validation for listing and seller in bid route feat(telemetry): add userId to MarketTelemetry style(globe): add null check for canvas element chore: add lint:fix script to package.json --- app/api/listings/[id]/bid/route.ts | 34 +++++++-- components/marketplace/AnalyticsDashboard.tsx | 24 +++--- components/marketplace/BidModal.tsx | 14 ++-- components/marketplace/CreateAdModal.tsx | 12 +-- components/marketplace/MyAdsIntentionList.tsx | 8 +- components/messages/MessagesPanel.tsx | 27 ++++--- components/ui/globe.tsx | 7 +- components/ui/particles.tsx | 75 ++++++++++--------- lib/ai/generate-item-structure.ts | 3 +- lib/mongodb/connection.ts | 2 +- lib/telemetry/MarketTelemetry.ts | 1 + package.json | 1 + 12 files changed, 119 insertions(+), 89 deletions(-) diff --git a/app/api/listings/[id]/bid/route.ts b/app/api/listings/[id]/bid/route.ts index 17d6cbb..e85486c 100644 --- a/app/api/listings/[id]/bid/route.ts +++ b/app/api/listings/[id]/bid/route.ts @@ -61,12 +61,32 @@ export async function POST( ); } + // Garantir existência do anúncio e vendedor antes de notificar + const listing = result.listing; + if (!listing) { + return NextResponse.json( + { success: false, error: "Anúncio não encontrado após registrar o lance" }, + { status: 404 }, + ); + } + + if (typeof listing.seller_id !== "number") { + secureLogger.warn("SellerId inválido ao criar notificação de lance", { + listingId, + sellerId: listing.seller_id, + }); + return NextResponse.json( + { success: false, error: "Vendedor inválido para notificação" }, + { status: 500 }, + ); + } + // Criar notificação para o vendedor await createNotification({ - user_id: result.listing?.seller_id, + user_id: listing.seller_id, notification_type: "bid", title: "Novo lance!", - message: `Novo lance de R$ ${amount.toFixed(2)} no item "${result.listing?.item_name}"`, + message: `Novo lance de R$ ${amount.toFixed(2)} no item "${listing.item_name}"`, related_id: listingId, send_whatsapp: false, }); @@ -77,7 +97,7 @@ export async function POST( user_id: result.previousBidderId, notification_type: "bid", title: "Lance superado", - message: `Seu lance foi superado no item "${result.listing?.item_name}"`, + message: `Seu lance foi superado no item "${listing.item_name}"`, related_id: listingId, send_whatsapp: false, }); @@ -92,11 +112,11 @@ export async function POST( metadata: { bid_amount: amount, payment_method: payment_method, - previous_bid: result.listing?.current_bid_amount - ? Number(result.listing?.current_bid_amount) + previous_bid: listing.current_bid_amount + ? Number(listing.current_bid_amount) : null, - min_increment: result.listing?.min_bid_increment - ? Number(result.listing?.min_bid_increment) + min_increment: listing.min_bid_increment + ? Number(listing.min_bid_increment) : null, }, }); diff --git a/components/marketplace/AnalyticsDashboard.tsx b/components/marketplace/AnalyticsDashboard.tsx index de08da9..59bd5ec 100644 --- a/components/marketplace/AnalyticsDashboard.tsx +++ b/components/marketplace/AnalyticsDashboard.tsx @@ -40,18 +40,6 @@ export function AnalyticsDashboard() { const [analytics, setAnalytics] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { - if (user?.userId) { - loadUserListings(); - } - }, [user?.userId, loadUserListings]); - - useEffect(() => { - if (selectedAd) { - loadAnalytics(selectedAd); - } - }, [selectedAd, loadAnalytics]); - const loadUserListings = async () => { setLoading(true); try { @@ -82,6 +70,18 @@ export function AnalyticsDashboard() { } }; + useEffect(() => { + if (user?.userId) { + loadUserListings(); + } + }, [user?.userId, loadUserListings]); + + useEffect(() => { + if (selectedAd) { + loadAnalytics(selectedAd); + } + }, [selectedAd, loadAnalytics]); + if (!user) { return (
diff --git a/components/marketplace/BidModal.tsx b/components/marketplace/BidModal.tsx index e326af7..d974f06 100644 --- a/components/marketplace/BidModal.tsx +++ b/components/marketplace/BidModal.tsx @@ -103,13 +103,6 @@ export function BidModal({ } }, [minBid, bidAmount]); - // Buscar histórico de lances quando abrir modal - useEffect(() => { - if (open && listing) { - fetchBidHistory(); - } - }, [open, listing?.listing_id, fetchBidHistory, listing]); - const fetchBidHistory = async () => { if (!listing) return; @@ -131,6 +124,13 @@ export function BidModal({ } }; + // Buscar histórico de lances quando abrir modal + useEffect(() => { + if (open && listing) { + fetchBidHistory(); + } + }, [open, listing?.listing_id, fetchBidHistory, listing]); + const handleBid = async () => { const amount = parseFloat(bidAmount); diff --git a/components/marketplace/CreateAdModal.tsx b/components/marketplace/CreateAdModal.tsx index 163cbe9..4bb2c39 100644 --- a/components/marketplace/CreateAdModal.tsx +++ b/components/marketplace/CreateAdModal.tsx @@ -204,12 +204,6 @@ export function CreateAdModal({ }; // Carregar servidores ao abrir o modal - useEffect(() => { - if (open) { - loadServers(); - } - }, [open, loadServers]); - const loadServers = async () => { setLoadingServers(true); try { @@ -225,6 +219,12 @@ export function CreateAdModal({ } }; + useEffect(() => { + if (open) { + loadServers(); + } + }, [open, loadServers]); + const handleClose = () => { if (!loading) { setSelectedItem(null); diff --git a/components/marketplace/MyAdsIntentionList.tsx b/components/marketplace/MyAdsIntentionList.tsx index cf7f65f..e239db9 100644 --- a/components/marketplace/MyAdsIntentionList.tsx +++ b/components/marketplace/MyAdsIntentionList.tsx @@ -135,10 +135,6 @@ export function MyAdsIntentionList({ "all" | "pending" | "accepted" | "rejected" >("all"); - useEffect(() => { - loadIntentions(); - }, [loadIntentions]); - const loadIntentions = async () => { setLoading(true); try { @@ -162,6 +158,10 @@ export function MyAdsIntentionList({ } }; + useEffect(() => { + loadIntentions(); + }, [loadIntentions]); + const pendingCount = intentions.filter((i) => i.status === "pending").length; if (loading) { diff --git a/components/messages/MessagesPanel.tsx b/components/messages/MessagesPanel.tsx index 303b43b..74c7c0a 100644 --- a/components/messages/MessagesPanel.tsx +++ b/components/messages/MessagesPanel.tsx @@ -69,20 +69,6 @@ export function MessagesPanel({ setIsLoading(false); } }; - - useEffect(() => { - if (open && user) { - fetchChats(); - // Marcar todos como lidos ao abrir - markAllAsRead(); - } - }, [ - open, - user, - fetchChats, // Marcar todos como lidos ao abrir - markAllAsRead, - ]); - const markAllAsRead = async () => { try { await fetch("/api/chats/mark-all-read", { @@ -105,6 +91,19 @@ export function MessagesPanel({ } }; + useEffect(() => { + if (open && user) { + fetchChats(); + // Marcar todos como lidos ao abrir + markAllAsRead(); + } + }, [ + open, + user, + fetchChats, // Marcar todos como lidos ao abrir + markAllAsRead, + ]); + const handleChatClick = (chatId: string, _listingId: number) => { setSelectedChatId(chatId); setShowChatModal(true); diff --git a/components/ui/globe.tsx b/components/ui/globe.tsx index d314a9f..35ae382 100644 --- a/components/ui/globe.tsx +++ b/components/ui/globe.tsx @@ -93,7 +93,12 @@ export function Globe({ }, }); - setTimeout(() => (canvasRef.current?.style.opacity = "1"), 0); + setTimeout(() => { + const el = canvasRef.current; + if (el) { + el.style.opacity = "1"; + } + }, 0); return () => { globe.destroy(); window.removeEventListener("resize", onResize); diff --git a/components/ui/particles.tsx b/components/ui/particles.tsx index 6353387..021c2d4 100644 --- a/components/ui/particles.tsx +++ b/components/ui/particles.tsx @@ -101,42 +101,7 @@ export const Particles: React.FC = ({ const rafID = useRef(null); const resizeTimeout = useRef(null); - useEffect(() => { - if (canvasRef.current) { - context.current = canvasRef.current.getContext("2d"); - } - initCanvas(); - animate(); - - const handleResize = () => { - if (resizeTimeout.current) { - clearTimeout(resizeTimeout.current); - } - resizeTimeout.current = setTimeout(() => { - initCanvas(); - }, 200); - }; - - window.addEventListener("resize", handleResize); - - return () => { - if (rafID.current != null) { - window.cancelAnimationFrame(rafID.current); - } - if (resizeTimeout.current) { - clearTimeout(resizeTimeout.current); - } - window.removeEventListener("resize", handleResize); - }; - }, [animate, initCanvas]); - - useEffect(() => { - onMouseMove(); - }, [onMouseMove]); - - useEffect(() => { - initCanvas(); - }, [initCanvas]); + const initCanvas = () => { resizeCanvas(); @@ -302,6 +267,44 @@ export const Particles: React.FC = ({ rafID.current = window.requestAnimationFrame(animate); }; + // Efeitos devem vir após a declaração das funções referenciadas + useEffect(() => { + if (canvasRef.current) { + context.current = canvasRef.current.getContext("2d"); + } + initCanvas(); + animate(); + + const handleResize = () => { + if (resizeTimeout.current) { + clearTimeout(resizeTimeout.current); + } + resizeTimeout.current = setTimeout(() => { + initCanvas(); + }, 200); + }; + + window.addEventListener("resize", handleResize); + + return () => { + if (rafID.current != null) { + window.cancelAnimationFrame(rafID.current); + } + if (resizeTimeout.current) { + clearTimeout(resizeTimeout.current); + } + window.removeEventListener("resize", handleResize); + }; + }, [animate, initCanvas]); + + useEffect(() => { + onMouseMove(); + }, [onMouseMove]); + + useEffect(() => { + initCanvas(); + }, [initCanvas]); + return (
= new Map(); private batchInterval: NodeJS.Timeout | null = null; diff --git a/package.json b/package.json index 93f36ae..49828d1 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start": "NODE_ENV=production tsx server.ts", "start:next": "next start", "lint": "biome check", + "lint:fix": "pnpm format && biome check --write && biome check --write --unsafe && biome check --reporter summary", "format": "biome format --write", "db:migrate": "tsx lib/db/migrations/run-migrations.ts", "db:seed": "tsx lib/db/seeds/servers-seed.ts",