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,589 @@
|
||||
/**
|
||||
* Queries de listings (anúncios) usando Prisma
|
||||
* Migrado de lib/db/queries/listings.ts
|
||||
*/
|
||||
|
||||
import { prisma } from "../app";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
// Re-exportar tipos para compatibilidade
|
||||
export type ListingStatus = "active" | "sold" | "cancelled" | "in_escrow" | "expired";
|
||||
|
||||
export interface Listing {
|
||||
listing_id: number;
|
||||
seller_id: number;
|
||||
item_id: number;
|
||||
item_name: string;
|
||||
quantity: number;
|
||||
server_name: string;
|
||||
price_pix: number | null;
|
||||
price_tibia_coins: number | null;
|
||||
price_gold_coins: number | null;
|
||||
is_auction: boolean;
|
||||
current_bid_amount: number | null;
|
||||
current_bidder_id: number | null;
|
||||
auction_ends_at: Date | null;
|
||||
description: string | null;
|
||||
listing_status: ListingStatus;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
sold_at: Date | null;
|
||||
views_count: number;
|
||||
is_featured?: boolean;
|
||||
favorites_count?: number;
|
||||
inquiries_count?: number;
|
||||
price_history?: any;
|
||||
expires_at?: Date | null;
|
||||
trade_item_id?: number | null;
|
||||
trade_item_name?: string | null;
|
||||
trade_item_quantity?: number | null;
|
||||
min_bid_increment?: number | null;
|
||||
initial_price_pix?: number | null;
|
||||
initial_price_tibia_coins?: number | null;
|
||||
initial_price_gold_coins?: number | null;
|
||||
seller_character_name?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateListingData {
|
||||
seller_id: number;
|
||||
item_id: number;
|
||||
item_name: string;
|
||||
quantity: number;
|
||||
server_name: string;
|
||||
price_pix?: number;
|
||||
price_tibia_coins?: number;
|
||||
price_gold_coins?: number;
|
||||
is_auction?: boolean;
|
||||
auction_ends_at?: Date;
|
||||
description?: string;
|
||||
trade_item_id?: number | null;
|
||||
trade_item_name?: string | null;
|
||||
trade_item_quantity?: number | null;
|
||||
min_bid_increment?: number;
|
||||
}
|
||||
|
||||
export interface UpdateListingData {
|
||||
quantity?: number;
|
||||
price_pix?: number;
|
||||
price_tibia_coins?: number;
|
||||
price_gold_coins?: number;
|
||||
description?: string;
|
||||
listing_status?: ListingStatus;
|
||||
current_bid_amount?: number;
|
||||
current_bidder_id?: number;
|
||||
}
|
||||
|
||||
export interface ListingFilters {
|
||||
status?: ListingStatus;
|
||||
server_name?: string;
|
||||
item_id?: number;
|
||||
seller_id?: number;
|
||||
is_auction?: boolean;
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: "created_at" | "price_pix" | "price_tibia_coins" | "price_gold_coins" | "views_count";
|
||||
sortOrder?: "ASC" | "DESC";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria um novo anúncio
|
||||
*/
|
||||
export async function createListing(
|
||||
data: CreateListingData
|
||||
): Promise<Listing> {
|
||||
// Calcular incremento mÃnimo se for leilão e não foi fornecido
|
||||
let minBidIncrement = data.min_bid_increment;
|
||||
if (data.is_auction && !minBidIncrement) {
|
||||
const initialPrice = data.price_pix || 0;
|
||||
minBidIncrement = Math.max(initialPrice * 0.05, 10.0);
|
||||
}
|
||||
|
||||
const listing = await prisma.listing.create({
|
||||
data: {
|
||||
seller_id: data.seller_id,
|
||||
item_id: data.item_id,
|
||||
item_name: data.item_name,
|
||||
quantity: data.quantity,
|
||||
server_name: data.server_name,
|
||||
price_pix: data.price_pix || null,
|
||||
price_tibia_coins: data.price_tibia_coins || null,
|
||||
price_gold_coins: data.price_gold_coins ? BigInt(data.price_gold_coins) : null,
|
||||
is_auction: data.is_auction || false,
|
||||
auction_ends_at: data.auction_ends_at || null,
|
||||
description: data.description || null,
|
||||
trade_item_id: data.trade_item_id || null,
|
||||
trade_item_name: data.trade_item_name || null,
|
||||
trade_item_quantity: data.trade_item_quantity || null,
|
||||
min_bid_increment: minBidIncrement || null,
|
||||
initial_price_pix: data.is_auction ? (data.price_pix || null) : null,
|
||||
initial_price_tibia_coins: data.is_auction ? (data.price_tibia_coins || null) : null,
|
||||
initial_price_gold_coins: data.is_auction && data.price_gold_coins ? BigInt(data.price_gold_coins) : null,
|
||||
},
|
||||
});
|
||||
|
||||
// Buscar character name separadamente
|
||||
const character = await prisma.userCharacter.findFirst({
|
||||
where: {
|
||||
user_id: data.seller_id,
|
||||
server_name: data.server_name,
|
||||
is_main: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Converter todos os campos Decimal e BigInt para números JavaScript
|
||||
return {
|
||||
...listing,
|
||||
price_pix: listing.price_pix ? Number(listing.price_pix) : null,
|
||||
current_bid_amount: listing.current_bid_amount ? Number(listing.current_bid_amount) : null,
|
||||
min_bid_increment: listing.min_bid_increment ? Number(listing.min_bid_increment) : null,
|
||||
initial_price_pix: listing.initial_price_pix ? Number(listing.initial_price_pix) : null,
|
||||
price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null,
|
||||
initial_price_gold_coins: listing.initial_price_gold_coins ? Number(listing.initial_price_gold_coins) : null,
|
||||
seller_character_name: character?.character_name || null,
|
||||
} as Listing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca anúncio por ID
|
||||
*/
|
||||
export async function getListingById(listingId: number): Promise<Listing | null> {
|
||||
const listing = await prisma.listing.findUnique({
|
||||
where: { listing_id: listingId },
|
||||
});
|
||||
|
||||
if (!listing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Buscar character name separadamente
|
||||
const character = await prisma.userCharacter.findFirst({
|
||||
where: {
|
||||
user_id: listing.seller_id,
|
||||
server_name: listing.server_name,
|
||||
is_main: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Converter todos os campos Decimal e BigInt para números JavaScript
|
||||
return {
|
||||
...listing,
|
||||
price_pix: listing.price_pix ? Number(listing.price_pix) : null,
|
||||
current_bid_amount: listing.current_bid_amount ? Number(listing.current_bid_amount) : null,
|
||||
min_bid_increment: listing.min_bid_increment ? Number(listing.min_bid_increment) : null,
|
||||
initial_price_pix: listing.initial_price_pix ? Number(listing.initial_price_pix) : null,
|
||||
price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null,
|
||||
initial_price_gold_coins: listing.initial_price_gold_coins ? Number(listing.initial_price_gold_coins) : null,
|
||||
seller_character_name: character?.character_name || null,
|
||||
} as Listing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista anúncios com filtros e paginação
|
||||
*/
|
||||
export async function getListings(
|
||||
filters: ListingFilters = {}
|
||||
): Promise<{ listings: Listing[]; total: number }> {
|
||||
const {
|
||||
status,
|
||||
server_name,
|
||||
item_id,
|
||||
seller_id,
|
||||
is_auction,
|
||||
search,
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
sortBy = "created_at",
|
||||
sortOrder = "DESC",
|
||||
} = filters;
|
||||
|
||||
const where: Prisma.ListingWhereInput = {};
|
||||
|
||||
if (status) {
|
||||
where.listing_status = status;
|
||||
}
|
||||
|
||||
if (server_name) {
|
||||
where.server_name = server_name;
|
||||
}
|
||||
|
||||
if (item_id) {
|
||||
where.item_id = item_id;
|
||||
}
|
||||
|
||||
if (seller_id) {
|
||||
where.seller_id = seller_id;
|
||||
}
|
||||
|
||||
if (is_auction !== undefined) {
|
||||
where.is_auction = is_auction;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ item_name: { contains: search, mode: "insensitive" } },
|
||||
{ description: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
// Validar sortBy
|
||||
const validSortColumns = ["created_at", "price_pix", "price_tibia_coins", "price_gold_coins", "views_count"];
|
||||
const sortColumn = validSortColumns.includes(sortBy) ? sortBy : "created_at";
|
||||
const orderBy: Prisma.ListingOrderByWithRelationInput = {
|
||||
[sortColumn]: sortOrder.toLowerCase() as "asc" | "desc",
|
||||
};
|
||||
|
||||
// Contar total
|
||||
const total = await prisma.listing.count({ where });
|
||||
|
||||
// Buscar listings com paginação
|
||||
const listings = await prisma.listing.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
// Buscar character names para cada listing (otimizado com uma query por seller_id + server_name)
|
||||
const sellerServerPairs = new Set(
|
||||
listings.map((l) => `${l.seller_id}:${l.server_name}`)
|
||||
);
|
||||
|
||||
const charactersMap = new Map<string, string>();
|
||||
for (const pair of sellerServerPairs) {
|
||||
const [sellerId, serverName] = pair.split(":");
|
||||
const character = await prisma.userCharacter.findFirst({
|
||||
where: {
|
||||
user_id: parseInt(sellerId, 10),
|
||||
server_name: serverName,
|
||||
is_main: true,
|
||||
},
|
||||
});
|
||||
if (character) {
|
||||
charactersMap.set(pair, character.character_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Converter todos os campos Decimal e BigInt para números JavaScript
|
||||
const listingsWithCharacterNames = listings.map((listing) => {
|
||||
const key = `${listing.seller_id}:${listing.server_name}`;
|
||||
return {
|
||||
...listing,
|
||||
price_pix: listing.price_pix ? Number(listing.price_pix) : null,
|
||||
current_bid_amount: listing.current_bid_amount ? Number(listing.current_bid_amount) : null,
|
||||
min_bid_increment: listing.min_bid_increment ? Number(listing.min_bid_increment) : null,
|
||||
initial_price_pix: listing.initial_price_pix ? Number(listing.initial_price_pix) : null,
|
||||
price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null,
|
||||
initial_price_gold_coins: listing.initial_price_gold_coins ? Number(listing.initial_price_gold_coins) : null,
|
||||
seller_character_name: charactersMap.get(key) || null,
|
||||
} as Listing;
|
||||
});
|
||||
|
||||
return {
|
||||
listings: listingsWithCharacterNames,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza anúncio
|
||||
*/
|
||||
export async function updateListing(
|
||||
listingId: number,
|
||||
data: UpdateListingData
|
||||
): Promise<Listing | null> {
|
||||
const updateData: Prisma.ListingUpdateInput = {};
|
||||
|
||||
if (data.quantity !== undefined) updateData.quantity = data.quantity;
|
||||
if (data.price_pix !== undefined) updateData.price_pix = data.price_pix;
|
||||
if (data.price_tibia_coins !== undefined) updateData.price_tibia_coins = data.price_tibia_coins;
|
||||
if (data.price_gold_coins !== undefined) updateData.price_gold_coins = BigInt(data.price_gold_coins);
|
||||
if (data.description !== undefined) updateData.description = data.description;
|
||||
if (data.listing_status !== undefined) updateData.listing_status = data.listing_status;
|
||||
if (data.current_bid_amount !== undefined) updateData.current_bid_amount = data.current_bid_amount;
|
||||
if (data.current_bidder_id !== undefined) {
|
||||
updateData.current_bidder_id = data.current_bidder_id;
|
||||
}
|
||||
|
||||
const listing = await prisma.listing.update({
|
||||
where: { listing_id: listingId },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
// Buscar character name
|
||||
const character = await prisma.userCharacter.findFirst({
|
||||
where: {
|
||||
user_id: listing.seller_id,
|
||||
server_name: listing.server_name,
|
||||
is_main: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Converter todos os campos Decimal e BigInt para números JavaScript
|
||||
return {
|
||||
...listing,
|
||||
price_pix: listing.price_pix ? Number(listing.price_pix) : null,
|
||||
current_bid_amount: listing.current_bid_amount ? Number(listing.current_bid_amount) : null,
|
||||
min_bid_increment: listing.min_bid_increment ? Number(listing.min_bid_increment) : null,
|
||||
initial_price_pix: listing.initial_price_pix ? Number(listing.initial_price_pix) : null,
|
||||
price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null,
|
||||
initial_price_gold_coins: listing.initial_price_gold_coins ? Number(listing.initial_price_gold_coins) : null,
|
||||
seller_character_name: character?.character_name || null,
|
||||
} as Listing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove anúncio
|
||||
*/
|
||||
export async function deleteListing(listingId: number): Promise<boolean> {
|
||||
try {
|
||||
await prisma.listing.delete({
|
||||
where: { listing_id: listingId },
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrementa contador de visualizações
|
||||
*/
|
||||
export async function incrementViews(listingId: number): Promise<void> {
|
||||
await prisma.listing.update({
|
||||
where: { listing_id: listingId },
|
||||
data: {
|
||||
views_count: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista anúncios de um vendedor
|
||||
*/
|
||||
export async function getListingsBySeller(
|
||||
sellerId: number,
|
||||
filters: { status?: ListingStatus; page?: number; pageSize?: number } = {}
|
||||
): Promise<{ listings: Listing[]; total: number }> {
|
||||
return getListings({
|
||||
seller_id: sellerId,
|
||||
status: filters.status,
|
||||
page: filters.page,
|
||||
pageSize: filters.pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista apenas anúncios ativos (incluindo vendidos nas últimas 1 hora)
|
||||
*/
|
||||
export async function getActiveListings(
|
||||
filters: Omit<ListingFilters, "status"> = {}
|
||||
): Promise<{ listings: Listing[]; total: number }> {
|
||||
const {
|
||||
server_name,
|
||||
item_id,
|
||||
seller_id,
|
||||
is_auction,
|
||||
search,
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
sortBy = "created_at",
|
||||
sortOrder = "DESC",
|
||||
} = filters;
|
||||
|
||||
const where: Prisma.ListingWhereInput = {
|
||||
OR: [
|
||||
// Anúncios ativos
|
||||
{ listing_status: "active" },
|
||||
// Anúncios vendidos nas últimas 1 hora
|
||||
{
|
||||
listing_status: "sold",
|
||||
sold_at: {
|
||||
gte: new Date(Date.now() - 60 * 60 * 1000), // 1 hora atrás
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (server_name) {
|
||||
where.server_name = server_name;
|
||||
}
|
||||
|
||||
if (item_id) {
|
||||
where.item_id = item_id;
|
||||
}
|
||||
|
||||
if (seller_id) {
|
||||
where.seller_id = seller_id;
|
||||
}
|
||||
|
||||
if (is_auction !== undefined) {
|
||||
where.is_auction = is_auction;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const searchCondition = {
|
||||
OR: [
|
||||
{ item_name: { contains: search, mode: "insensitive" } },
|
||||
{ description: { contains: search, mode: "insensitive" } },
|
||||
],
|
||||
};
|
||||
// Combinar com a condição OR existente
|
||||
where.AND = [
|
||||
{
|
||||
OR: [
|
||||
{ listing_status: "active" },
|
||||
{
|
||||
listing_status: "sold",
|
||||
sold_at: {
|
||||
gte: new Date(Date.now() - 60 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
searchCondition,
|
||||
];
|
||||
delete where.OR;
|
||||
}
|
||||
|
||||
// Validar sortBy
|
||||
const validSortColumns = ["created_at", "price_pix", "price_tibia_coins", "price_gold_coins", "views_count"];
|
||||
const sortColumn = validSortColumns.includes(sortBy) ? sortBy : "created_at";
|
||||
const orderBy: Prisma.ListingOrderByWithRelationInput = {
|
||||
[sortColumn]: sortOrder.toLowerCase() as "asc" | "desc",
|
||||
};
|
||||
|
||||
// Contar total
|
||||
const total = await prisma.listing.count({ where });
|
||||
|
||||
// Buscar listings com paginação
|
||||
const listings = await prisma.listing.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
// Buscar character names para cada listing
|
||||
const sellerServerPairs = new Set(
|
||||
listings.map((l) => `${l.seller_id}:${l.server_name}`)
|
||||
);
|
||||
|
||||
const charactersMap = new Map<string, string>();
|
||||
for (const pair of sellerServerPairs) {
|
||||
const [sellerId, serverName] = pair.split(":");
|
||||
const character = await prisma.userCharacter.findFirst({
|
||||
where: {
|
||||
user_id: parseInt(sellerId, 10),
|
||||
server_name: serverName,
|
||||
is_main: true,
|
||||
},
|
||||
});
|
||||
if (character) {
|
||||
charactersMap.set(pair, character.character_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Converter todos os campos Decimal e BigInt para números JavaScript
|
||||
const listingsWithCharacterNames = listings.map((listing) => {
|
||||
const key = `${listing.seller_id}:${listing.server_name}`;
|
||||
return {
|
||||
...listing,
|
||||
price_pix: listing.price_pix ? Number(listing.price_pix) : null,
|
||||
current_bid_amount: listing.current_bid_amount ? Number(listing.current_bid_amount) : null,
|
||||
min_bid_increment: listing.min_bid_increment ? Number(listing.min_bid_increment) : null,
|
||||
initial_price_pix: listing.initial_price_pix ? Number(listing.initial_price_pix) : null,
|
||||
price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null,
|
||||
initial_price_gold_coins: listing.initial_price_gold_coins ? Number(listing.initial_price_gold_coins) : null,
|
||||
seller_character_name: charactersMap.get(key) || null,
|
||||
} as Listing;
|
||||
});
|
||||
|
||||
return {
|
||||
listings: listingsWithCharacterNames,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Coloca um lance em um leilão
|
||||
*/
|
||||
export async function placeBid(
|
||||
listingId: number,
|
||||
bidderId: number,
|
||||
amount: number,
|
||||
paymentMethod: "pix" | "tc" | "gp"
|
||||
): Promise<{ success: boolean; listing?: Listing; error?: string; previousBidderId?: number }> {
|
||||
// Usar transação para garantir atomicidade
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
// Buscar listing atual
|
||||
const listing = await tx.listing.findUnique({
|
||||
where: { listing_id: listingId },
|
||||
});
|
||||
|
||||
if (!listing) {
|
||||
return { success: false, error: "Anúncio não encontrado" };
|
||||
}
|
||||
|
||||
if (!listing.is_auction) {
|
||||
return { success: false, error: "Este anúncio não é um leilão" };
|
||||
}
|
||||
|
||||
if (listing.listing_status !== "active") {
|
||||
return { success: false, error: "Leilão não está ativo" };
|
||||
}
|
||||
|
||||
if (listing.seller_id === bidderId) {
|
||||
return { success: false, error: "Você não pode dar lance no seu próprio anúncio" };
|
||||
}
|
||||
|
||||
// Validar valor mÃnimo
|
||||
const minIncrement = listing.min_bid_increment ? Number(listing.min_bid_increment) : 10;
|
||||
const currentBid = listing.current_bid_amount ? Number(listing.current_bid_amount) : 0;
|
||||
const minBid = currentBid + minIncrement;
|
||||
|
||||
if (amount < minBid) {
|
||||
return { success: false, error: `O lance deve ser de pelo menos ${minBid.toFixed(2)}` };
|
||||
}
|
||||
|
||||
const previousBidderId = listing.current_bidder_id;
|
||||
|
||||
// Atualizar listing
|
||||
const updatedListing = await tx.listing.update({
|
||||
where: { listing_id: listingId },
|
||||
data: {
|
||||
current_bid_amount: amount,
|
||||
current_bidder_id: bidderId,
|
||||
},
|
||||
});
|
||||
|
||||
// Buscar character name
|
||||
const character = await tx.userCharacter.findFirst({
|
||||
where: {
|
||||
user_id: updatedListing.seller_id,
|
||||
server_name: updatedListing.server_name,
|
||||
is_main: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Converter todos os campos Decimal e BigInt para números JavaScript
|
||||
return {
|
||||
success: true,
|
||||
listing: {
|
||||
...updatedListing,
|
||||
price_pix: updatedListing.price_pix ? Number(updatedListing.price_pix) : null,
|
||||
current_bid_amount: updatedListing.current_bid_amount ? Number(updatedListing.current_bid_amount) : null,
|
||||
min_bid_increment: updatedListing.min_bid_increment ? Number(updatedListing.min_bid_increment) : null,
|
||||
initial_price_pix: updatedListing.initial_price_pix ? Number(updatedListing.initial_price_pix) : null,
|
||||
price_gold_coins: updatedListing.price_gold_coins ? Number(updatedListing.price_gold_coins) : null,
|
||||
initial_price_gold_coins: updatedListing.initial_price_gold_coins ? Number(updatedListing.initial_price_gold_coins) : null,
|
||||
seller_character_name: character?.character_name || null,
|
||||
} as Listing,
|
||||
previousBidderId: previousBidderId || undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user