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
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-11 03:20:01 -03:00
1 parent 47c04de6fe
commit 6adc8d96e8
12 files changed
+119 -89

No files matched your search

+27 -7
View File
@@ -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,
},
});
+12 -12
View File
@@ -40,18 +40,6 @@ export function AnalyticsDashboard() {
const [analytics, setAnalytics] = useState<AdAnalytics | null>(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 (
<div className="text-center py-12">
+7 -7
View File
@@ -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);
+6 -6
View File
@@ -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);
@@ -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) {
+13 -14
View File
@@ -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);
+6 -1
View File
@@ -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);
+39 -36
View File
@@ -101,42 +101,7 @@ export const Particles: React.FC<ParticlesProps> = ({
const rafID = useRef<number | null>(null);
const resizeTimeout = useRef<NodeJS.Timeout | null>(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<ParticlesProps> = ({
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 (
<div
className={cn("pointer-events-none", className)}
+2 -1
View File
@@ -123,7 +123,7 @@ export async function generateItemStructure(
method: "POST",
headers: {
"Content-Type": "application/json",
"x-goog-api-key": apiKey,
...(apiKey ? { "x-goog-api-key": apiKey } : {}),
},
body: JSON.stringify({
contents: [
@@ -258,3 +258,4 @@ Instruções:
- Garante que o JSON seja minuciosamente válido.
`;
}
+1 -1
View File
@@ -3,7 +3,7 @@
* Singleton pattern para reutilizar conexão
*/
import type mongoose from "mongoose";
import mongoose from "mongoose";
function getMongoUrl(): string {
const url = process.env.MONGO_URL;
+1
View File
@@ -34,6 +34,7 @@ interface PendingEvent extends TelemetryEvent {
export class MarketTelemetry {
private sessionId: string;
private userId?: number;
private eventQueue: PendingEvent[] = [];
private viewStartTimes: Map<number, number> = new Map();
private batchInterval: NodeJS.Timeout | null = null;
+1
View File
@@ -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",