diff --git a/app/[locale]/market/page.tsx b/app/[locale]/market/page.tsx
index a1beea1..62cf55a 100644
--- a/app/[locale]/market/page.tsx
+++ b/app/[locale]/market/page.tsx
@@ -63,13 +63,33 @@ export default async function MarketPage({
sortOrder: (typeof sp.sortOrder === "string" ? sp.sortOrder : "DESC") as "ASC" | "DESC",
});
+ // Converter Date para string para compatibilidade com o tipo do cliente
+ const serializedListings = listingsData.listings.map((listing) => ({
+ ...listing,
+ created_at: listing.created_at instanceof Date
+ ? listing.created_at.toISOString()
+ : listing.created_at,
+ updated_at: listing.updated_at instanceof Date
+ ? listing.updated_at.toISOString()
+ : listing.updated_at,
+ sold_at: listing.sold_at instanceof Date
+ ? listing.sold_at.toISOString()
+ : listing.sold_at,
+ auction_ends_at: listing.auction_ends_at instanceof Date
+ ? listing.auction_ends_at.toISOString()
+ : listing.auction_ends_at,
+ expires_at: listing.expires_at instanceof Date
+ ? listing.expires_at.toISOString()
+ : listing.expires_at,
+ }));
+
return (
= [];
- try {
- // Usar transação Prisma
- await prisma.$transaction(async (tx) => {
- // Criar usuário
- const user = await tx.user.create({
- data: {
- username: validatedData.username,
- email: validatedData.email,
- password_hash: passwordHash,
- first_name: validatedData.firstName,
- last_name: validatedData.lastName,
- phone: validatedData.phone || null,
- cpf: null, // CPF não é mais obrigatório
- },
- select: {
- user_id: true,
- username: true,
- email: true,
- },
- });
+ // Usar transação Prisma
+ const newUser = await prisma.$transaction(async (tx) => {
+ // Criar usuário
+ const user = await tx.user.create({
+ data: {
+ username: validatedData.username,
+ email: validatedData.email,
+ password_hash: passwordHash,
+ first_name: validatedData.firstName,
+ last_name: validatedData.lastName,
+ phone: validatedData.phone || null,
+ cpf: null, // CPF não é mais obrigatório
+ },
+ select: {
+ user_id: true,
+ username: true,
+ email: true,
+ },
+ });
- newUser = {
- userId: user.user_id,
- username: user.username,
- email: user.email,
- };
+ const userData = {
+ userId: user.user_id,
+ username: user.username,
+ email: user.email,
+ };
- // Criar personagens (primeiro é main por padrão)
- for (let i = 0; i < validatedData.servers.length; i++) {
- const serverData = validatedData.servers[i];
- const isMain = i === 0 || serverData.isMain === true;
+ // Criar personagens (primeiro é main por padrão)
+ for (let i = 0; i < validatedData.servers.length; i++) {
+ const serverData = validatedData.servers[i];
+ const isMain = i === 0 || serverData.isMain === true;
- // Se este personagem será main e não é o primeiro, desmarcar outros
- if (isMain && i > 0) {
- await tx.userCharacter.updateMany({
- where: { user_id: newUser.userId },
- data: { is_main: false },
- });
- }
-
- // Inserir personagem
- const character = await tx.userCharacter.create({
- data: {
- user_id: newUser.userId,
- character_name: serverData.characterName,
- server_name: serverData.serverName,
- is_main: isMain,
- },
- });
-
- createdCharacters.push({
- characterName: character.character_name,
- serverName: character.server_name,
+ // Se este personagem será main e não é o primeiro, desmarcar outros
+ if (isMain && i > 0) {
+ await tx.userCharacter.updateMany({
+ where: { user_id: userData.userId },
+ data: { is_main: false },
});
}
- // Incrementar total_users dos servidores (dentro da transação)
- await tx.server.updateMany({
- where: {
- server_name: { in: uniqueServerNames },
- },
+ // Inserir personagem
+ const character = await tx.userCharacter.create({
data: {
- total_users: { increment: 1 },
- last_activity_at: new Date(),
+ user_id: userData.userId,
+ character_name: serverData.characterName,
+ server_name: serverData.serverName,
+ is_main: isMain,
},
});
+
+ createdCharacters.push({
+ characterName: character.character_name,
+ serverName: character.server_name,
+ });
+ }
+
+ // Incrementar total_users dos servidores (dentro da transação)
+ await tx.server.updateMany({
+ where: {
+ server_name: { in: uniqueServerNames },
+ },
+ data: {
+ total_users: { increment: 1 },
+ last_activity_at: new Date(),
+ },
});
- } catch (error) {
- // Prisma já faz rollback automático em caso de erro
- throw error;
- }
+
+ return userData;
+ });
// Gerar tokens
const accessToken = generateAccessToken(newUser.userId, newUser.username);
diff --git a/app/api/listings/[id]/bid/route.ts b/app/api/listings/[id]/bid/route.ts
index 28ba3a2..20fa7c1 100644
--- a/app/api/listings/[id]/bid/route.ts
+++ b/app/api/listings/[id]/bid/route.ts
@@ -10,14 +10,15 @@ import { createNotification } from "@/lib/prisma/queries/notifications";
*/
export async function POST(
request: NextRequest,
- { params }: { params: { id: string } }
+ { params }: { params: Promise<{ id: string }> }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
const auth = authResult;
- const listingId = parseInt(params.id, 10);
+ const { id } = await params;
+ const listingId = parseInt(id, 10);
if (isNaN(listingId)) {
return NextResponse.json(
{ success: false, error: "ID de anúncio inválido" },
diff --git a/app/api/listings/[id]/bids/route.ts b/app/api/listings/[id]/bids/route.ts
index 5bf5e0b..b44f14f 100644
--- a/app/api/listings/[id]/bids/route.ts
+++ b/app/api/listings/[id]/bids/route.ts
@@ -11,13 +11,14 @@ import { trackEvent } from "@/lib/prisma/queries/telemetry";
*/
export async function GET(
request: NextRequest,
- { params }: { params: { id: string } }
+ { params }: { params: Promise<{ id: string }> }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
- const listingId = parseInt(params.id, 10);
+ const { id } = await params;
+ const listingId = parseInt(id, 10);
if (isNaN(listingId)) {
return NextResponse.json(
{ success: false, error: "ID de anúncio inválido" },
diff --git a/app/api/listings/[id]/intentions/[intentionId]/route.ts b/app/api/listings/[id]/intentions/[intentionId]/route.ts
index 9f8aed4..c77812b 100644
--- a/app/api/listings/[id]/intentions/[intentionId]/route.ts
+++ b/app/api/listings/[id]/intentions/[intentionId]/route.ts
@@ -14,13 +14,14 @@ import { createNotification } from "@/lib/prisma/queries/notifications";
*/
export async function GET(
request: NextRequest,
- { params }: { params: { id: string; intentionId: string } }
+ { params }: { params: Promise<{ id: string; intentionId: string }> }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
- const intentionId = parseInt(params.intentionId, 10);
+ const { intentionId: intentionIdParam } = await params;
+ const intentionId = parseInt(intentionIdParam, 10);
if (isNaN(intentionId)) {
return NextResponse.json(
{ success: false, error: "ID de intenção inválido" },
@@ -70,13 +71,14 @@ export async function GET(
*/
export async function PATCH(
request: NextRequest,
- { params }: { params: { id: string; intentionId: string } }
+ { params }: { params: Promise<{ id: string; intentionId: string }> }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
- const intentionId = parseInt(params.intentionId, 10);
+ const { intentionId: intentionIdParam } = await params;
+ const intentionId = parseInt(intentionIdParam, 10);
if (isNaN(intentionId)) {
return NextResponse.json(
{ success: false, error: "ID de intenção inválido" },
diff --git a/app/api/listings/[id]/intentions/route.ts b/app/api/listings/[id]/intentions/route.ts
index a529e23..b767ca4 100644
--- a/app/api/listings/[id]/intentions/route.ts
+++ b/app/api/listings/[id]/intentions/route.ts
@@ -16,14 +16,15 @@ import { trackEvent } from "@/lib/prisma/queries/telemetry";
*/
export async function POST(
request: NextRequest,
- { params }: { params: { id: string } }
+ { params }: { params: Promise<{ id: string }> }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
const auth = authResult;
- const listingId = parseInt(params.id, 10);
+ const { id } = await params;
+ const listingId = parseInt(id, 10);
if (isNaN(listingId)) {
return NextResponse.json(
{ success: false, error: "ID de anúncio inválido" },
@@ -166,14 +167,15 @@ export async function POST(
*/
export async function GET(
request: NextRequest,
- { params }: { params: { id: string } }
+ { params }: { params: Promise<{ id: string }> }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
const auth = authResult;
-
- const listingId = parseInt(params.id, 10);
+
+ const { id } = await params;
+ const listingId = parseInt(id, 10);
if (isNaN(listingId)) {
return NextResponse.json(
{ success: false, error: "ID de anúncio inválido" },
diff --git a/app/api/listings/[id]/purchase/route.ts b/app/api/listings/[id]/purchase/route.ts
index ce1d0ef..9eff3b8 100644
--- a/app/api/listings/[id]/purchase/route.ts
+++ b/app/api/listings/[id]/purchase/route.ts
@@ -15,14 +15,15 @@ import { trackEvent } from "@/lib/prisma/queries/telemetry";
*/
export async function POST(
request: NextRequest,
- { params }: { params: { id: string } }
+ { params }: { params: Promise<{ id: string }> }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
const auth = authResult;
- const listingId = parseInt(params.id, 10);
+ const { id } = await params;
+ const listingId = parseInt(id, 10);
if (isNaN(listingId)) {
return NextResponse.json(
{ success: false, error: "ID de anúncio inválido" },
diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts
index b60e807..0ebb2fc 100644
--- a/app/api/listings/[id]/route.ts
+++ b/app/api/listings/[id]/route.ts
@@ -142,8 +142,27 @@ export async function PATCH(
const validatedData = validationResult.data;
+ // Converter null para undefined para compatibilidade com UpdateListingData
+ const updateData: {
+ quantity?: number;
+ price_pix?: number;
+ price_tibia_coins?: number;
+ price_gold_coins?: number;
+ description?: string;
+ listing_status?: "active" | "sold" | "cancelled" | "in_escrow" | "expired";
+ current_bid_amount?: number;
+ current_bidder_id?: number;
+ } = {
+ ...(validatedData.quantity !== undefined && { quantity: validatedData.quantity }),
+ ...(validatedData.price_pix !== null && validatedData.price_pix !== undefined && { price_pix: validatedData.price_pix }),
+ ...(validatedData.price_tibia_coins !== null && validatedData.price_tibia_coins !== undefined && { price_tibia_coins: validatedData.price_tibia_coins }),
+ ...(validatedData.price_gold_coins !== null && validatedData.price_gold_coins !== undefined && { price_gold_coins: validatedData.price_gold_coins }),
+ ...(validatedData.description !== null && validatedData.description !== undefined && { description: validatedData.description }),
+ ...(validatedData.listing_status !== undefined && { listing_status: validatedData.listing_status }),
+ };
+
// Atualizar anúncio
- const updatedListing = await updateListing(listingId, validatedData);
+ const updatedListing = await updateListing(listingId, updateData);
if (!updatedListing) {
return NextResponse.json(
diff --git a/app/api/listings/[id]/seller/route.ts b/app/api/listings/[id]/seller/route.ts
index 481a01d..711d4c6 100644
--- a/app/api/listings/[id]/seller/route.ts
+++ b/app/api/listings/[id]/seller/route.ts
@@ -9,10 +9,11 @@ import { getMainCharacterByUserId } from "@/lib/prisma/queries/characters";
*/
export async function GET(
request: NextRequest,
- { params }: { params: { id: string } }
+ { params }: { params: Promise<{ id: string }> }
) {
try {
- const listingId = parseInt(params.id, 10);
+ const { id } = await params;
+ const listingId = parseInt(id, 10);
if (isNaN(listingId)) {
return NextResponse.json(
{ success: false, error: "ID de anúncio inválido" },
diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts
index cf13391..9cf4778 100644
--- a/app/api/listings/route.ts
+++ b/app/api/listings/route.ts
@@ -159,10 +159,14 @@ export async function POST(request: NextRequest) {
}
// Criar anúncio
+ // O schema garante que pelo menos item_id ou item_id_unified está presente
+ // Mas a interface CreateListingData exige item_id, então usamos item_id_unified como fallback
+ // O refine do schema garante que pelo menos um está presente, então o fallback nunca será usado
+ const itemId = validatedData.item_id ?? validatedData.item_id_unified!;
+
const listing = await createListing({
seller_id: auth.userId,
- item_id: validatedData.item_id,
- item_id_unified: validatedData.item_id_unified,
+ item_id: itemId,
item_name: validatedData.item_name,
quantity: validatedData.quantity,
server_name: validatedData.server_name,
diff --git a/app/api/notifications/[id]/read/route.ts b/app/api/notifications/[id]/read/route.ts
index 946591a..bcc6eb0 100644
--- a/app/api/notifications/[id]/read/route.ts
+++ b/app/api/notifications/[id]/read/route.ts
@@ -9,14 +9,15 @@ import { markAsRead } from "@/lib/prisma/queries/notifications";
*/
export async function POST(
request: NextRequest,
- { params }: { params: { id: string } }
+ { params }: { params: Promise<{ id: string }> }
) {
try {
const authResult = await requireAuth(request);
if (authResult instanceof NextResponse) return authResult;
const auth = authResult;
- const notificationId = parseInt(params.id, 10);
+ const { id } = await params;
+ const notificationId = parseInt(id, 10);
if (isNaN(notificationId)) {
return NextResponse.json(
{ success: false, error: "ID de notificação inválido" },
diff --git a/app/api/telemetry/route.ts b/app/api/telemetry/route.ts
index b6e3842..0a3ebc0 100644
--- a/app/api/telemetry/route.ts
+++ b/app/api/telemetry/route.ts
@@ -71,6 +71,7 @@ export async function POST(request: NextRequest) {
...event,
user_id: userId,
session_id: batchSessionId || event.session_id || undefined,
+ ad_id: event.ad_id ?? undefined, // Converter null para undefined
metadata: event.metadata || {},
}));
@@ -117,6 +118,7 @@ export async function POST(request: NextRequest) {
...eventData,
user_id: userId,
session_id: body.session_id || undefined,
+ ad_id: eventData.ad_id ?? undefined, // Converter null para undefined
metadata: eventData.metadata || {},
});
diff --git a/components/marketplace/AdCard.tsx b/components/marketplace/AdCard.tsx
index bf09121..e6bf984 100644
--- a/components/marketplace/AdCard.tsx
+++ b/components/marketplace/AdCard.tsx
@@ -8,11 +8,13 @@ import { Heart, Clock, Eye, Star, MapPin, DollarSign, Coins, RefreshCw, Shopping
import { useAdTracking, useMarketTelemetry } from "@/lib/hooks/use-telemetry";
import { useSession } from "@/lib/hooks/use-session";
import { useQueryClient } from "@tanstack/react-query";
+import { cn } from "@/lib/utils";
import Image from "next/image";
import { PurchaseModal } from "./PurchaseModal";
import { BidModal } from "./BidModal";
import { SellerInfo } from "./SellerInfo";
import { ListingDetailsModal } from "./ListingDetailsModal";
+import { AuctionCountdown } from "./AuctionCountdown";
// Função auxiliar para formatar tempo relativo
function formatTimeAgo(date: Date): string {
@@ -297,8 +299,22 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
const itemImageUrl = getItemImageUrl();
- // Componente para exibir badges de preços
- const PriceBadges = ({ prices, compact = false }: { prices: PriceInfo[]; compact?: boolean }) => {
+ // Função para obter URL da imagem do item de troca
+ const getTradeItemImageUrl = (): string | null => {
+ if (!listing.trade_item_id || !listing.trade_item_name) {
+ return null;
+ }
+
+ // Usar trade_item_id para buscar imagem
+ const params = new URLSearchParams({ item: listing.trade_item_id.toString() });
+ params.set("name", listing.trade_item_name);
+ return `/api/images?${params.toString()}`;
+ };
+
+ const tradeItemImageUrl = getTradeItemImageUrl();
+
+ // Componente para exibir badges de preços em quadrados
+ const PriceBadges = ({ prices, compact = false, tradeItemImageUrl, listing }: { prices: PriceInfo[]; compact?: boolean; tradeItemImageUrl?: string | null; listing?: Listing }) => {
if (prices.length === 0) {
return Preço não informado
;
}
@@ -306,13 +322,13 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
const getBadgeClassName = (type: PriceInfo["type"]) => {
switch (type) {
case "pix":
- return "bg-green-500 hover:bg-green-600 text-white";
+ return "bg-green-100 hover:bg-green-200 dark:bg-green-900/30 dark:hover:bg-green-900/50 text-green-700 dark:text-green-300 border-green-300 dark:border-green-700";
case "tc":
- return "bg-blue-500 hover:bg-blue-600 text-white";
+ return "bg-blue-100 hover:bg-blue-200 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700";
case "gp":
- return "bg-yellow-500 hover:bg-yellow-600 text-white";
+ return "bg-amber-100 hover:bg-amber-200 dark:bg-amber-900/30 dark:hover:bg-amber-900/50 text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-700";
case "trade":
- return "bg-purple-500 hover:bg-purple-600 text-white";
+ return "bg-purple-100 hover:bg-purple-200 dark:bg-purple-900/30 dark:hover:bg-purple-900/50 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-700";
default:
return "";
}
@@ -332,58 +348,76 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
}
};
- if (prices.length === 1 && !compact) {
- // Se houver apenas um preço, mostrar em destaque
- const price = prices[0];
- return (
-
-
{price.value}
-
- {getIcon(price.type)}
- {price.label}
-
-
- );
- }
+ // Calcular número de colunas (apenas preços)
+ const totalCols = prices.length;
+ // Ajustar tamanho mínimo para caber todos os itens
+ const colClass = compact
+ ? `flex-1 min-w-0`
+ : `flex-1 min-w-0`;
- // Múltiplos preços: mostrar badges
- if (compact) {
- // Layout compacto para modo list
- return (
-
- {prices.map((price, index) => (
-
- {getIcon(price.type)}
- {price.value}
-
- ))}
-
- );
- }
-
- // Layout para modo grid
- if (prices.length <= 3) {
- // Grid 2 colunas para 2-3 preços
- return (
-
- {prices.map((price, index) => (
-
- {getIcon(price.type)}
- {price.value}
-
- ))}
-
- );
- }
-
- // Flex wrap para 4+ preços
return (
-
+
+ {/* Quadrados de preços individuais */}
{prices.map((price, index) => (
-
- {getIcon(price.type)}
- {price.value}
-
+
+ {price.type === "trade" ? (
+ <>
+
+ {getIcon(price.type)}
+ Troca
+
+ {tradeItemImageUrl && listing && listing.trade_item_name && (
+
+
{
+ (e.target as HTMLImageElement).src = "/placeholder-item.png";
+ }}
+ />
+ {listing.trade_item_quantity && (
+
+ {listing.trade_item_quantity}x
+
+ )}
+
+ )}
+ >
+ ) : (
+ <>
+
+ {getIcon(price.type)}
+ {price.label}
+
+
+ {price.value.split(' ')[0]}
+
+ {price.value.includes(' ') && (
+
+ {price.value.split(' ').slice(1).join(' ')}
+
+ )}
+ >
+ )}
+
))}
);
@@ -392,24 +426,40 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
if (viewMode === "list") {
return (
{isSold && (
)}
-
- {/* Thumbnail */}
-
-
{
- (e.target as HTMLImageElement).src = "/placeholder-item.png";
- }}
- />
+ {/* Título */}
+
+
Item a venda
+
+
+ {/* Thumbnails */}
+
+ {/* Imagem do item principal */}
+
+ {
+ (e.target as HTMLImageElement).src = "/placeholder-item.png";
+ }}
+ />
+
{/* Conteúdo */}
@@ -417,7 +467,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
{listing.item_name}
-
+
{listing.seller_character_name && (
)}
-
+
{timeAgo}
@@ -447,7 +497,10 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
)}
{listing.is_auction && (
- Leilão
+
+
+ Leilão
+
)}
{listing.description && (
-
+
{listing.description}
)}
-
-
-
+
+
+ {/* Preços - mostrar lance atual como principal em leilões */}
+ {listing.is_auction && listing.current_bid_amount ? (
+
+
Lance Atual
+
+ R$ {typeof listing.current_bid_amount === 'number' ? listing.current_bid_amount.toFixed(2) : parseFloat(String(listing.current_bid_amount)).toFixed(2)}
+
+ {listing.initial_price_pix && (
+
+ Inicial: R$ {typeof listing.initial_price_pix === 'number' ? listing.initial_price_pix.toFixed(2) : parseFloat(String(listing.initial_price_pix)).toFixed(2)}
+
+ )}
+
+ ) : (
+
+ )}
{/* Informações de lance se for leilão */}
{listing.is_auction && (
-
+
{listing.current_bid_amount ? (
<>
-
-
Lance Atual:
-
- R$ {typeof listing.current_bid_amount === 'number' ? listing.current_bid_amount.toFixed(2) : parseFloat(String(listing.current_bid_amount)).toFixed(2)}
-
+
+ {listing.min_bid_increment && (
+
+ Incremento Mín
+
+ R$ {typeof listing.min_bid_increment === 'number' ? listing.min_bid_increment.toFixed(2) : parseFloat(String(listing.min_bid_increment)).toFixed(2)}
+
+
+ )}
+ {listing.auction_ends_at && (
+
+ )}
-
-
Leilão Ativo
-
+
+
- Lances
+ Leilão Ativo
>
) : (
-
- Sem lances ainda
+
+
+
Sem lances ainda
+ {listing.initial_price_pix && (
+
+ Lance inicial: R$ {typeof listing.initial_price_pix === 'number' ? listing.initial_price_pix.toFixed(2) : parseFloat(String(listing.initial_price_pix)).toFixed(2)}
+
+ )}
+
+ {listing.auction_ends_at && (
+
+ )}
+
+
+
+ Leilão Ativo
+
+
)}
)}
-
-
+
+
{listing.views_count}
@@ -519,6 +626,18 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {