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,240 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
} from "@/lib/rate-limiter";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import {
|
||||
telemetryEventSchema,
|
||||
telemetryBatchSchema,
|
||||
} from "@/lib/validation-schemas";
|
||||
import { optionalAuth } from "@/lib/auth/middleware";
|
||||
import {
|
||||
trackEvent,
|
||||
trackEventsBatch,
|
||||
getAdAnalytics,
|
||||
getMarketplaceStats,
|
||||
} from "@/lib/prisma/queries/telemetry";
|
||||
|
||||
/**
|
||||
* POST /api/telemetry/event
|
||||
* Registra um único evento de telemetria
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Rate limiting
|
||||
const ip = getClientIP(request);
|
||||
const rateLimit = checkRateLimit(ip, "/api/telemetry/event");
|
||||
|
||||
if (!rateLimit.allowed) {
|
||||
return createRateLimitResponse(rateLimit.resetAt);
|
||||
}
|
||||
|
||||
// Autenticação opcional (usuários anônimos podem gerar eventos)
|
||||
const authResult = await optionalAuth(request);
|
||||
const userId = authResult instanceof NextResponse ? undefined : authResult?.userId;
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Verificar se é batch ou evento único
|
||||
const isBatch = body.events && Array.isArray(body.events) && body.events.length > 0;
|
||||
|
||||
if (isBatch) {
|
||||
// Batch de eventos
|
||||
// Garantir que todos os eventos tenham metadata
|
||||
const normalizedBody = {
|
||||
...body,
|
||||
events: body.events.map((event: any) => ({
|
||||
...event,
|
||||
metadata: event.metadata || {},
|
||||
})),
|
||||
};
|
||||
|
||||
const validationResult = telemetryBatchSchema.safeParse(normalizedBody);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Dados de entrada inválidos",
|
||||
details: validationResult.error.issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { events, session_id: batchSessionId } = validationResult.data;
|
||||
|
||||
// Adicionar user_id e session_id aos eventos
|
||||
const eventsWithAuth = events.map((event) => ({
|
||||
...event,
|
||||
user_id: userId,
|
||||
session_id: batchSessionId || event.session_id || undefined,
|
||||
metadata: event.metadata || {},
|
||||
}));
|
||||
|
||||
const trackedEvents = await trackEventsBatch(eventsWithAuth);
|
||||
|
||||
secureLogger.debug("📊 Batch de eventos registrado", {
|
||||
count: trackedEvents.length,
|
||||
userId,
|
||||
ip,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
events: trackedEvents,
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} else {
|
||||
// Evento único
|
||||
// Garantir que o evento tenha metadata
|
||||
const normalizedBody = {
|
||||
...body,
|
||||
metadata: body.metadata || {},
|
||||
};
|
||||
|
||||
const validationResult = telemetryEventSchema.safeParse(normalizedBody);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Dados de entrada inválidos",
|
||||
details: validationResult.error.issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const eventData = validationResult.data;
|
||||
|
||||
// Adicionar user_id e session_id
|
||||
const trackedEvent = await trackEvent({
|
||||
...eventData,
|
||||
user_id: userId,
|
||||
session_id: body.session_id || undefined,
|
||||
metadata: eventData.metadata || {},
|
||||
});
|
||||
|
||||
secureLogger.debug("📊 Evento registrado", {
|
||||
eventType: eventData.event_type,
|
||||
adId: eventData.ad_id,
|
||||
userId,
|
||||
ip,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
event: trackedEvent,
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao registrar evento de telemetria", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao registrar evento",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/telemetry/analytics/:adId
|
||||
* Busca analytics de um anúncio específico
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const adIdParam = searchParams.get("adId");
|
||||
const dashboard = searchParams.get("dashboard");
|
||||
|
||||
// Dashboard agregado (admin)
|
||||
if (dashboard === "true") {
|
||||
const startDateParam = searchParams.get("startDate");
|
||||
const endDateParam = searchParams.get("endDate");
|
||||
|
||||
const startDate = startDateParam ? new Date(startDateParam) : undefined;
|
||||
const endDate = endDateParam ? new Date(endDateParam) : undefined;
|
||||
|
||||
const stats = await getMarketplaceStats(startDate, endDate);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
stats,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
|
||||
// Analytics de um anúncio específico
|
||||
if (adIdParam) {
|
||||
const adId = parseInt(adIdParam, 10);
|
||||
|
||||
if (isNaN(adId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID do anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const analytics = await getAdAnalytics(adId);
|
||||
|
||||
if (!analytics) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Analytics não encontrado para este anúncio",
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
analytics,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Parâmetro 'adId' ou 'dashboard' é obrigatório",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar analytics", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao buscar analytics",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user