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:
Evandro Fonseca Junior committed 2025-11-05 01:58:03 -03:00
1 parent 55eebde0ba
commit ab37b477df
106 files changed
+15261 -301

No files matched your search

@@ -0,0 +1,177 @@
import { NextRequest, NextResponse } from "next/server";
import { secureLogger } from "@/lib/secure-logger";
import { requireAuth } from "@/lib/auth/middleware";
import { getListingById } from "@/lib/prisma/queries/listings";
import {
getPurchaseIntentionById,
updatePurchaseIntentionStatus,
} from "@/lib/prisma/queries/purchase-intentions";
import { createNotification } from "@/lib/prisma/queries/notifications";
/**
* GET /api/listings/[id]/intentions/[intentionId]
* Busca uma intenção específica
*/
export async function GET(
request: NextRequest,
{ params }: { params: { id: string; intentionId: string } }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
const intentionId = parseInt(params.intentionId, 10);
if (isNaN(intentionId)) {
return NextResponse.json(
{ success: false, error: "ID de intenção inválido" },
{ status: 400 }
);
}
const intention = await getPurchaseIntentionById(intentionId);
if (!intention) {
return NextResponse.json(
{ success: false, error: "Intenção não encontrada" },
{ status: 404 }
);
}
// Verificar permissões: vendedor ou comprador
if (intention.listing?.seller_id !== authResult.userId && intention.buyer_id !== authResult.userId) {
return NextResponse.json(
{ success: false, error: "Você não tem permissão para ver esta intenção" },
{ status: 403 }
);
}
return NextResponse.json(
{
success: true,
intention,
},
{ status: 200 }
);
} catch (error) {
secureLogger.error("❌ Erro ao buscar intenção", {
error: error instanceof Error ? error.message : "Erro desconhecido",
stack: error instanceof Error ? error.stack : undefined,
});
return NextResponse.json(
{ success: false, error: "Erro ao buscar intenção" },
{ status: 500 }
);
}
}
/**
* PATCH /api/listings/[id]/intentions/[intentionId]
* Atualiza o status de uma intenção (aceitar/rejeitar)
*/
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string; intentionId: string } }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
const intentionId = parseInt(params.intentionId, 10);
if (isNaN(intentionId)) {
return NextResponse.json(
{ success: false, error: "ID de intenção inválido" },
{ status: 400 }
);
}
const body = await request.json();
const { status, seller_response } = body;
if (!status || !["accepted", "rejected"].includes(status)) {
return NextResponse.json(
{ success: false, error: "Status inválido. Use 'accepted' ou 'rejected'" },
{ status: 400 }
);
}
// Buscar intenção
const intention = await getPurchaseIntentionById(intentionId);
if (!intention) {
return NextResponse.json(
{ success: false, error: "Intenção não encontrada" },
{ status: 404 }
);
}
// Verificar se o usuário é o vendedor
if (intention.listing?.seller_id !== authResult.userId) {
return NextResponse.json(
{ success: false, error: "Apenas o vendedor pode aceitar ou rejeitar intenções" },
{ status: 403 }
);
}
// Verificar se a intenção ainda está pendente
if (intention.status !== "pending") {
return NextResponse.json(
{ success: false, error: `Esta intenção já foi ${intention.status}` },
{ status: 400 }
);
}
// Verificar se o anúncio ainda está disponível (ativo ou em escrow)
const listing = await getListingById(intention.listing_id);
if (!listing || (listing.listing_status !== "active" && listing.listing_status !== "in_escrow")) {
return NextResponse.json(
{ success: false, error: "Anúncio não está mais disponível" },
{ status: 400 }
);
}
// Atualizar status da intenção (não altera status do listing)
const updatedIntention = await updatePurchaseIntentionStatus(
intentionId,
status,
seller_response
);
// Criar notificação para o comprador
await createNotification({
user_id: intention.buyer_id,
notification_type: status === "accepted" ? "purchase" : "system",
title: status === "accepted" ? "Intenção aceita!" : "Intenção rejeitada",
message: status === "accepted"
? `O vendedor aceitou sua intenção de compra do item "${intention.listing?.item_name}"`
: `O vendedor rejeitou sua intenção de compra do item "${intention.listing?.item_name}"`,
related_id: intention.listing_id,
send_whatsapp: false,
});
secureLogger.info(`✅ Intenção ${status === "accepted" ? "aceita" : "rejeitada"}`, {
userId: authResult.userId,
intentionId,
listingId: intention.listing_id,
buyerId: intention.buyer_id,
});
return NextResponse.json(
{
success: true,
intention: updatedIntention,
message: `Intenção ${status === "accepted" ? "aceita" : "rejeitada"} com sucesso`,
},
{ status: 200 }
);
} catch (error) {
secureLogger.error("❌ Erro ao atualizar intenção", {
error: error instanceof Error ? error.message : "Erro desconhecido",
stack: error instanceof Error ? error.stack : undefined,
});
return NextResponse.json(
{ success: false, error: "Erro ao atualizar intenção" },
{ status: 500 }
);
}
}
+242
View File
@@ -0,0 +1,242 @@
import { NextRequest, NextResponse } from "next/server";
import { secureLogger } from "@/lib/secure-logger";
import { requireAuth } from "@/lib/auth/middleware";
import { getListingById } from "@/lib/prisma/queries/listings";
import {
createPurchaseIntention,
getPurchaseIntentionsByListing,
hasPendingIntention,
} from "@/lib/prisma/queries/purchase-intentions";
import { createNotification } from "@/lib/prisma/queries/notifications";
import { trackEvent } from "@/lib/prisma/queries/telemetry";
/**
* POST /api/listings/[id]/intentions
* Cria uma nova intenção de compra
*/
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
const auth = authResult;
const listingId = parseInt(params.id, 10);
if (isNaN(listingId)) {
return NextResponse.json(
{ success: false, error: "ID de anúncio inválido" },
{ status: 400 }
);
}
const body = await request.json();
const { payment_method, buyer_message } = body;
if (!payment_method || !["pix", "tibia_coins", "gold_coins", "trade"].includes(payment_method)) {
return NextResponse.json(
{ success: false, error: "Método de pagamento inválido" },
{ status: 400 }
);
}
// Buscar listing
const listing = await getListingById(listingId);
if (!listing) {
return NextResponse.json(
{ success: false, error: "Anúncio não encontrado" },
{ status: 404 }
);
}
// Validar se não é o vendedor
if (listing.seller_id === auth.userId) {
return NextResponse.json(
{ success: false, error: "Você não pode criar uma intenção de compra para seu próprio anúncio" },
{ status: 403 }
);
}
// Validar se está ativo
if (listing.listing_status !== "active") {
return NextResponse.json(
{ success: false, error: "Anúncio não está disponível para compra" },
{ status: 400 }
);
}
// Validar se é leilão (leilões não podem ter intenções, apenas lances)
if (listing.is_auction) {
return NextResponse.json(
{ success: false, error: "Este item está em leilão. Use a funcionalidade de lance." },
{ status: 400 }
);
}
// Validar método de pagamento disponível
const availableMethods: string[] = [];
if (listing.price_pix) availableMethods.push("pix");
if (listing.price_tibia_coins) availableMethods.push("tibia_coins");
if (listing.price_gold_coins) availableMethods.push("gold_coins");
if (listing.trade_item_id) availableMethods.push("trade");
if (!availableMethods.includes(payment_method)) {
return NextResponse.json(
{
success: false,
error: `Método de pagamento "${payment_method}" não está disponível para este anúncio`,
available_methods: availableMethods,
},
{ status: 400 }
);
}
// Verificar se já existe uma intenção pendente
const alreadyHasPending = await hasPendingIntention(listingId, auth.userId);
if (alreadyHasPending) {
return NextResponse.json(
{ success: false, error: "Você já possui uma intenção de compra pendente para este anúncio" },
{ status: 400 }
);
}
// Criar intenção de compra
const intention = await createPurchaseIntention({
listing_id: listingId,
buyer_id: auth.userId,
payment_method: payment_method as any,
buyer_message: buyer_message || undefined,
});
// Criar notificação para o vendedor
await createNotification({
user_id: listing.seller_id,
notification_type: "purchase",
title: "Nova intenção de compra!",
message: `Alguém manifestou interesse em comprar seu item "${listing.item_name}" por ${payment_method.toUpperCase()}`,
related_id: listingId,
send_whatsapp: false,
});
// Registrar telemetria
await trackEvent({
event_type: "purchase_intention_created",
ad_id: listingId,
user_id: auth.userId,
metadata: {
payment_method,
listing_price_pix: listing.price_pix ? Number(listing.price_pix) : null,
listing_price_tc: listing.price_tibia_coins || null,
listing_price_gp: listing.price_gold_coins ? Number(listing.price_gold_coins) : null,
},
});
secureLogger.info("✅ Intenção de compra criada com sucesso", {
userId: auth.userId,
listingId,
intentionId: intention.intention_id,
paymentMethod: payment_method,
});
return NextResponse.json(
{
success: true,
intention,
message: "Intenção de compra criada com sucesso. O vendedor será notificado.",
},
{ status: 201 }
);
} catch (error) {
secureLogger.error("❌ Erro ao criar intenção de compra", {
error: error instanceof Error ? error.message : "Erro desconhecido",
stack: error instanceof Error ? error.stack : undefined,
});
return NextResponse.json(
{ success: false, error: "Erro ao criar intenção de compra" },
{ status: 500 }
);
}
}
/**
* GET /api/listings/[id]/intentions
* Lista intenções de compra de um anúncio (apenas vendedor)
*/
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
const auth = authResult;
const listingId = parseInt(params.id, 10);
if (isNaN(listingId)) {
return NextResponse.json(
{ success: false, error: "ID de anúncio inválido" },
{ status: 400 }
);
}
// Buscar listing para verificar se o usuário é o vendedor
const listing = await getListingById(listingId);
if (!listing) {
return NextResponse.json(
{ success: false, error: "Anúncio não encontrado" },
{ status: 404 }
);
}
// Apenas o vendedor pode ver as intenções
if (listing.seller_id !== auth.userId) {
return NextResponse.json(
{ success: false, error: "Você não tem permissão para ver as intenções deste anúncio" },
{ status: 403 }
);
}
// Buscar parâmetros de filtro
const { searchParams } = new URL(request.url);
const status = searchParams.get("status") as any;
const limit = parseInt(searchParams.get("limit") || "50", 10);
// Buscar intenções
const intentions = await getPurchaseIntentionsByListing(listingId, {
status,
limit,
});
// Registrar telemetria
await trackEvent({
event_type: "purchase_intention_viewed",
ad_id: listingId,
user_id: auth.userId,
metadata: {
intentions_count: intentions.length,
},
});
return NextResponse.json(
{
success: true,
intentions,
count: intentions.length,
},
{ status: 200 }
);
} catch (error) {
secureLogger.error("❌ Erro ao buscar intenções de compra", {
error: error instanceof Error ? error.message : "Erro desconhecido",
stack: error instanceof Error ? error.stack : undefined,
});
return NextResponse.json(
{ success: false, error: "Erro ao buscar intenções de compra" },
{ status: 500 }
);
}
}