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

+133
View File
@@ -0,0 +1,133 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
export interface Listing {
listing_id: number;
seller_id: number;
item_id: number;
item_name: string;
quantity: number;
server_name: string;
price_pix: number | string | null;
price_tibia_coins: number | string | null;
price_gold_coins: number | string | null;
is_auction: boolean;
description: string | null;
listing_status: string;
created_at: string;
views_count: number;
favorites_count: number;
is_featured?: boolean;
item_id_unified?: number | null;
image_local_path?: string | null;
image_url?: string | null;
item_category?: string | null;
seller_character_name?: string | null;
}
interface ListingsResponse {
listings: Listing[];
pagination?: {
total: number;
page: number;
pageSize: number;
totalPages: number;
};
}
interface ListingsParams {
search?: string;
server_name?: string;
price_min?: string;
price_max?: string;
sortBy?: string;
sortOrder?: "ASC" | "DESC";
page?: number;
pageSize?: number;
}
/**
* Hook para buscar listings do marketplace
* Atualiza automaticamente a cada 10 segundos
*/
export function useListings(params: ListingsParams = {}) {
const queryParams = new URLSearchParams();
if (params.search) queryParams.set("search", params.search);
if (params.server_name) queryParams.set("server_name", params.server_name);
if (params.price_min) queryParams.set("price_min", params.price_min);
if (params.price_max) queryParams.set("price_max", params.price_max);
queryParams.set("sortBy", params.sortBy || "created_at");
queryParams.set("sortOrder", params.sortOrder || "DESC");
queryParams.set("page", String(params.page || 1));
queryParams.set("pageSize", String(params.pageSize || 20));
const { data, isLoading, error, refetch } = useQuery<ListingsResponse>({
queryKey: ["listings", params],
queryFn: async () => {
const res = await fetch(`/api/listings?${queryParams.toString()}`);
if (!res.ok) throw new Error("Erro ao buscar anúncios");
return res.json();
},
staleTime: 1000 * 10, // 10 segundos
refetchInterval: 1000 * 10, // Refetch a cada 10 segundos
refetchOnWindowFocus: true,
refetchOnReconnect: true,
});
return {
listings: data?.listings || [],
total: data?.pagination?.total || 0,
page: data?.pagination?.page || params.page || 1,
pageSize: data?.pagination?.pageSize || params.pageSize || 20,
totalPages: data?.pagination?.totalPages || 1,
isLoading,
error,
refetch,
};
}
/**
* Hook para buscar meus anúncios
* Atualiza automaticamente a cada 10 segundos
*/
export function useMyListings(status?: string) {
const queryParams = new URLSearchParams();
if (status) queryParams.set("status", status);
const { data, isLoading, error, refetch } = useQuery<ListingsResponse>({
queryKey: ["my-listings", status],
queryFn: async () => {
const res = await fetch(`/api/listings/my?${queryParams.toString()}`);
if (!res.ok) throw new Error("Erro ao buscar meus anúncios");
return res.json();
},
staleTime: 1000 * 10, // 10 segundos
refetchInterval: 1000 * 10, // Refetch a cada 10 segundos
refetchOnWindowFocus: true,
refetchOnReconnect: true,
enabled: true, // Sempre habilitado (autenticação é verificada no backend)
});
return {
listings: data?.listings || [],
isLoading,
error,
refetch,
};
}
/**
* Hook para invalidar cache de listings
*/
export function useInvalidateListings() {
const queryClient = useQueryClient();
const invalidate = () => {
queryClient.invalidateQueries({ queryKey: ["listings"] });
queryClient.invalidateQueries({ queryKey: ["my-listings"] });
};
return invalidate;
}
+58
View File
@@ -0,0 +1,58 @@
"use client";
import { useQuery } from "@tanstack/react-query";
export interface SessionUser {
userId: number;
username: string;
email: string;
}
interface SessionResponse {
success: boolean;
authenticated: boolean;
user?: SessionUser;
}
/**
* Hook para gerenciar sessão do usuário com React Query
* Atualiza automaticamente a cada 30 segundos
*/
export function useSession() {
const { data, isLoading, refetch } = useQuery<SessionResponse>({
queryKey: ["session"],
queryFn: async () => {
const response = await fetch("/api/auth/session", {
method: "GET",
credentials: "include",
});
return response.json();
},
staleTime: 1000 * 30, // 30 segundos
refetchInterval: 1000 * 30, // Refetch a cada 30 segundos
refetchOnWindowFocus: true,
refetchOnReconnect: true,
});
const logout = async () => {
try {
await fetch("/api/auth/session", {
method: "DELETE",
credentials: "include",
});
// Invalidar cache e recarregar
window.location.href = "/";
} catch (error) {
console.error("Erro ao fazer logout:", error);
}
};
return {
user: data?.user || null,
isLoading,
isAuthenticated: data?.authenticated || false,
logout,
refreshSession: refetch,
};
}
+2 -102
View File
@@ -1,105 +1,5 @@
"use client";
import { useState, useEffect, useCallback } from "react";
export interface SessionUser {
userId: number;
username: string;
email: string;
}
interface SessionState {
user: SessionUser | null;
isLoading: boolean;
isAuthenticated: boolean;
}
/**
* Hook para gerenciar sessão do usuário
*/
export function useSession() {
const [session, setSession] = useState<SessionState>({
user: null,
isLoading: true,
isAuthenticated: false,
});
/**
* Verifica a sessão atual
*/
const checkSession = useCallback(async () => {
try {
const response = await fetch("/api/auth/session", {
method: "GET",
credentials: "include",
});
const data = await response.json();
if (data.success && data.authenticated) {
setSession({
user: data.user,
isLoading: false,
isAuthenticated: true,
});
} else {
setSession({
user: null,
isLoading: false,
isAuthenticated: false,
});
}
} catch (error) {
console.error("Erro ao verificar sessão:", error);
setSession({
user: null,
isLoading: false,
isAuthenticated: false,
});
}
}, []);
/**
* Encerra a sessão
*/
const logout = useCallback(async () => {
try {
await fetch("/api/auth/session", {
method: "DELETE",
credentials: "include",
});
setSession({
user: null,
isLoading: false,
isAuthenticated: false,
});
// Recarregar página para limpar estado
window.location.href = "/";
} catch (error) {
console.error("Erro ao fazer logout:", error);
}
}, []);
/**
* Atualiza a sessão após login
*/
const refreshSession = useCallback(async () => {
await checkSession();
}, [checkSession]);
// Verificar sessão ao montar componente
useEffect(() => {
checkSession();
}, [checkSession]);
return {
user: session.user,
isLoading: session.isLoading,
isAuthenticated: session.isAuthenticated,
logout,
refreshSession,
};
}
// Re-exportar o hook que usa React Query
export { useSession, type SessionUser } from "./use-session-query";
+100
View File
@@ -0,0 +1,100 @@
/**
* Hooks para tracking de telemetria
*/
import { useEffect, useRef, useCallback } from "react";
import { getTelemetry } from "../telemetry/MarketTelemetry";
import { useSession } from "./use-session";
/**
* Hook para tracking automático de visualização de anúncio
*/
export function useAdTracking(adId: number | null) {
const startTimeRef = useRef<number | null>(null);
const { user } = useSession();
useEffect(() => {
if (!adId) return;
const telemetry = getTelemetry(user?.userId);
startTimeRef.current = Date.now();
telemetry.trackAdView(adId);
return () => {
if (startTimeRef.current && adId) {
telemetry.trackAdExit(adId);
}
};
}, [adId, user?.userId]);
}
/**
* Hook para tracking global do marketplace
*/
export function useMarketTelemetry() {
const { user } = useSession();
const telemetryRef = useRef(getTelemetry(user?.userId));
// Atualizar user_id quando mudar
useEffect(() => {
if (user?.userId) {
telemetryRef.current.setUserId(user.userId);
}
}, [user?.userId]);
// Cleanup ao desmontar
useEffect(() => {
return () => {
telemetryRef.current.stop();
};
}, []);
const trackSearch = useCallback((query: string, filters?: Record<string, any>) => {
telemetryRef.current.trackSearch(query, filters);
}, []);
const trackFilter = useCallback((filters: Record<string, any>) => {
telemetryRef.current.trackFilterApplied(filters);
}, []);
const trackFavorite = useCallback((adId: number, action: "favorite" | "unfavorite") => {
telemetryRef.current.trackFavorite(adId, action);
}, []);
const trackPurchase = useCallback((adId: number, price: number) => {
telemetryRef.current.trackPurchase(adId, price);
}, []);
const trackInquiry = useCallback((adId: number) => {
telemetryRef.current.trackInquiry(adId);
}, []);
const trackPageView = useCallback((page: string) => {
telemetryRef.current.trackPageView(page);
}, []);
const trackAdClick = useCallback((adId: number, position?: number) => {
telemetryRef.current.trackAdClick(adId, position);
}, []);
const trackCustomEvent = useCallback((
eventType: string,
adId?: number,
metadata?: Record<string, any>
) => {
telemetryRef.current.trackCustomEvent(eventType, adId, metadata);
}, []);
return {
trackSearch,
trackFilter,
trackFavorite,
trackPurchase,
trackInquiry,
trackPageView,
trackAdClick,
trackCustomEvent,
telemetry: telemetryRef.current,
};
}