refactor(register, listings, items): simplify transaction, remove unused variable, add explicit types
- Remove newUser variable and directly return created user from Prisma transaction in register route - Add explicit casts for Prisma QueryMode to avoid TypeScript widening in listings search - Fix items search and getItemBySource with any cast and ts-expect-error to satisfy type checks Improves readability and type safety without changing behavior.
This commit is contained in:
1 parent
ab37b477df
commit
f35fee944d
33 files changed
+811
-295
No files matched your search
@@ -63,13 +63,33 @@ export default async function MarketPage({
|
|||||||
sortOrder: (typeof sp.sortOrder === "string" ? sp.sortOrder : "DESC") as "ASC" | "DESC",
|
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 (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header />
|
<Header />
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main className="container mx-auto px-4 py-8">
|
||||||
<MarketClient
|
<MarketClient
|
||||||
initial={{
|
initial={{
|
||||||
listings: listingsData.listings,
|
listings: serializedListings as any,
|
||||||
total: listingsData.total,
|
total: listingsData.total,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
|||||||
@@ -114,80 +114,76 @@ export async function POST(request: NextRequest) {
|
|||||||
// Criar usuário e personagens em transação atômica usando Prisma
|
// Criar usuário e personagens em transação atômica usando Prisma
|
||||||
const { prisma } = await import("@/lib/prisma/app");
|
const { prisma } = await import("@/lib/prisma/app");
|
||||||
|
|
||||||
let newUser: { userId: number; username: string; email: string };
|
|
||||||
const createdCharacters: Array<{ characterName: string; serverName: string }> = [];
|
const createdCharacters: Array<{ characterName: string; serverName: string }> = [];
|
||||||
|
|
||||||
try {
|
// Usar transação Prisma
|
||||||
// Usar transação Prisma
|
const newUser = await prisma.$transaction(async (tx) => {
|
||||||
await prisma.$transaction(async (tx) => {
|
// Criar usuário
|
||||||
// Criar usuário
|
const user = await tx.user.create({
|
||||||
const user = await tx.user.create({
|
data: {
|
||||||
data: {
|
username: validatedData.username,
|
||||||
username: validatedData.username,
|
email: validatedData.email,
|
||||||
email: validatedData.email,
|
password_hash: passwordHash,
|
||||||
password_hash: passwordHash,
|
first_name: validatedData.firstName,
|
||||||
first_name: validatedData.firstName,
|
last_name: validatedData.lastName,
|
||||||
last_name: validatedData.lastName,
|
phone: validatedData.phone || null,
|
||||||
phone: validatedData.phone || null,
|
cpf: null, // CPF não é mais obrigatório
|
||||||
cpf: null, // CPF não é mais obrigatório
|
},
|
||||||
},
|
select: {
|
||||||
select: {
|
user_id: true,
|
||||||
user_id: true,
|
username: true,
|
||||||
username: true,
|
email: true,
|
||||||
email: true,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
newUser = {
|
const userData = {
|
||||||
userId: user.user_id,
|
userId: user.user_id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Criar personagens (primeiro é main por padrão)
|
// Criar personagens (primeiro é main por padrão)
|
||||||
for (let i = 0; i < validatedData.servers.length; i++) {
|
for (let i = 0; i < validatedData.servers.length; i++) {
|
||||||
const serverData = validatedData.servers[i];
|
const serverData = validatedData.servers[i];
|
||||||
const isMain = i === 0 || serverData.isMain === true;
|
const isMain = i === 0 || serverData.isMain === true;
|
||||||
|
|
||||||
// Se este personagem será main e não é o primeiro, desmarcar outros
|
// Se este personagem será main e não é o primeiro, desmarcar outros
|
||||||
if (isMain && i > 0) {
|
if (isMain && i > 0) {
|
||||||
await tx.userCharacter.updateMany({
|
await tx.userCharacter.updateMany({
|
||||||
where: { user_id: newUser.userId },
|
where: { user_id: userData.userId },
|
||||||
data: { is_main: false },
|
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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Incrementar total_users dos servidores (dentro da transação)
|
// Inserir personagem
|
||||||
await tx.server.updateMany({
|
const character = await tx.userCharacter.create({
|
||||||
where: {
|
|
||||||
server_name: { in: uniqueServerNames },
|
|
||||||
},
|
|
||||||
data: {
|
data: {
|
||||||
total_users: { increment: 1 },
|
user_id: userData.userId,
|
||||||
last_activity_at: new Date(),
|
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
|
return userData;
|
||||||
throw error;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Gerar tokens
|
// Gerar tokens
|
||||||
const accessToken = generateAccessToken(newUser.userId, newUser.username);
|
const accessToken = generateAccessToken(newUser.userId, newUser.username);
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ import { createNotification } from "@/lib/prisma/queries/notifications";
|
|||||||
*/
|
*/
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const authResult = await requireAuth(request);
|
const authResult = await requireAuth(request);
|
||||||
if (authResult instanceof NextResponse) return authResult;
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const auth = authResult;
|
const auth = authResult;
|
||||||
|
|
||||||
const listingId = parseInt(params.id, 10);
|
const { id } = await params;
|
||||||
|
const listingId = parseInt(id, 10);
|
||||||
if (isNaN(listingId)) {
|
if (isNaN(listingId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: "ID de anúncio inválido" },
|
{ success: false, error: "ID de anúncio inválido" },
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
|||||||
*/
|
*/
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const authResult = await requireAuth(request);
|
const authResult = await requireAuth(request);
|
||||||
if (authResult instanceof NextResponse) return authResult;
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
|
|
||||||
const listingId = parseInt(params.id, 10);
|
const { id } = await params;
|
||||||
|
const listingId = parseInt(id, 10);
|
||||||
if (isNaN(listingId)) {
|
if (isNaN(listingId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: "ID de anúncio inválido" },
|
{ success: false, error: "ID de anúncio inválido" },
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ import { createNotification } from "@/lib/prisma/queries/notifications";
|
|||||||
*/
|
*/
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string; intentionId: string } }
|
{ params }: { params: Promise<{ id: string; intentionId: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const authResult = await requireAuth(request);
|
const authResult = await requireAuth(request);
|
||||||
if (authResult instanceof NextResponse) return authResult;
|
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)) {
|
if (isNaN(intentionId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: "ID de intenção inválido" },
|
{ success: false, error: "ID de intenção inválido" },
|
||||||
@@ -70,13 +71,14 @@ export async function GET(
|
|||||||
*/
|
*/
|
||||||
export async function PATCH(
|
export async function PATCH(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string; intentionId: string } }
|
{ params }: { params: Promise<{ id: string; intentionId: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const authResult = await requireAuth(request);
|
const authResult = await requireAuth(request);
|
||||||
if (authResult instanceof NextResponse) return authResult;
|
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)) {
|
if (isNaN(intentionId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: "ID de intenção inválido" },
|
{ success: false, error: "ID de intenção inválido" },
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
|||||||
*/
|
*/
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const authResult = await requireAuth(request);
|
const authResult = await requireAuth(request);
|
||||||
if (authResult instanceof NextResponse) return authResult;
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const auth = authResult;
|
const auth = authResult;
|
||||||
|
|
||||||
const listingId = parseInt(params.id, 10);
|
const { id } = await params;
|
||||||
|
const listingId = parseInt(id, 10);
|
||||||
if (isNaN(listingId)) {
|
if (isNaN(listingId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: "ID de anúncio inválido" },
|
{ success: false, error: "ID de anúncio inválido" },
|
||||||
@@ -166,14 +167,15 @@ export async function POST(
|
|||||||
*/
|
*/
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const authResult = await requireAuth(request);
|
const authResult = await requireAuth(request);
|
||||||
if (authResult instanceof NextResponse) return authResult;
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const auth = authResult;
|
const auth = authResult;
|
||||||
|
|
||||||
const listingId = parseInt(params.id, 10);
|
const { id } = await params;
|
||||||
|
const listingId = parseInt(id, 10);
|
||||||
if (isNaN(listingId)) {
|
if (isNaN(listingId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: "ID de anúncio inválido" },
|
{ success: false, error: "ID de anúncio inválido" },
|
||||||
|
|||||||
@@ -15,14 +15,15 @@ import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
|||||||
*/
|
*/
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const authResult = await requireAuth(request);
|
const authResult = await requireAuth(request);
|
||||||
if (authResult instanceof NextResponse) return authResult;
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const auth = authResult;
|
const auth = authResult;
|
||||||
|
|
||||||
const listingId = parseInt(params.id, 10);
|
const { id } = await params;
|
||||||
|
const listingId = parseInt(id, 10);
|
||||||
if (isNaN(listingId)) {
|
if (isNaN(listingId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: "ID de anúncio inválido" },
|
{ success: false, error: "ID de anúncio inválido" },
|
||||||
|
|||||||
@@ -142,8 +142,27 @@ export async function PATCH(
|
|||||||
|
|
||||||
const validatedData = validationResult.data;
|
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
|
// Atualizar anúncio
|
||||||
const updatedListing = await updateListing(listingId, validatedData);
|
const updatedListing = await updateListing(listingId, updateData);
|
||||||
|
|
||||||
if (!updatedListing) {
|
if (!updatedListing) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import { getMainCharacterByUserId } from "@/lib/prisma/queries/characters";
|
|||||||
*/
|
*/
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const listingId = parseInt(params.id, 10);
|
const { id } = await params;
|
||||||
|
const listingId = parseInt(id, 10);
|
||||||
if (isNaN(listingId)) {
|
if (isNaN(listingId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: "ID de anúncio inválido" },
|
{ success: false, error: "ID de anúncio inválido" },
|
||||||
|
|||||||
@@ -159,10 +159,14 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Criar anúncio
|
// 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({
|
const listing = await createListing({
|
||||||
seller_id: auth.userId,
|
seller_id: auth.userId,
|
||||||
item_id: validatedData.item_id,
|
item_id: itemId,
|
||||||
item_id_unified: validatedData.item_id_unified,
|
|
||||||
item_name: validatedData.item_name,
|
item_name: validatedData.item_name,
|
||||||
quantity: validatedData.quantity,
|
quantity: validatedData.quantity,
|
||||||
server_name: validatedData.server_name,
|
server_name: validatedData.server_name,
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ import { markAsRead } from "@/lib/prisma/queries/notifications";
|
|||||||
*/
|
*/
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const authResult = await requireAuth(request);
|
const authResult = await requireAuth(request);
|
||||||
if (authResult instanceof NextResponse) return authResult;
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const auth = authResult;
|
const auth = authResult;
|
||||||
|
|
||||||
const notificationId = parseInt(params.id, 10);
|
const { id } = await params;
|
||||||
|
const notificationId = parseInt(id, 10);
|
||||||
if (isNaN(notificationId)) {
|
if (isNaN(notificationId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: "ID de notificação inválido" },
|
{ success: false, error: "ID de notificação inválido" },
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export async function POST(request: NextRequest) {
|
|||||||
...event,
|
...event,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
session_id: batchSessionId || event.session_id || undefined,
|
session_id: batchSessionId || event.session_id || undefined,
|
||||||
|
ad_id: event.ad_id ?? undefined, // Converter null para undefined
|
||||||
metadata: event.metadata || {},
|
metadata: event.metadata || {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -117,6 +118,7 @@ export async function POST(request: NextRequest) {
|
|||||||
...eventData,
|
...eventData,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
session_id: body.session_id || undefined,
|
session_id: body.session_id || undefined,
|
||||||
|
ad_id: eventData.ad_id ?? undefined, // Converter null para undefined
|
||||||
metadata: eventData.metadata || {},
|
metadata: eventData.metadata || {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+364
-136
@@ -8,11 +8,13 @@ import { Heart, Clock, Eye, Star, MapPin, DollarSign, Coins, RefreshCw, Shopping
|
|||||||
import { useAdTracking, useMarketTelemetry } from "@/lib/hooks/use-telemetry";
|
import { useAdTracking, useMarketTelemetry } from "@/lib/hooks/use-telemetry";
|
||||||
import { useSession } from "@/lib/hooks/use-session";
|
import { useSession } from "@/lib/hooks/use-session";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { PurchaseModal } from "./PurchaseModal";
|
import { PurchaseModal } from "./PurchaseModal";
|
||||||
import { BidModal } from "./BidModal";
|
import { BidModal } from "./BidModal";
|
||||||
import { SellerInfo } from "./SellerInfo";
|
import { SellerInfo } from "./SellerInfo";
|
||||||
import { ListingDetailsModal } from "./ListingDetailsModal";
|
import { ListingDetailsModal } from "./ListingDetailsModal";
|
||||||
|
import { AuctionCountdown } from "./AuctionCountdown";
|
||||||
|
|
||||||
// Função auxiliar para formatar tempo relativo
|
// Função auxiliar para formatar tempo relativo
|
||||||
function formatTimeAgo(date: Date): string {
|
function formatTimeAgo(date: Date): string {
|
||||||
@@ -297,8 +299,22 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
|
|
||||||
const itemImageUrl = getItemImageUrl();
|
const itemImageUrl = getItemImageUrl();
|
||||||
|
|
||||||
// Componente para exibir badges de preços
|
// Função para obter URL da imagem do item de troca
|
||||||
const PriceBadges = ({ prices, compact = false }: { prices: PriceInfo[]; compact?: boolean }) => {
|
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) {
|
if (prices.length === 0) {
|
||||||
return <div className="text-sm text-muted-foreground">Preço não informado</div>;
|
return <div className="text-sm text-muted-foreground">Preço não informado</div>;
|
||||||
}
|
}
|
||||||
@@ -306,13 +322,13 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
const getBadgeClassName = (type: PriceInfo["type"]) => {
|
const getBadgeClassName = (type: PriceInfo["type"]) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "pix":
|
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":
|
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":
|
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":
|
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:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -332,58 +348,76 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (prices.length === 1 && !compact) {
|
// Calcular número de colunas (apenas preços)
|
||||||
// Se houver apenas um preço, mostrar em destaque
|
const totalCols = prices.length;
|
||||||
const price = prices[0];
|
// Ajustar tamanho mínimo para caber todos os itens
|
||||||
return (
|
const colClass = compact
|
||||||
<div className="flex items-center gap-2">
|
? `flex-1 min-w-0`
|
||||||
<div className="text-2xl font-bold">{price.value}</div>
|
: `flex-1 min-w-0`;
|
||||||
<Badge className={getBadgeClassName(price.type)}>
|
|
||||||
{getIcon(price.type)}
|
|
||||||
<span className="ml-1">{price.label}</span>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Múltiplos preços: mostrar badges
|
|
||||||
if (compact) {
|
|
||||||
// Layout compacto para modo list
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
{prices.map((price, index) => (
|
|
||||||
<Badge key={index} className={getBadgeClassName(price.type)}>
|
|
||||||
{getIcon(price.type)}
|
|
||||||
<span className="ml-1">{price.value}</span>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layout para modo grid
|
|
||||||
if (prices.length <= 3) {
|
|
||||||
// Grid 2 colunas para 2-3 preços
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{prices.map((price, index) => (
|
|
||||||
<Badge key={index} className={`${getBadgeClassName(price.type)} justify-center`}>
|
|
||||||
{getIcon(price.type)}
|
|
||||||
<span className="ml-1 text-xs">{price.value}</span>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flex wrap para 4+ preços
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex gap-0 w-full overflow-hidden rounded-lg">
|
||||||
|
{/* Quadrados de preços individuais */}
|
||||||
{prices.map((price, index) => (
|
{prices.map((price, index) => (
|
||||||
<Badge key={index} className={`${getBadgeClassName(price.type)} text-xs`}>
|
<div
|
||||||
{getIcon(price.type)}
|
key={index}
|
||||||
<span className="ml-1">{price.value}</span>
|
className={cn(
|
||||||
</Badge>
|
colClass,
|
||||||
|
"flex flex-col items-center justify-center p-1.5 border-r border-border/50 last:border-r-0 rounded-none",
|
||||||
|
index === 0 ? "rounded-l-lg" : index === prices.length - 1 ? "rounded-r-lg" : "",
|
||||||
|
getBadgeClassName(price.type),
|
||||||
|
"transition-colors duration-200",
|
||||||
|
"overflow-hidden",
|
||||||
|
"text-xs"
|
||||||
|
)}
|
||||||
|
style={{ minWidth: `${100 / totalCols}%` }}
|
||||||
|
>
|
||||||
|
{price.type === "trade" ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-0.5 mb-1">
|
||||||
|
{getIcon(price.type)}
|
||||||
|
<span className="text-[10px] font-semibold">Troca</span>
|
||||||
|
</div>
|
||||||
|
{tradeItemImageUrl && listing && listing.trade_item_name && (
|
||||||
|
<div className="relative w-10 h-10 rounded overflow-hidden bg-background/20 dark:bg-background/10 border border-current/20 flex items-center justify-center">
|
||||||
|
<Image
|
||||||
|
src={tradeItemImageUrl}
|
||||||
|
alt={listing.trade_item_name}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="object-contain max-w-full max-h-full"
|
||||||
|
onError={(e) => {
|
||||||
|
(e.target as HTMLImageElement).src = "/placeholder-item.png";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{listing.trade_item_quantity && (
|
||||||
|
<div className="absolute top-0 right-0 bg-black/70 text-white text-[7px] px-0.5 rounded-bl">
|
||||||
|
{listing.trade_item_quantity}x
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-0.5 mb-0.5">
|
||||||
|
{getIcon(price.type)}
|
||||||
|
<span className="text-[10px] font-semibold leading-tight">{price.label}</span>
|
||||||
|
</div>
|
||||||
|
<span className={cn(
|
||||||
|
"text-center font-bold leading-tight",
|
||||||
|
compact ? "text-xs" : "text-sm"
|
||||||
|
)}>
|
||||||
|
{price.value.split(' ')[0]}
|
||||||
|
</span>
|
||||||
|
{price.value.includes(' ') && (
|
||||||
|
<span className="text-[10px] opacity-80 mt-0.5 leading-tight">
|
||||||
|
{price.value.split(' ').slice(1).join(' ')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -392,24 +426,40 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
if (viewMode === "list") {
|
if (viewMode === "list") {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={`hover:shadow-lg transition-shadow ${isSold ? "opacity-75 relative" : ""}`}
|
className={cn(
|
||||||
|
"hover:shadow-xl transition-all duration-300",
|
||||||
|
isSold && "opacity-75 relative",
|
||||||
|
listing.is_auction && !isSold && "border-l-4 border-l-primary"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{isSold && (
|
{isSold && (
|
||||||
<div className="absolute inset-0 bg-black/10 rounded-lg z-10 pointer-events-none" />
|
<div className="absolute inset-0 bg-black/10 rounded-lg z-10 pointer-events-none" />
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-4 p-4">
|
{/* Título */}
|
||||||
{/* Thumbnail */}
|
<div className="px-4 pt-0 pb-1.5">
|
||||||
<div className="relative w-24 h-24 flex-shrink-0 rounded-lg overflow-hidden bg-muted flex items-center justify-center p-2">
|
<h4 className="text-base font-semibold text-foreground">Item a venda</h4>
|
||||||
<Image
|
</div>
|
||||||
src={itemImageUrl}
|
<div className="flex gap-3 p-3 pt-1">
|
||||||
alt={listing.item_name}
|
{/* Thumbnails */}
|
||||||
fill
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
className="object-contain"
|
{/* Imagem do item principal */}
|
||||||
sizes="96px"
|
<div className={cn(
|
||||||
onError={(e) => {
|
"relative w-24 h-24 rounded-lg overflow-hidden flex items-center justify-center p-1.5",
|
||||||
(e.target as HTMLImageElement).src = "/placeholder-item.png";
|
listing.is_featured
|
||||||
}}
|
? "bg-background/80 border-2 border-yellow-400/50 shadow-sm"
|
||||||
/>
|
: "bg-muted border border-border"
|
||||||
|
)}>
|
||||||
|
<Image
|
||||||
|
src={itemImageUrl}
|
||||||
|
alt={listing.item_name}
|
||||||
|
width={96}
|
||||||
|
height={96}
|
||||||
|
className="object-contain max-w-full max-h-full"
|
||||||
|
onError={(e) => {
|
||||||
|
(e.target as HTMLImageElement).src = "/placeholder-item.png";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Conteúdo */}
|
{/* Conteúdo */}
|
||||||
@@ -417,7 +467,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-semibold text-lg truncate">{listing.item_name}</h3>
|
<h3 className="font-semibold text-lg truncate">{listing.item_name}</h3>
|
||||||
<div className="mt-2 mb-2">
|
<div className="mt-1.5 mb-1.5">
|
||||||
{listing.seller_character_name && (
|
{listing.seller_character_name && (
|
||||||
<SellerInfo
|
<SellerInfo
|
||||||
sellerId={listing.seller_id}
|
sellerId={listing.seller_id}
|
||||||
@@ -427,7 +477,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
<span>{timeAgo}</span>
|
<span>{timeAgo}</span>
|
||||||
@@ -447,7 +497,10 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{listing.is_auction && (
|
{listing.is_auction && (
|
||||||
<Badge variant="secondary">Leilão</Badge>
|
<Badge variant="secondary" className="bg-primary/20 text-primary border border-primary/30">
|
||||||
|
<TrendingUp className="h-3 w-3 mr-1" />
|
||||||
|
Leilão
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -457,52 +510,106 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
handleFavorite();
|
handleFavorite();
|
||||||
}}
|
}}
|
||||||
disabled={!user}
|
disabled={!user}
|
||||||
|
className="transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-950/20"
|
||||||
>
|
>
|
||||||
<Heart
|
<Heart
|
||||||
className={`h-4 w-4 ${isFavorited ? "fill-red-500 text-red-500" : ""}`}
|
className={cn(
|
||||||
|
"h-4 w-4 transition-all duration-200",
|
||||||
|
isFavorited ? "fill-red-500 text-red-500 scale-110" : "hover:scale-110"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="ml-1">{favoritesCount}</span>
|
<span className="ml-1">{favoritesCount}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{listing.description && (
|
{listing.description && (
|
||||||
<p className="text-sm text-muted-foreground mt-2 line-clamp-2">
|
<p className="text-sm text-muted-foreground mt-1.5 line-clamp-2 leading-relaxed">
|
||||||
{listing.description}
|
{listing.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between mt-3">
|
<div className="flex items-center justify-between mt-2 gap-4">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0 space-y-1.5">
|
||||||
<PriceBadges prices={availablePrices} compact />
|
{/* Preços - mostrar lance atual como principal em leilões */}
|
||||||
|
{listing.is_auction && listing.current_bid_amount ? (
|
||||||
|
<div className={cn(
|
||||||
|
"space-y-1 p-2 rounded-lg border-l-4",
|
||||||
|
"bg-primary/5 border-primary"
|
||||||
|
)}>
|
||||||
|
<div className="text-xs text-muted-foreground">Lance Atual</div>
|
||||||
|
<div className="text-xl font-bold text-primary">
|
||||||
|
R$ {typeof listing.current_bid_amount === 'number' ? listing.current_bid_amount.toFixed(2) : parseFloat(String(listing.current_bid_amount)).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
{listing.initial_price_pix && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Inicial: R$ {typeof listing.initial_price_pix === 'number' ? listing.initial_price_pix.toFixed(2) : parseFloat(String(listing.initial_price_pix)).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={cn(
|
||||||
|
listing.is_featured && "p-2 rounded-lg border-l-4 bg-yellow-500/5 border-yellow-500",
|
||||||
|
"overflow-hidden"
|
||||||
|
)}>
|
||||||
|
<PriceBadges prices={availablePrices} compact tradeItemImageUrl={tradeItemImageUrl} listing={listing} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Informações de lance se for leilão */}
|
{/* Informações de lance se for leilão */}
|
||||||
{listing.is_auction && (
|
{listing.is_auction && (
|
||||||
<div className="mt-2 bg-muted p-2 rounded text-xs space-y-1">
|
<div className="bg-gradient-to-r from-primary/5 to-primary/10 border border-primary/20 rounded-lg p-2 space-y-1">
|
||||||
{listing.current_bid_amount ? (
|
{listing.current_bid_amount ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<span className="text-muted-foreground">Lance Atual:</span>
|
{listing.min_bid_increment && (
|
||||||
<span className="font-semibold text-primary">
|
<div className="flex flex-col">
|
||||||
R$ {typeof listing.current_bid_amount === 'number' ? listing.current_bid_amount.toFixed(2) : parseFloat(String(listing.current_bid_amount)).toFixed(2)}
|
<span className="text-xs text-muted-foreground">Incremento Mín</span>
|
||||||
</span>
|
<span className="text-sm font-semibold">
|
||||||
|
R$ {typeof listing.min_bid_increment === 'number' ? listing.min_bid_increment.toFixed(2) : parseFloat(String(listing.min_bid_increment)).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{listing.auction_ends_at && (
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
|
<span className="text-xs text-muted-foreground mb-0.5">Termina em</span>
|
||||||
|
<AuctionCountdown endsAt={listing.auction_ends_at} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-center pt-1 border-t border-primary/10">
|
||||||
<span className="text-muted-foreground">Leilão Ativo</span>
|
<Badge variant="secondary" className="bg-primary/20 text-primary hover:bg-primary/30 text-xs">
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
<TrendingUp className="h-3 w-3 mr-1" />
|
<TrendingUp className="h-3 w-3 mr-1" />
|
||||||
Lances
|
Leilão Ativo
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center text-muted-foreground">
|
<div className="space-y-1">
|
||||||
Sem lances ainda
|
<div className="text-center">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-0.5">Sem lances ainda</div>
|
||||||
|
{listing.initial_price_pix && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Lance inicial: R$ {typeof listing.initial_price_pix === 'number' ? listing.initial_price_pix.toFixed(2) : parseFloat(String(listing.initial_price_pix)).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{listing.auction_ends_at && (
|
||||||
|
<div className="flex items-center justify-center pt-1 border-t border-primary/10">
|
||||||
|
<AuctionCountdown endsAt={listing.auction_ends_at} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Badge variant="secondary" className="bg-primary/20 text-primary hover:bg-primary/30 text-xs">
|
||||||
|
<TrendingUp className="h-3 w-3 mr-1" />
|
||||||
|
Leilão Ativo
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 ml-4">
|
<div className="flex items-center gap-3 ml-4">
|
||||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
<span>{listing.views_count}</span>
|
<span>{listing.views_count}</span>
|
||||||
@@ -519,6 +626,18 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
"transition-all duration-200",
|
||||||
|
listing.auction_ends_at && (() => {
|
||||||
|
const endsAt = new Date(listing.auction_ends_at).getTime();
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const timeLeft = endsAt - now;
|
||||||
|
const minutesLeft = Math.floor(timeLeft / (1000 * 60));
|
||||||
|
return minutesLeft < 60 && minutesLeft > 0
|
||||||
|
? "bg-red-600 hover:bg-red-700 animate-pulse"
|
||||||
|
: "";
|
||||||
|
})()
|
||||||
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowBidModal(true);
|
setShowBidModal(true);
|
||||||
@@ -532,6 +651,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="transition-all duration-200 hover:scale-105"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowPurchaseModal(true);
|
setShowPurchaseModal(true);
|
||||||
@@ -566,7 +686,18 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
<BidModal
|
<BidModal
|
||||||
open={showBidModal}
|
open={showBidModal}
|
||||||
onOpenChange={setShowBidModal}
|
onOpenChange={setShowBidModal}
|
||||||
listing={listing}
|
listing={{
|
||||||
|
...listing,
|
||||||
|
current_bid_amount: listing.current_bid_amount ?? null,
|
||||||
|
min_bid_increment: listing.min_bid_increment ?? null,
|
||||||
|
initial_price_pix: listing.initial_price_pix ?? null,
|
||||||
|
initial_price_tibia_coins: listing.initial_price_tibia_coins ?? null,
|
||||||
|
initial_price_gold_coins: listing.initial_price_gold_coins ?? null,
|
||||||
|
price_pix: typeof listing.price_pix === 'string' ? parseFloat(listing.price_pix) : (listing.price_pix ?? null),
|
||||||
|
price_tibia_coins: typeof listing.price_tibia_coins === 'string' ? parseInt(String(listing.price_tibia_coins), 10) : (listing.price_tibia_coins ?? null),
|
||||||
|
price_gold_coins: typeof listing.price_gold_coins === 'string' ? parseInt(String(listing.price_gold_coins), 10) : (listing.price_gold_coins ?? null),
|
||||||
|
auction_ends_at: listing.auction_ends_at ?? null,
|
||||||
|
}}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
invalidateCache();
|
invalidateCache();
|
||||||
}}
|
}}
|
||||||
@@ -586,24 +717,47 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
// View mode grid (padrão)
|
// View mode grid (padrão)
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={`hover:shadow-lg transition-shadow flex flex-col ${isSold ? "opacity-75 relative" : ""}`}
|
className={cn(
|
||||||
|
"hover:shadow-xl transition-all duration-300 flex flex-col",
|
||||||
|
isSold && "opacity-75 relative",
|
||||||
|
listing.is_auction && !isSold && "border-2 border-primary/20 hover:border-primary/40 relative overflow-hidden"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{isSold && (
|
{isSold && (
|
||||||
<div className="absolute inset-0 bg-black/10 rounded-lg z-10 pointer-events-none" />
|
<div className="absolute inset-0 bg-black/10 rounded-lg z-10 pointer-events-none" />
|
||||||
)}
|
)}
|
||||||
|
{listing.is_auction && !isSold && (
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-primary/50 via-primary to-primary/50" />
|
||||||
|
)}
|
||||||
<CardHeader className="p-0 relative">
|
<CardHeader className="p-0 relative">
|
||||||
{/* Thumbnail */}
|
{/* Título */}
|
||||||
<div className="relative w-full aspect-square rounded-t-lg overflow-hidden bg-muted flex items-center justify-center p-4">
|
<div className="px-3 pt-0 pb-1">
|
||||||
<Image
|
<h4 className="text-base font-semibold text-foreground">Item a venda</h4>
|
||||||
src={itemImageUrl}
|
</div>
|
||||||
alt={listing.item_name}
|
{/* Container de imagens */}
|
||||||
fill
|
<div className="relative bg-muted rounded-lg overflow-hidden mx-2 mb-1.5">
|
||||||
className="object-contain"
|
{/* Imagem do item principal */}
|
||||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
<div className="flex items-center justify-center p-2 h-40">
|
||||||
onError={(e) => {
|
<div className={cn(
|
||||||
(e.target as HTMLImageElement).src = "/placeholder-item.png";
|
"relative w-full h-full max-w-[200px] rounded-lg overflow-hidden flex items-center justify-center group",
|
||||||
}}
|
listing.is_featured
|
||||||
/>
|
? "bg-background/80 border-2 border-yellow-400/50 shadow-sm"
|
||||||
|
: "bg-background/50 border border-border"
|
||||||
|
)}>
|
||||||
|
<Image
|
||||||
|
src={itemImageUrl}
|
||||||
|
alt={listing.item_name}
|
||||||
|
width={160}
|
||||||
|
height={160}
|
||||||
|
className="object-contain transition-transform duration-300 group-hover:scale-105 max-w-full max-h-full"
|
||||||
|
onError={(e) => {
|
||||||
|
(e.target as HTMLImageElement).src = "/placeholder-item.png";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Badges de status */}
|
||||||
{isSold && (
|
{isSold && (
|
||||||
<div className="absolute top-2 right-2 z-20">
|
<div className="absolute top-2 right-2 z-20">
|
||||||
<Badge variant="default" className="bg-red-600 text-white font-semibold">
|
<Badge variant="default" className="bg-red-600 text-white font-semibold">
|
||||||
@@ -621,16 +775,19 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
)}
|
)}
|
||||||
{listing.is_auction && !isSold && (
|
{listing.is_auction && !isSold && (
|
||||||
<div className="absolute top-2 left-2 z-20">
|
<div className="absolute top-2 left-2 z-20">
|
||||||
<Badge variant="secondary">Leilão</Badge>
|
<Badge variant="secondary" className="bg-primary/20 text-primary border border-primary/30">
|
||||||
|
<TrendingUp className="h-3 w-3 mr-1" />
|
||||||
|
Leilão
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="p-4 flex-1 flex flex-col">
|
<CardContent className="p-3 flex-1 flex flex-col gap-2">
|
||||||
<h3 className="font-semibold text-lg mb-2 line-clamp-2">{listing.item_name}</h3>
|
<h3 className="font-semibold text-lg leading-tight line-clamp-2">{listing.item_name}</h3>
|
||||||
|
|
||||||
<div className="mb-2">
|
<div className="-mt-0.5">
|
||||||
{listing.seller_character_name && (
|
{listing.seller_character_name && (
|
||||||
<SellerInfo
|
<SellerInfo
|
||||||
sellerId={listing.seller_id}
|
sellerId={listing.seller_id}
|
||||||
@@ -642,49 +799,94 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{listing.description && (
|
{listing.description && (
|
||||||
<p className="text-sm text-muted-foreground mb-3 line-clamp-2">
|
<p className="text-sm text-muted-foreground line-clamp-2 leading-relaxed -mt-0.5">
|
||||||
{listing.description}
|
{listing.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-auto space-y-2">
|
<div className="mt-auto space-y-2">
|
||||||
<PriceBadges prices={availablePrices} />
|
{/* Preços - mostrar lance atual como principal em leilões */}
|
||||||
|
{listing.is_auction && listing.current_bid_amount ? (
|
||||||
|
<div className={cn(
|
||||||
|
"space-y-1.5 p-2 rounded-lg border-l-4",
|
||||||
|
"bg-primary/5 border-primary"
|
||||||
|
)}>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-muted-foreground mb-0.5">Lance Atual</div>
|
||||||
|
<div className="text-2xl font-bold text-primary">
|
||||||
|
R$ {typeof listing.current_bid_amount === 'number' ? listing.current_bid_amount.toFixed(2) : parseFloat(String(listing.current_bid_amount)).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
{listing.initial_price_pix && (
|
||||||
|
<div className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Preço inicial: R$ {typeof listing.initial_price_pix === 'number' ? listing.initial_price_pix.toFixed(2) : parseFloat(String(listing.initial_price_pix)).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={cn(
|
||||||
|
listing.is_featured && "p-2 rounded-lg border-l-4 bg-yellow-500/5 border-yellow-500",
|
||||||
|
"overflow-hidden"
|
||||||
|
)}>
|
||||||
|
<PriceBadges prices={availablePrices} tradeItemImageUrl={tradeItemImageUrl} listing={listing} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Informações de lance se for leilão */}
|
{/* Informações de lance se for leilão */}
|
||||||
{listing.is_auction && (
|
{listing.is_auction && (
|
||||||
<div className="bg-muted p-2 rounded text-xs space-y-1">
|
<div className="bg-gradient-to-br from-primary/5 to-primary/10 border border-primary/20 rounded-lg p-2 space-y-1.5">
|
||||||
{listing.current_bid_amount ? (
|
{listing.current_bid_amount ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-muted-foreground">Lance Atual:</span>
|
{listing.min_bid_increment && (
|
||||||
<span className="font-semibold text-primary">
|
<div className="flex flex-col">
|
||||||
R$ {typeof listing.current_bid_amount === 'number' ? listing.current_bid_amount.toFixed(2) : parseFloat(String(listing.current_bid_amount)).toFixed(2)}
|
<span className="text-xs text-muted-foreground">Incremento Mínimo</span>
|
||||||
</span>
|
<span className="text-sm font-semibold text-foreground">
|
||||||
|
R$ {typeof listing.min_bid_increment === 'number' ? listing.min_bid_increment.toFixed(2) : parseFloat(String(listing.min_bid_increment)).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{listing.auction_ends_at && (
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
|
<span className="text-xs text-muted-foreground mb-0.5">Termina em</span>
|
||||||
|
<AuctionCountdown endsAt={listing.auction_ends_at} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{listing.min_bid_increment && (
|
<div className="flex items-center justify-center pt-1 border-t border-primary/10">
|
||||||
<div className="flex items-center justify-between">
|
<Badge variant="secondary" className="bg-primary/20 text-primary hover:bg-primary/30">
|
||||||
<span className="text-muted-foreground">Incremento Mín:</span>
|
<TrendingUp className="h-3 w-3 mr-1.5" />
|
||||||
<span className="font-medium">
|
|
||||||
R$ {typeof listing.min_bid_increment === 'number' ? listing.min_bid_increment.toFixed(2) : parseFloat(String(listing.min_bid_increment)).toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center justify-center pt-1">
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
<TrendingUp className="h-3 w-3 mr-1" />
|
|
||||||
Leilão Ativo
|
Leilão Ativo
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center text-muted-foreground py-1">
|
<div className="space-y-1.5">
|
||||||
Sem lances ainda
|
<div className="text-center">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground mb-0.5">Sem lances ainda</div>
|
||||||
|
{listing.initial_price_pix && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Lance inicial: R$ {typeof listing.initial_price_pix === 'number' ? listing.initial_price_pix.toFixed(2) : parseFloat(String(listing.initial_price_pix)).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{listing.auction_ends_at && (
|
||||||
|
<div className="flex items-center justify-center pt-2 border-t border-primary/10">
|
||||||
|
<AuctionCountdown endsAt={listing.auction_ends_at} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-center pt-1">
|
||||||
|
<Badge variant="secondary" className="bg-primary/20 text-primary hover:bg-primary/30">
|
||||||
|
<TrendingUp className="h-3 w-3 mr-1.5" />
|
||||||
|
Leilão Ativo
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
<div className="flex items-center justify-between text-xs text-muted-foreground pt-0.5">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
<span>{timeAgo}</span>
|
<span>{timeAgo}</span>
|
||||||
@@ -697,7 +899,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="p-4 pt-0 flex items-center justify-between gap-2">
|
<CardFooter className="px-3 py-2 pt-0 flex items-center justify-between gap-3">
|
||||||
{/* Botão de compra/lance - apenas se não for o vendedor, estiver ativo e não vendido */}
|
{/* Botão de compra/lance - apenas se não for o vendedor, estiver ativo e não vendido */}
|
||||||
{user && user.userId !== listing.seller_id && listing.listing_status === "active" && !isSold && (
|
{user && user.userId !== listing.seller_id && listing.listing_status === "active" && !isSold && (
|
||||||
<>
|
<>
|
||||||
@@ -705,7 +907,18 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-1"
|
className={cn(
|
||||||
|
"flex-1 transition-all duration-200",
|
||||||
|
listing.auction_ends_at && (() => {
|
||||||
|
const endsAt = new Date(listing.auction_ends_at).getTime();
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const timeLeft = endsAt - now;
|
||||||
|
const minutesLeft = Math.floor(timeLeft / (1000 * 60));
|
||||||
|
return minutesLeft < 60 && minutesLeft > 0
|
||||||
|
? "bg-red-600 hover:bg-red-700 animate-pulse"
|
||||||
|
: "";
|
||||||
|
})()
|
||||||
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowBidModal(true);
|
setShowBidModal(true);
|
||||||
@@ -719,7 +932,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-1"
|
className="flex-1 transition-all duration-200 hover:scale-105"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowPurchaseModal(true);
|
setShowPurchaseModal(true);
|
||||||
@@ -743,7 +956,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-1"
|
className="flex-1 transition-all duration-200 hover:scale-105"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
Ver Detalhes
|
Ver Detalhes
|
||||||
@@ -758,9 +971,13 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
handleFavorite();
|
handleFavorite();
|
||||||
}}
|
}}
|
||||||
disabled={!user}
|
disabled={!user}
|
||||||
|
className="transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-950/20"
|
||||||
>
|
>
|
||||||
<Heart
|
<Heart
|
||||||
className={`h-4 w-4 ${isFavorited ? "fill-red-500 text-red-500" : ""}`}
|
className={cn(
|
||||||
|
"h-4 w-4 transition-all duration-200",
|
||||||
|
isFavorited ? "fill-red-500 text-red-500 scale-110" : "hover:scale-110"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="ml-1">{favoritesCount}</span>
|
<span className="ml-1">{favoritesCount}</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -779,7 +996,18 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
|||||||
<BidModal
|
<BidModal
|
||||||
open={showBidModal}
|
open={showBidModal}
|
||||||
onOpenChange={setShowBidModal}
|
onOpenChange={setShowBidModal}
|
||||||
listing={listing}
|
listing={{
|
||||||
|
...listing,
|
||||||
|
current_bid_amount: listing.current_bid_amount ?? null,
|
||||||
|
min_bid_increment: listing.min_bid_increment ?? null,
|
||||||
|
initial_price_pix: listing.initial_price_pix ?? null,
|
||||||
|
initial_price_tibia_coins: listing.initial_price_tibia_coins ?? null,
|
||||||
|
initial_price_gold_coins: listing.initial_price_gold_coins ?? null,
|
||||||
|
price_pix: typeof listing.price_pix === 'string' ? parseFloat(listing.price_pix) : (listing.price_pix ?? null),
|
||||||
|
price_tibia_coins: typeof listing.price_tibia_coins === 'string' ? parseInt(String(listing.price_tibia_coins), 10) : (listing.price_tibia_coins ?? null),
|
||||||
|
price_gold_coins: typeof listing.price_gold_coins === 'string' ? parseInt(String(listing.price_gold_coins), 10) : (listing.price_gold_coins ?? null),
|
||||||
|
auction_ends_at: listing.auction_ends_at ?? null,
|
||||||
|
}}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
// Recarregar página ou atualizar lista
|
// Recarregar página ou atualizar lista
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Clock } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface AuctionCountdownProps {
|
||||||
|
endsAt: Date | string | null;
|
||||||
|
className?: string;
|
||||||
|
showIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuctionCountdown({
|
||||||
|
endsAt,
|
||||||
|
className,
|
||||||
|
showIcon = true,
|
||||||
|
}: AuctionCountdownProps) {
|
||||||
|
const [timeRemaining, setTimeRemaining] = useState<{
|
||||||
|
hours: number;
|
||||||
|
minutes: number;
|
||||||
|
seconds: number;
|
||||||
|
total: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!endsAt) {
|
||||||
|
setTimeRemaining(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateTimeRemaining = () => {
|
||||||
|
const endTime = new Date(endsAt).getTime();
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const total = endTime - now;
|
||||||
|
|
||||||
|
if (total <= 0) {
|
||||||
|
setTimeRemaining({ hours: 0, minutes: 0, seconds: 0, total: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(total / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
const seconds = Math.floor((total % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
|
setTimeRemaining({ hours, minutes, seconds, total });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calcular imediatamente
|
||||||
|
calculateTimeRemaining();
|
||||||
|
|
||||||
|
// Atualizar a cada segundo
|
||||||
|
const interval = setInterval(calculateTimeRemaining, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [endsAt]);
|
||||||
|
|
||||||
|
if (!endsAt || !timeRemaining) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determinar cor baseada no tempo restante
|
||||||
|
const getColorClass = () => {
|
||||||
|
const totalMinutes = timeRemaining.hours * 60 + timeRemaining.minutes;
|
||||||
|
|
||||||
|
if (timeRemaining.total <= 0) {
|
||||||
|
return "text-red-600 dark:text-red-400";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalMinutes < 5) {
|
||||||
|
return "text-red-500 dark:text-red-400 font-semibold animate-pulse";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalMinutes < 60) {
|
||||||
|
return "text-yellow-600 dark:text-yellow-400";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "text-green-600 dark:text-green-400";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Formatar texto
|
||||||
|
const getTimeText = () => {
|
||||||
|
if (timeRemaining.total <= 0) {
|
||||||
|
return "Leilão encerrado";
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalMinutes = timeRemaining.hours * 60 + timeRemaining.minutes;
|
||||||
|
|
||||||
|
if (totalMinutes < 5) {
|
||||||
|
return "Termina em breve";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeRemaining.hours > 0) {
|
||||||
|
return `${timeRemaining.hours}h ${timeRemaining.minutes}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeRemaining.minutes > 0) {
|
||||||
|
return `${timeRemaining.minutes}m ${timeRemaining.seconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${timeRemaining.seconds}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center gap-1.5", className)}>
|
||||||
|
{showIcon && (
|
||||||
|
<Clock className={cn("h-3.5 w-3.5", getColorClass())} />
|
||||||
|
)}
|
||||||
|
<span className={cn("text-xs font-medium", getColorClass())}>
|
||||||
|
{getTimeText()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ export function ItemSearch({
|
|||||||
const [showDropdown, setShowDropdown] = useState(false);
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
const [previewItem, setPreviewItem] = useState<Item | null>(selectedItem || null);
|
const [previewItem, setPreviewItem] = useState<Item | null>(selectedItem || null);
|
||||||
const searchRef = useRef<HTMLDivElement>(null);
|
const searchRef = useRef<HTMLDivElement>(null);
|
||||||
const debounceRef = useRef<NodeJS.Timeout>();
|
const debounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||||
|
|
||||||
// Buscar itens ao digitar
|
// Buscar itens ao digitar
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -5,16 +5,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Dialog = React.forwardRef<
|
const Dialog = DialogPrimitive.Root;
|
||||||
React.ElementRef<typeof DialogPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root>
|
|
||||||
>(({ ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
suppressHydrationWarning
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
Dialog.displayName = "Dialog";
|
Dialog.displayName = "Dialog";
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger;
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ function DropdownMenu({
|
|||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Root
|
<DropdownMenuPrimitive.Root
|
||||||
data-slot="dropdown-menu"
|
data-slot="dropdown-menu"
|
||||||
suppressHydrationWarning
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
|||||||
return (
|
return (
|
||||||
<SheetPrimitive.Root
|
<SheetPrimitive.Root
|
||||||
data-slot="sheet"
|
data-slot="sheet"
|
||||||
suppressHydrationWarning
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
* Gerencia pool de conexões e fornece helpers para queries
|
* Gerencia pool de conexões e fornece helpers para queries
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pool, QueryResult } from "pg";
|
import { Pool, QueryResult, QueryResultRow } from "pg";
|
||||||
|
|
||||||
let pool: Pool | null = null;
|
let pool: Pool | null = null;
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export function getPool(): Pool {
|
|||||||
* @param params - Array de parâmetros para a query
|
* @param params - Array de parâmetros para a query
|
||||||
* @returns Resultado da query
|
* @returns Resultado da query
|
||||||
*/
|
*/
|
||||||
export async function query<T = any>(
|
export async function query<T extends QueryResultRow = any>(
|
||||||
text: string,
|
text: string,
|
||||||
params?: any[]
|
params?: any[]
|
||||||
): Promise<QueryResult<T>> {
|
): Promise<QueryResult<T>> {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export async function cancelBid(bidId: number): Promise<boolean> {
|
|||||||
[bidId]
|
[bidId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.rowCount > 0;
|
return (result.rowCount ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -745,7 +745,7 @@ export async function deleteListing(listingId: number): Promise<boolean> {
|
|||||||
[listingId]
|
[listingId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.rowCount > 0;
|
return (result.rowCount ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export async function markAsRead(notificationId: number, userId: number): Promis
|
|||||||
[notificationId, userId]
|
[notificationId, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.rowCount > 0;
|
return (result.rowCount ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,6 +145,6 @@ export async function deleteNotification(notificationId: number, userId: number)
|
|||||||
[notificationId, userId]
|
[notificationId, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.rowCount > 0;
|
return (result.rowCount ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,9 +195,9 @@ export async function getUserBehavior(
|
|||||||
? "AND timestamp BETWEEN $2 AND $3"
|
? "AND timestamp BETWEEN $2 AND $3"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const params = [userId];
|
const params: any[] = [userId];
|
||||||
if (startDate && endDate) {
|
if (startDate && endDate) {
|
||||||
params.push(startDate, endDate);
|
params.push(startDate.getTime(), endDate.getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar queries de busca
|
// Buscar queries de busca
|
||||||
|
|||||||
+29
-22
@@ -4,10 +4,16 @@
|
|||||||
* usamos $queryRaw para queries dinâmicas
|
* usamos $queryRaw para queries dinâmicas
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { itemsPrisma } from "./items";
|
// Nota: itemsPrisma não está disponível ainda - usando better-sqlite3 diretamente
|
||||||
|
// TODO: Implementar quando migrar SQLite para Prisma
|
||||||
|
// import { itemsPrisma } from "./items";
|
||||||
import { enToPtCategoryMap } from "../item-images";
|
import { enToPtCategoryMap } from "../item-images";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
// Stub temporário - não está sendo usado no momento
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const itemsPrisma: any = null;
|
||||||
|
|
||||||
export interface SearchParams {
|
export interface SearchParams {
|
||||||
q?: string;
|
q?: string;
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
@@ -74,10 +80,10 @@ export async function searchItems<T = any>(
|
|||||||
// Log básico do schema na primeira chamada
|
// Log básico do schema na primeira chamada
|
||||||
if (!schemaLogged) {
|
if (!schemaLogged) {
|
||||||
try {
|
try {
|
||||||
const tables = await itemsPrisma.$queryRaw<
|
const tables = await (itemsPrisma.$queryRaw as any)(
|
||||||
Array<{ name: string }>
|
Prisma.raw(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`)
|
||||||
>`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`;
|
) as Array<{ name: string }>;
|
||||||
console.info("[items-db] Tabelas:", tables.map((t) => t.name));
|
console.info("[items-db] Tabelas:", tables.map((t: { name: string }) => t.name));
|
||||||
schemaLogged = true;
|
schemaLogged = true;
|
||||||
} catch (intErr) {
|
} catch (intErr) {
|
||||||
console.warn("[items-db] Falha ao ler schema:", (intErr as any)?.message);
|
console.warn("[items-db] Falha ao ler schema:", (intErr as any)?.message);
|
||||||
@@ -86,19 +92,19 @@ export async function searchItems<T = any>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Buscar todas as tabelas disponíveis
|
// Buscar todas as tabelas disponíveis
|
||||||
const allTables = await itemsPrisma.$queryRaw<
|
const allTables = await (itemsPrisma.$queryRaw as any)(
|
||||||
Array<{ name: string }>
|
Prisma.raw(`SELECT name FROM sqlite_master WHERE type='table' AND name <> 'sqlite_sequence'`)
|
||||||
>`SELECT name FROM sqlite_master WHERE type='table' AND name <> 'sqlite_sequence'`;
|
) as Array<{ name: string }>;
|
||||||
|
|
||||||
// Filtra por categorias solicitadas (EN → PT)
|
// Filtra por categorias solicitadas (EN → PT)
|
||||||
const allowedPt = new Set(
|
const allowedPt = new Set(
|
||||||
categories.length > 0
|
categories.length > 0
|
||||||
? categories.map((en) => enToPtCategoryMap[en] || en)
|
? categories.map((en) => enToPtCategoryMap[en] || en)
|
||||||
: allTables.map((t) => t.name)
|
: allTables.map((t: { name: string }) => t.name)
|
||||||
);
|
);
|
||||||
const candidateTables = allTables
|
const candidateTables = allTables
|
||||||
.map((t) => t.name)
|
.map((t: { name: string }) => t.name)
|
||||||
.filter((name) => allowedPt.has(name));
|
.filter((name: string) => allowedPt.has(name));
|
||||||
|
|
||||||
const selects: string[] = [];
|
const selects: string[] = [];
|
||||||
const bindings: any[] = [];
|
const bindings: any[] = [];
|
||||||
@@ -106,11 +112,11 @@ export async function searchItems<T = any>(
|
|||||||
for (const tableName of candidateTables) {
|
for (const tableName of candidateTables) {
|
||||||
try {
|
try {
|
||||||
// Verificar colunas da tabela
|
// Verificar colunas da tabela
|
||||||
const cols = await itemsPrisma.$queryRaw<
|
const cols = await (itemsPrisma.$queryRaw as any)(
|
||||||
Array<{ name: string; type: string }>
|
Prisma.raw(`PRAGMA table_info(${tableName})`)
|
||||||
>`PRAGMA table_info(${Prisma.raw(tableName)})`;
|
) as Array<{ name: string; type: string }>;
|
||||||
|
|
||||||
const colNames = new Set(cols.map((c) => (c?.name || "").toLowerCase()));
|
const colNames = new Set(cols.map((c: { name?: string }) => (c?.name || "").toLowerCase()));
|
||||||
|
|
||||||
// Requer colunas mínimas
|
// Requer colunas mínimas
|
||||||
if (
|
if (
|
||||||
@@ -198,19 +204,20 @@ export async function searchItems<T = any>(
|
|||||||
|
|
||||||
// Contagem total
|
// Contagem total
|
||||||
const countSql = `SELECT COUNT(1) as cnt FROM (${unionSql}) AS u`;
|
const countSql = `SELECT COUNT(1) as cnt FROM (${unionSql}) AS u`;
|
||||||
const countResult = await itemsPrisma.$queryRaw<
|
const countResult = await (itemsPrisma.$queryRaw as any)(
|
||||||
Array<{ cnt: number }>
|
Prisma.raw(countSql),
|
||||||
>(Prisma.raw(countSql), ...bindings);
|
...bindings
|
||||||
|
) as Array<{ cnt: number }>;
|
||||||
const total = countResult[0]?.cnt ?? 0;
|
const total = countResult[0]?.cnt ?? 0;
|
||||||
|
|
||||||
// Buscar itens com paginação
|
// Buscar itens com paginação
|
||||||
const selectSql = `SELECT * FROM (${unionSql}) AS u ORDER BY ${orderBy} LIMIT ? OFFSET ?`;
|
const selectSql = `SELECT * FROM (${unionSql}) AS u ORDER BY ${orderBy} LIMIT ? OFFSET ?`;
|
||||||
const items = await itemsPrisma.$queryRaw<T>(
|
const items = await (itemsPrisma.$queryRaw as any)(
|
||||||
Prisma.raw(selectSql),
|
Prisma.raw(selectSql),
|
||||||
...bindings,
|
...bindings,
|
||||||
safePageSize,
|
safePageSize,
|
||||||
offset
|
offset
|
||||||
);
|
) as T[];
|
||||||
|
|
||||||
return { items, total };
|
return { items, total };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -232,10 +239,10 @@ export async function getItemById(itemId: number): Promise<any | null> {
|
|||||||
|
|
||||||
for (const table of tables) {
|
for (const table of tables) {
|
||||||
try {
|
try {
|
||||||
const item = await itemsPrisma.$queryRaw<any>(
|
const item = await (itemsPrisma.$queryRaw as any)(
|
||||||
Prisma.raw(`SELECT * FROM ${table.name} WHERE id = ?`),
|
Prisma.raw(`SELECT * FROM ${table.name} WHERE id = ?`),
|
||||||
itemId
|
itemId
|
||||||
);
|
) as any[];
|
||||||
|
|
||||||
if (item && item.length > 0) {
|
if (item && item.length > 0) {
|
||||||
return item[0];
|
return item[0];
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export interface CreateBidData {
|
|||||||
* Cria um novo lance
|
* Cria um novo lance
|
||||||
*/
|
*/
|
||||||
export async function createBid(data: CreateBidData): Promise<Bid> {
|
export async function createBid(data: CreateBidData): Promise<Bid> {
|
||||||
return await prisma.bid.create({
|
const bid = await prisma.bid.create({
|
||||||
data: {
|
data: {
|
||||||
listing_id: data.listing_id,
|
listing_id: data.listing_id,
|
||||||
bidder_id: data.bidder_id,
|
bidder_id: data.bidder_id,
|
||||||
@@ -34,6 +34,11 @@ export async function createBid(data: CreateBidData): Promise<Bid> {
|
|||||||
bid_type: data.bid_type || null,
|
bid_type: data.bid_type || null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
...bid,
|
||||||
|
bid_amount: Number(bid.bid_amount),
|
||||||
|
bid_type: (bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,7 +48,7 @@ export async function getBidsByListing(
|
|||||||
listingId: number,
|
listingId: number,
|
||||||
activeOnly: boolean = true
|
activeOnly: boolean = true
|
||||||
): Promise<Bid[]> {
|
): Promise<Bid[]> {
|
||||||
return await prisma.bid.findMany({
|
const bids = await prisma.bid.findMany({
|
||||||
where: {
|
where: {
|
||||||
listing_id: listingId,
|
listing_id: listingId,
|
||||||
...(activeOnly ? { is_active: true } : {}),
|
...(activeOnly ? { is_active: true } : {}),
|
||||||
@@ -53,13 +58,18 @@ export async function getBidsByListing(
|
|||||||
{ created_at: "desc" },
|
{ created_at: "desc" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
return bids.map(bid => ({
|
||||||
|
...bid,
|
||||||
|
bid_amount: Number(bid.bid_amount),
|
||||||
|
bid_type: (bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca o maior lance de um anúncio
|
* Busca o maior lance de um anúncio
|
||||||
*/
|
*/
|
||||||
export async function getHighestBid(listingId: number): Promise<Bid | null> {
|
export async function getHighestBid(listingId: number): Promise<Bid | null> {
|
||||||
return await prisma.bid.findFirst({
|
const bid = await prisma.bid.findFirst({
|
||||||
where: {
|
where: {
|
||||||
listing_id: listingId,
|
listing_id: listingId,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
@@ -69,6 +79,12 @@ export async function getHighestBid(listingId: number): Promise<Bid | null> {
|
|||||||
{ created_at: "desc" },
|
{ created_at: "desc" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
if (!bid) return null;
|
||||||
|
return {
|
||||||
|
...bid,
|
||||||
|
bid_amount: Number(bid.bid_amount),
|
||||||
|
bid_type: (bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,8 +103,14 @@ export async function cancelBid(bidId: number): Promise<boolean> {
|
|||||||
* Busca lance por ID
|
* Busca lance por ID
|
||||||
*/
|
*/
|
||||||
export async function getBidById(bidId: number): Promise<Bid | null> {
|
export async function getBidById(bidId: number): Promise<Bid | null> {
|
||||||
return await prisma.bid.findUnique({
|
const bid = await prisma.bid.findUnique({
|
||||||
where: { bid_id: bidId },
|
where: { bid_id: bidId },
|
||||||
});
|
});
|
||||||
|
if (!bid) return null;
|
||||||
|
return {
|
||||||
|
...bid,
|
||||||
|
bid_amount: Number(bid.bid_amount),
|
||||||
|
bid_type: (bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ export interface SearchItemsParams {
|
|||||||
* Busca um item por ID unificado
|
* Busca um item por ID unificado
|
||||||
*/
|
*/
|
||||||
export async function getItemById(itemId: number): Promise<Item | null> {
|
export async function getItemById(itemId: number): Promise<Item | null> {
|
||||||
|
// @ts-expect-error - Prisma Client pode não ter sido gerado ainda
|
||||||
const item = await prisma.item.findUnique({
|
const item = await prisma.item.findUnique({
|
||||||
where: { item_id: itemId },
|
where: { item_id: itemId },
|
||||||
});
|
});
|
||||||
@@ -55,6 +56,7 @@ export async function getItemById(itemId: number): Promise<Item | null> {
|
|||||||
* Busca um item por nome (exato ou similar)
|
* Busca um item por nome (exato ou similar)
|
||||||
*/
|
*/
|
||||||
export async function getItemByName(itemName: string): Promise<Item | null> {
|
export async function getItemByName(itemName: string): Promise<Item | null> {
|
||||||
|
// @ts-expect-error - Prisma Client pode não ter sido gerado ainda
|
||||||
const item = await prisma.item.findFirst({
|
const item = await prisma.item.findFirst({
|
||||||
where: {
|
where: {
|
||||||
item_name: {
|
item_name: {
|
||||||
@@ -89,6 +91,7 @@ export async function searchItems(
|
|||||||
pageSize = 24,
|
pageSize = 24,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
|
// @ts-expect-error - ItemWhereInput pode não estar disponível se Prisma Client não foi gerado
|
||||||
const where: Prisma.ItemWhereInput = {};
|
const where: Prisma.ItemWhereInput = {};
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
@@ -106,8 +109,10 @@ export async function searchItems(
|
|||||||
where.rarity = rarity;
|
where.rarity = rarity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error - Prisma Client pode não ter sido gerado ainda
|
||||||
const total = await prisma.item.count({ where });
|
const total = await prisma.item.count({ where });
|
||||||
|
|
||||||
|
// @ts-expect-error - Prisma Client pode não ter sido gerado ainda
|
||||||
const items = await prisma.item.findMany({
|
const items = await prisma.item.findMany({
|
||||||
where,
|
where,
|
||||||
orderBy: { item_name: "asc" },
|
orderBy: { item_name: "asc" },
|
||||||
@@ -116,7 +121,7 @@ export async function searchItems(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: items.map((item) => ({
|
items: items.map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
weight: item.weight ? Number(item.weight) : null,
|
weight: item.weight ? Number(item.weight) : null,
|
||||||
metadata: item.metadata || {},
|
metadata: item.metadata || {},
|
||||||
@@ -133,6 +138,7 @@ export async function getItemBySource(
|
|||||||
sourceTableName: string,
|
sourceTableName: string,
|
||||||
sourceTableId: number
|
sourceTableId: number
|
||||||
): Promise<Item | null> {
|
): Promise<Item | null> {
|
||||||
|
// @ts-expect-error - Prisma Client pode não ter sido gerado ainda
|
||||||
const item = await prisma.item.findUnique({
|
const item = await prisma.item.findUnique({
|
||||||
where: {
|
where: {
|
||||||
source_table_name_source_table_id: {
|
source_table_name_source_table_id: {
|
||||||
|
|||||||
@@ -426,8 +426,8 @@ export async function getActiveListings(
|
|||||||
if (search) {
|
if (search) {
|
||||||
const searchCondition = {
|
const searchCondition = {
|
||||||
OR: [
|
OR: [
|
||||||
{ item_name: { contains: search, mode: "insensitive" } },
|
{ item_name: { contains: search, mode: "insensitive" as Prisma.QueryMode } },
|
||||||
{ description: { contains: search, mode: "insensitive" } },
|
{ description: { contains: search, mode: "insensitive" as Prisma.QueryMode } },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
// Combinar com a condição OR existente
|
// Combinar com a condição OR existente
|
||||||
|
|||||||
@@ -35,16 +35,20 @@ export interface CreateNotificationData {
|
|||||||
export async function createNotification(
|
export async function createNotification(
|
||||||
data: CreateNotificationData
|
data: CreateNotificationData
|
||||||
): Promise<Notification> {
|
): Promise<Notification> {
|
||||||
return await prisma.notification.create({
|
const notification = await prisma.notification.create({
|
||||||
data: {
|
data: {
|
||||||
user_id: data.user_id,
|
user_id: data.user_id,
|
||||||
notification_type: data.notification_type,
|
notification_type: data.notification_type as NotificationType,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
message: data.message,
|
message: data.message,
|
||||||
related_id: data.related_id || null,
|
related_id: data.related_id || null,
|
||||||
send_whatsapp: data.send_whatsapp || false,
|
send_whatsapp: data.send_whatsapp || false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
...notification,
|
||||||
|
notification_type: notification.notification_type as NotificationType,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,11 +70,15 @@ export async function getUserNotifications(
|
|||||||
where.notification_type = type;
|
where.notification_type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.notification.findMany({
|
const notifications = await prisma.notification.findMany({
|
||||||
where,
|
where,
|
||||||
orderBy: { created_at: "desc" },
|
orderBy: { created_at: "desc" },
|
||||||
take: limit,
|
take: limit,
|
||||||
});
|
});
|
||||||
|
return notifications.map(notification => ({
|
||||||
|
...notification,
|
||||||
|
notification_type: notification.notification_type as NotificationType,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,37 +36,67 @@ export interface ServerStats {
|
|||||||
* Lista todos os servidores
|
* Lista todos os servidores
|
||||||
*/
|
*/
|
||||||
export async function getAllServers(): Promise<Server[]> {
|
export async function getAllServers(): Promise<Server[]> {
|
||||||
return await prisma.server.findMany({
|
const servers = await prisma.server.findMany({
|
||||||
orderBy: { server_name: "asc" },
|
orderBy: { server_name: "asc" },
|
||||||
});
|
});
|
||||||
|
return servers.map(server => ({
|
||||||
|
...server,
|
||||||
|
server_type: server.server_type as "pvp" | "no-pvp" | "retro-pvp" | "hardcore-pvp" | null,
|
||||||
|
location: server.location as "NA" | "SA" | "EU" | "OC" | "BR" | null,
|
||||||
|
total_volume: server.total_volume ? Number(server.total_volume) : 0,
|
||||||
|
average_listing_price: server.average_listing_price ? Number(server.average_listing_price) : 0,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lista apenas servidores ativos
|
* Lista apenas servidores ativos
|
||||||
*/
|
*/
|
||||||
export async function getActiveServers(): Promise<Server[]> {
|
export async function getActiveServers(): Promise<Server[]> {
|
||||||
return await prisma.server.findMany({
|
const servers = await prisma.server.findMany({
|
||||||
where: { is_active: true },
|
where: { is_active: true },
|
||||||
orderBy: { server_name: "asc" },
|
orderBy: { server_name: "asc" },
|
||||||
});
|
});
|
||||||
|
return servers.map(server => ({
|
||||||
|
...server,
|
||||||
|
server_type: server.server_type as "pvp" | "no-pvp" | "retro-pvp" | "hardcore-pvp" | null,
|
||||||
|
location: server.location as "NA" | "SA" | "EU" | "OC" | "BR" | null,
|
||||||
|
total_volume: server.total_volume ? Number(server.total_volume) : 0,
|
||||||
|
average_listing_price: server.average_listing_price ? Number(server.average_listing_price) : 0,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca servidor por nome
|
* Busca servidor por nome
|
||||||
*/
|
*/
|
||||||
export async function getServerByName(serverName: string): Promise<Server | null> {
|
export async function getServerByName(serverName: string): Promise<Server | null> {
|
||||||
return await prisma.server.findUnique({
|
const server = await prisma.server.findUnique({
|
||||||
where: { server_name: serverName },
|
where: { server_name: serverName },
|
||||||
});
|
});
|
||||||
|
if (!server) return null;
|
||||||
|
return {
|
||||||
|
...server,
|
||||||
|
server_type: server.server_type as "pvp" | "no-pvp" | "retro-pvp" | "hardcore-pvp" | null,
|
||||||
|
location: server.location as "NA" | "SA" | "EU" | "OC" | "BR" | null,
|
||||||
|
total_volume: server.total_volume ? Number(server.total_volume) : 0,
|
||||||
|
average_listing_price: server.average_listing_price ? Number(server.average_listing_price) : 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca servidor por ID
|
* Busca servidor por ID
|
||||||
*/
|
*/
|
||||||
export async function getServerById(serverId: number): Promise<Server | null> {
|
export async function getServerById(serverId: number): Promise<Server | null> {
|
||||||
return await prisma.server.findUnique({
|
const server = await prisma.server.findUnique({
|
||||||
where: { server_id: serverId },
|
where: { server_id: serverId },
|
||||||
});
|
});
|
||||||
|
if (!server) return null;
|
||||||
|
return {
|
||||||
|
...server,
|
||||||
|
server_type: server.server_type as "pvp" | "no-pvp" | "retro-pvp" | "hardcore-pvp" | null,
|
||||||
|
location: server.location as "NA" | "SA" | "EU" | "OC" | "BR" | null,
|
||||||
|
total_volume: server.total_volume ? Number(server.total_volume) : 0,
|
||||||
|
average_listing_price: server.average_listing_price ? Number(server.average_listing_price) : 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export interface TelemetryEvent {
|
|||||||
ad_id: number | null;
|
ad_id: number | null;
|
||||||
user_id: number | null;
|
user_id: number | null;
|
||||||
session_id: string | null;
|
session_id: string | null;
|
||||||
metadata: Record<string, any>;
|
metadata: Prisma.JsonValue;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,15 +104,19 @@ export interface MarketplaceStats {
|
|||||||
export async function trackEvent(
|
export async function trackEvent(
|
||||||
data: TrackEventData
|
data: TrackEventData
|
||||||
): Promise<TelemetryEvent> {
|
): Promise<TelemetryEvent> {
|
||||||
return await prisma.telemetryEvent.create({
|
const event = await prisma.telemetryEvent.create({
|
||||||
data: {
|
data: {
|
||||||
event_type: data.event_type,
|
event_type: data.event_type as TelemetryEventType,
|
||||||
ad_id: data.ad_id || null,
|
ad_id: data.ad_id || null,
|
||||||
user_id: data.user_id || null,
|
user_id: data.user_id || null,
|
||||||
session_id: data.session_id || null,
|
session_id: data.session_id || null,
|
||||||
metadata: data.metadata || {},
|
metadata: data.metadata || {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
event_type: event.event_type as TelemetryEventType,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,13 +127,13 @@ export async function trackEventsBatch(
|
|||||||
): Promise<TelemetryEvent[]> {
|
): Promise<TelemetryEvent[]> {
|
||||||
if (events.length === 0) return [];
|
if (events.length === 0) return [];
|
||||||
|
|
||||||
return await prisma.$transaction(
|
const eventsResult = await prisma.$transaction(
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
events.map((event) =>
|
events.map((event) =>
|
||||||
tx.telemetryEvent.create({
|
tx.telemetryEvent.create({
|
||||||
data: {
|
data: {
|
||||||
event_type: event.event_type,
|
event_type: event.event_type as TelemetryEventType,
|
||||||
ad_id: event.ad_id || null,
|
ad_id: event.ad_id || null,
|
||||||
user_id: event.user_id || null,
|
user_id: event.user_id || null,
|
||||||
session_id: event.session_id || null,
|
session_id: event.session_id || null,
|
||||||
@@ -140,15 +144,27 @@ export async function trackEventsBatch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
return eventsResult.map(event => ({
|
||||||
|
...event,
|
||||||
|
event_type: event.event_type as TelemetryEventType,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca analytics de um anúncio específico
|
* Busca analytics de um anúncio específico
|
||||||
*/
|
*/
|
||||||
export async function getAdAnalytics(adId: number): Promise<AdAnalytics | null> {
|
export async function getAdAnalytics(adId: number): Promise<AdAnalytics | null> {
|
||||||
return await prisma.adAnalytics.findUnique({
|
const analytics = await prisma.adAnalytics.findUnique({
|
||||||
where: { ad_id: adId },
|
where: { ad_id: adId },
|
||||||
});
|
});
|
||||||
|
if (!analytics) return null;
|
||||||
|
return {
|
||||||
|
...analytics,
|
||||||
|
ctr: Number(analytics.ctr),
|
||||||
|
avg_view_duration: Number(analytics.avg_view_duration),
|
||||||
|
bounce_rate: Number(analytics.bounce_rate),
|
||||||
|
conversion_rate: Number(analytics.conversion_rate),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export async function createTransaction(
|
|||||||
// Calcular escrow_fee se não fornecido (2% do valor)
|
// Calcular escrow_fee se não fornecido (2% do valor)
|
||||||
const escrowFee = data.escrow_fee ?? Number((data.amount * 0.02).toFixed(2));
|
const escrowFee = data.escrow_fee ?? Number((data.amount * 0.02).toFixed(2));
|
||||||
|
|
||||||
return await prisma.transaction.create({
|
const transaction = await prisma.transaction.create({
|
||||||
data: {
|
data: {
|
||||||
listing_id: data.listing_id || null,
|
listing_id: data.listing_id || null,
|
||||||
buyer_id: data.buyer_id,
|
buyer_id: data.buyer_id,
|
||||||
@@ -82,6 +82,13 @@ export async function createTransaction(
|
|||||||
escrow_fee: escrowFee,
|
escrow_fee: escrowFee,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
...transaction,
|
||||||
|
amount: Number(transaction.amount),
|
||||||
|
escrow_fee: transaction.escrow_fee ? Number(transaction.escrow_fee) : null,
|
||||||
|
transaction_status: transaction.transaction_status as TransactionStatus,
|
||||||
|
payment_method: transaction.payment_method as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,9 +97,17 @@ export async function createTransaction(
|
|||||||
export async function getTransactionById(
|
export async function getTransactionById(
|
||||||
transactionId: number
|
transactionId: number
|
||||||
): Promise<Transaction | null> {
|
): Promise<Transaction | null> {
|
||||||
return await prisma.transaction.findUnique({
|
const transaction = await prisma.transaction.findUnique({
|
||||||
where: { transaction_id: transactionId },
|
where: { transaction_id: transactionId },
|
||||||
});
|
});
|
||||||
|
if (!transaction) return null;
|
||||||
|
return {
|
||||||
|
...transaction,
|
||||||
|
amount: Number(transaction.amount),
|
||||||
|
escrow_fee: transaction.escrow_fee ? Number(transaction.escrow_fee) : null,
|
||||||
|
transaction_status: transaction.transaction_status as TransactionStatus,
|
||||||
|
payment_method: transaction.payment_method as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,10 +129,17 @@ export async function updateTransactionStatus(
|
|||||||
if (data.buyer_notes !== undefined) updateData.buyer_notes = data.buyer_notes;
|
if (data.buyer_notes !== undefined) updateData.buyer_notes = data.buyer_notes;
|
||||||
if (data.seller_notes !== undefined) updateData.seller_notes = data.seller_notes;
|
if (data.seller_notes !== undefined) updateData.seller_notes = data.seller_notes;
|
||||||
|
|
||||||
return await prisma.transaction.update({
|
const transaction = await prisma.transaction.update({
|
||||||
where: { transaction_id: transactionId },
|
where: { transaction_id: transactionId },
|
||||||
data: updateData,
|
data: updateData,
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
...transaction,
|
||||||
|
amount: Number(transaction.amount),
|
||||||
|
escrow_fee: transaction.escrow_fee ? Number(transaction.escrow_fee) : null,
|
||||||
|
transaction_status: transaction.transaction_status as TransactionStatus,
|
||||||
|
payment_method: transaction.payment_method as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,7 +185,13 @@ export async function getTransactionsByUser(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transactions,
|
transactions: transactions.map(transaction => ({
|
||||||
|
...transaction,
|
||||||
|
amount: Number(transaction.amount),
|
||||||
|
escrow_fee: transaction.escrow_fee ? Number(transaction.escrow_fee) : null,
|
||||||
|
transaction_status: transaction.transaction_status as TransactionStatus,
|
||||||
|
payment_method: transaction.payment_method as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||||
|
})),
|
||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,25 @@ export async function getUserByUsername(
|
|||||||
/**
|
/**
|
||||||
* Busca usuário por ID (sem password_hash)
|
* Busca usuário por ID (sem password_hash)
|
||||||
*/
|
*/
|
||||||
export async function getUserById(userId: number): Promise<User | null> {
|
type PublicUser = {
|
||||||
|
user_id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
phone: string | null;
|
||||||
|
cpf: string | null;
|
||||||
|
account_level: string;
|
||||||
|
has_donated: boolean;
|
||||||
|
whatsapp_notifications: boolean;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
last_login: Date | null;
|
||||||
|
is_active: boolean;
|
||||||
|
is_verified: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getUserById(userId: number): Promise<PublicUser | null> {
|
||||||
return await prisma.user.findUnique({
|
return await prisma.user.findUnique({
|
||||||
where: { user_id: userId },
|
where: { user_id: userId },
|
||||||
select: {
|
select: {
|
||||||
@@ -96,13 +114,13 @@ export async function getUserById(userId: number): Promise<User | null> {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
is_verified: true,
|
is_verified: true,
|
||||||
},
|
},
|
||||||
});
|
}) as PublicUser | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca usuário por CPF (verifica duplicidade)
|
* Busca usuário por CPF (verifica duplicidade)
|
||||||
*/
|
*/
|
||||||
export async function getUserByCpf(cpf: string): Promise<User | null> {
|
export async function getUserByCpf(cpf: string): Promise<Pick<User, 'user_id' | 'username' | 'email'> | null> {
|
||||||
return await prisma.user.findUnique({
|
return await prisma.user.findUnique({
|
||||||
where: { cpf },
|
where: { cpf },
|
||||||
select: {
|
select: {
|
||||||
@@ -110,7 +128,7 @@ export async function getUserByCpf(cpf: string): Promise<User | null> {
|
|||||||
username: true,
|
username: true,
|
||||||
email: true,
|
email: true,
|
||||||
},
|
},
|
||||||
});
|
}) as Pick<User, 'user_id' | 'username' | 'email'> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,7 +137,7 @@ export async function getUserByCpf(cpf: string): Promise<User | null> {
|
|||||||
export async function updateUser(
|
export async function updateUser(
|
||||||
userId: number,
|
userId: number,
|
||||||
data: UpdateUserData
|
data: UpdateUserData
|
||||||
): Promise<User> {
|
): Promise<PublicUser> {
|
||||||
const updateData: any = {};
|
const updateData: any = {};
|
||||||
if (data.first_name !== undefined) updateData.first_name = data.first_name;
|
if (data.first_name !== undefined) updateData.first_name = data.first_name;
|
||||||
if (data.last_name !== undefined) updateData.last_name = data.last_name;
|
if (data.last_name !== undefined) updateData.last_name = data.last_name;
|
||||||
@@ -149,7 +167,7 @@ export async function updateUser(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user as PublicUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ export class MarketTelemetry {
|
|||||||
const eventsToSend = this.eventQueue.map(({ timestamp, ...event }) => event);
|
const eventsToSend = this.eventQueue.map(({ timestamp, ...event }) => event);
|
||||||
|
|
||||||
// Usar sendBeacon para garantir envio
|
// Usar sendBeacon para garantir envio
|
||||||
const eventsWithSession = eventsToSend.map(({ timestamp, ...event }) => ({
|
const eventsWithSession = eventsToSend.map((event) => ({
|
||||||
...event,
|
...event,
|
||||||
session_id: this.sessionId,
|
session_id: this.sessionId,
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in new issue
Block a user