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,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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user