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
+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 { useSession } from "@/lib/hooks/use-session";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { PurchaseModal } from "./PurchaseModal";
|
||||
import { BidModal } from "./BidModal";
|
||||
import { SellerInfo } from "./SellerInfo";
|
||||
import { ListingDetailsModal } from "./ListingDetailsModal";
|
||||
import { AuctionCountdown } from "./AuctionCountdown";
|
||||
|
||||
// Função auxiliar para formatar tempo relativo
|
||||
function formatTimeAgo(date: Date): string {
|
||||
@@ -297,8 +299,22 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
|
||||
const itemImageUrl = getItemImageUrl();
|
||||
|
||||
// Componente para exibir badges de preços
|
||||
const PriceBadges = ({ prices, compact = false }: { prices: PriceInfo[]; compact?: boolean }) => {
|
||||
// Função para obter URL da imagem do item de troca
|
||||
const getTradeItemImageUrl = (): string | null => {
|
||||
if (!listing.trade_item_id || !listing.trade_item_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Usar trade_item_id para buscar imagem
|
||||
const params = new URLSearchParams({ item: listing.trade_item_id.toString() });
|
||||
params.set("name", listing.trade_item_name);
|
||||
return `/api/images?${params.toString()}`;
|
||||
};
|
||||
|
||||
const tradeItemImageUrl = getTradeItemImageUrl();
|
||||
|
||||
// Componente para exibir badges de preços em quadrados
|
||||
const PriceBadges = ({ prices, compact = false, tradeItemImageUrl, listing }: { prices: PriceInfo[]; compact?: boolean; tradeItemImageUrl?: string | null; listing?: Listing }) => {
|
||||
if (prices.length === 0) {
|
||||
return <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"]) => {
|
||||
switch (type) {
|
||||
case "pix":
|
||||
return "bg-green-500 hover:bg-green-600 text-white";
|
||||
return "bg-green-100 hover:bg-green-200 dark:bg-green-900/30 dark:hover:bg-green-900/50 text-green-700 dark:text-green-300 border-green-300 dark:border-green-700";
|
||||
case "tc":
|
||||
return "bg-blue-500 hover:bg-blue-600 text-white";
|
||||
return "bg-blue-100 hover:bg-blue-200 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700";
|
||||
case "gp":
|
||||
return "bg-yellow-500 hover:bg-yellow-600 text-white";
|
||||
return "bg-amber-100 hover:bg-amber-200 dark:bg-amber-900/30 dark:hover:bg-amber-900/50 text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-700";
|
||||
case "trade":
|
||||
return "bg-purple-500 hover:bg-purple-600 text-white";
|
||||
return "bg-purple-100 hover:bg-purple-200 dark:bg-purple-900/30 dark:hover:bg-purple-900/50 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-700";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -332,58 +348,76 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
}
|
||||
};
|
||||
|
||||
if (prices.length === 1 && !compact) {
|
||||
// Se houver apenas um preço, mostrar em destaque
|
||||
const price = prices[0];
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-2xl font-bold">{price.value}</div>
|
||||
<Badge className={getBadgeClassName(price.type)}>
|
||||
{getIcon(price.type)}
|
||||
<span className="ml-1">{price.label}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Calcular número de colunas (apenas preços)
|
||||
const totalCols = prices.length;
|
||||
// Ajustar tamanho mínimo para caber todos os itens
|
||||
const colClass = compact
|
||||
? `flex-1 min-w-0`
|
||||
: `flex-1 min-w-0`;
|
||||
|
||||
// Múltiplos preços: mostrar badges
|
||||
if (compact) {
|
||||
// Layout compacto para modo list
|
||||
return (
|
||||
<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 (
|
||||
<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) => (
|
||||
<Badge key={index} className={`${getBadgeClassName(price.type)} text-xs`}>
|
||||
{getIcon(price.type)}
|
||||
<span className="ml-1">{price.value}</span>
|
||||
</Badge>
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
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>
|
||||
);
|
||||
@@ -392,24 +426,40 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
if (viewMode === "list") {
|
||||
return (
|
||||
<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 && (
|
||||
<div className="absolute inset-0 bg-black/10 rounded-lg z-10 pointer-events-none" />
|
||||
)}
|
||||
<div className="flex gap-4 p-4">
|
||||
{/* Thumbnail */}
|
||||
<div className="relative w-24 h-24 flex-shrink-0 rounded-lg overflow-hidden bg-muted flex items-center justify-center p-2">
|
||||
<Image
|
||||
src={itemImageUrl}
|
||||
alt={listing.item_name}
|
||||
fill
|
||||
className="object-contain"
|
||||
sizes="96px"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = "/placeholder-item.png";
|
||||
}}
|
||||
/>
|
||||
{/* Título */}
|
||||
<div className="px-4 pt-0 pb-1.5">
|
||||
<h4 className="text-base font-semibold text-foreground">Item a venda</h4>
|
||||
</div>
|
||||
<div className="flex gap-3 p-3 pt-1">
|
||||
{/* Thumbnails */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{/* Imagem do item principal */}
|
||||
<div className={cn(
|
||||
"relative w-24 h-24 rounded-lg overflow-hidden flex items-center justify-center p-1.5",
|
||||
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>
|
||||
|
||||
{/* 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-1 min-w-0">
|
||||
<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 && (
|
||||
<SellerInfo
|
||||
sellerId={listing.seller_id}
|
||||
@@ -427,7 +477,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
/>
|
||||
)}
|
||||
</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">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{timeAgo}</span>
|
||||
@@ -447,7 +497,10 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
</Badge>
|
||||
)}
|
||||
{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
|
||||
variant="ghost"
|
||||
@@ -457,52 +510,106 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
handleFavorite();
|
||||
}}
|
||||
disabled={!user}
|
||||
className="transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-950/20"
|
||||
>
|
||||
<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>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<PriceBadges prices={availablePrices} compact />
|
||||
<div className="flex items-center justify-between mt-2 gap-4">
|
||||
<div className="flex-1 min-w-0 space-y-1.5">
|
||||
{/* 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 */}
|
||||
{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 ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Lance Atual:</span>
|
||||
<span className="font-semibold text-primary">
|
||||
R$ {typeof listing.current_bid_amount === 'number' ? listing.current_bid_amount.toFixed(2) : parseFloat(String(listing.current_bid_amount)).toFixed(2)}
|
||||
</span>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
{listing.min_bid_increment && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-muted-foreground">Incremento Mín</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 className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Leilão Ativo</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<div className="flex items-center justify-center pt-1 border-t border-primary/10">
|
||||
<Badge variant="secondary" className="bg-primary/20 text-primary hover:bg-primary/30 text-xs">
|
||||
<TrendingUp className="h-3 w-3 mr-1" />
|
||||
Lances
|
||||
Leilão Ativo
|
||||
</Badge>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground">
|
||||
Sem lances ainda
|
||||
<div className="space-y-1">
|
||||
<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 className="flex items-center gap-4 ml-4">
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-3 ml-4">
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Eye className="h-4 w-4" />
|
||||
<span>{listing.views_count}</span>
|
||||
@@ -519,6 +626,18 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
<Button
|
||||
variant="default"
|
||||
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) => {
|
||||
e.stopPropagation();
|
||||
setShowBidModal(true);
|
||||
@@ -532,6 +651,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="transition-all duration-200 hover:scale-105"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowPurchaseModal(true);
|
||||
@@ -566,7 +686,18 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
<BidModal
|
||||
open={showBidModal}
|
||||
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={() => {
|
||||
invalidateCache();
|
||||
}}
|
||||
@@ -586,24 +717,47 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
// View mode grid (padrão)
|
||||
return (
|
||||
<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 && (
|
||||
<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">
|
||||
{/* Thumbnail */}
|
||||
<div className="relative w-full aspect-square rounded-t-lg overflow-hidden bg-muted flex items-center justify-center p-4">
|
||||
<Image
|
||||
src={itemImageUrl}
|
||||
alt={listing.item_name}
|
||||
fill
|
||||
className="object-contain"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = "/placeholder-item.png";
|
||||
}}
|
||||
/>
|
||||
{/* Título */}
|
||||
<div className="px-3 pt-0 pb-1">
|
||||
<h4 className="text-base font-semibold text-foreground">Item a venda</h4>
|
||||
</div>
|
||||
{/* Container de imagens */}
|
||||
<div className="relative bg-muted rounded-lg overflow-hidden mx-2 mb-1.5">
|
||||
{/* Imagem do item principal */}
|
||||
<div className="flex items-center justify-center p-2 h-40">
|
||||
<div className={cn(
|
||||
"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 && (
|
||||
<div className="absolute top-2 right-2 z-20">
|
||||
<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 && (
|
||||
<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>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-4 flex-1 flex flex-col">
|
||||
<h3 className="font-semibold text-lg mb-2 line-clamp-2">{listing.item_name}</h3>
|
||||
<CardContent className="p-3 flex-1 flex flex-col gap-2">
|
||||
<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 && (
|
||||
<SellerInfo
|
||||
sellerId={listing.seller_id}
|
||||
@@ -642,49 +799,94 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
</div>
|
||||
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<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 */}
|
||||
{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 ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Lance Atual:</span>
|
||||
<span className="font-semibold text-primary">
|
||||
R$ {typeof listing.current_bid_amount === 'number' ? listing.current_bid_amount.toFixed(2) : parseFloat(String(listing.current_bid_amount)).toFixed(2)}
|
||||
</span>
|
||||
{listing.min_bid_increment && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-muted-foreground">Incremento Mínimo</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>
|
||||
{listing.min_bid_increment && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Incremento Mín:</span>
|
||||
<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" />
|
||||
<div className="flex items-center justify-center pt-1 border-t border-primary/10">
|
||||
<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 className="text-center text-muted-foreground py-1">
|
||||
Sem lances ainda
|
||||
<div className="space-y-1.5">
|
||||
<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 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">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{timeAgo}</span>
|
||||
@@ -697,7 +899,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
</div>
|
||||
</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 */}
|
||||
{user && user.userId !== listing.seller_id && listing.listing_status === "active" && !isSold && (
|
||||
<>
|
||||
@@ -705,7 +907,18 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
<Button
|
||||
variant="default"
|
||||
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) => {
|
||||
e.stopPropagation();
|
||||
setShowBidModal(true);
|
||||
@@ -719,7 +932,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
className="flex-1 transition-all duration-200 hover:scale-105"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowPurchaseModal(true);
|
||||
@@ -743,7 +956,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
className="flex-1 transition-all duration-200 hover:scale-105"
|
||||
onClick={handleClick}
|
||||
>
|
||||
Ver Detalhes
|
||||
@@ -758,9 +971,13 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
handleFavorite();
|
||||
}}
|
||||
disabled={!user}
|
||||
className="transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-950/20"
|
||||
>
|
||||
<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>
|
||||
</Button>
|
||||
@@ -779,7 +996,18 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
|
||||
<BidModal
|
||||
open={showBidModal}
|
||||
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={() => {
|
||||
// Recarregar página ou atualizar lista
|
||||
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 [previewItem, setPreviewItem] = useState<Item | null>(selectedItem || null);
|
||||
const searchRef = useRef<HTMLDivElement>(null);
|
||||
const debounceRef = useRef<NodeJS.Timeout>();
|
||||
const debounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
// Buscar itens ao digitar
|
||||
useEffect(() => {
|
||||
|
||||
@@ -5,16 +5,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root>
|
||||
>(({ ...props }, ref) => (
|
||||
<DialogPrimitive.Root
|
||||
ref={ref}
|
||||
suppressHydrationWarning
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
Dialog.displayName = "Dialog";
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
@@ -12,7 +12,6 @@ function DropdownMenu({
|
||||
return (
|
||||
<DropdownMenuPrimitive.Root
|
||||
data-slot="dropdown-menu"
|
||||
suppressHydrationWarning
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return (
|
||||
<SheetPrimitive.Root
|
||||
data-slot="sheet"
|
||||
suppressHydrationWarning
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user