"use client"; import { useState, useEffect } from "react"; import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Heart, Clock, Eye, Star, MapPin, DollarSign, Coins, RefreshCw, ShoppingCart, TrendingUp, User, MessageCircle } from "lucide-react"; 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"; import { ChatModal } from "@/components/chat/ChatModal"; // Função auxiliar para formatar tempo relativo function formatTimeAgo(date: Date): string { const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000); if (seconds < 60) return "há alguns segundos"; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `há ${minutes} ${minutes === 1 ? "minuto" : "minutos"}`; const hours = Math.floor(minutes / 60); if (hours < 24) return `há ${hours} ${hours === 1 ? "hora" : "horas"}`; const days = Math.floor(hours / 24); if (days < 30) return `há ${days} ${days === 1 ? "dia" : "dias"}`; const months = Math.floor(days / 30); if (months < 12) return `há ${months} ${months === 1 ? "mês" : "meses"}`; const years = Math.floor(months / 12); return `há ${years} ${years === 1 ? "ano" : "anos"}`; } interface Listing { listing_id: number; seller_id: number; item_id: number; // ID legado (SQLite) - mantido para compatibilidade item_name: string; quantity: number; server_name: string; price_pix: number | string | null; // PostgreSQL DECIMAL pode vir como string price_tibia_coins: number | string | null; // PostgreSQL INTEGER pode vir como string price_gold_coins: number | string | null; // PostgreSQL BIGINT pode vir como string is_auction: boolean; description: string | null; listing_status: string; created_at: string; views_count: number; favorites_count: number; is_featured?: boolean; trade_item_id?: number | null; trade_item_name?: string | null; trade_item_quantity?: number | null; // Campos de leilão current_bid_amount?: number | null; min_bid_increment?: number | null; initial_price_pix?: number | null; initial_price_tibia_coins?: number | null; initial_price_gold_coins?: number | null; auction_ends_at?: Date | null; // Campos do JOIN com items (unificado) item_id_unified?: number | null; // ID unificado da tabela items image_local_path?: string | null; image_url?: string | null; item_category?: string | null; // Campo do vendedor seller_character_name?: string | null; has_pending_intention_for_current_user?: boolean; highest_custom_offer_amount?: number | string | null; highest_custom_offer_currency?: string | null; } interface PriceInfo { type: "pix" | "tibia_coins" | "gold_coins" | "trade"; label: string; value: string; } interface AdCardProps { listing: Listing; viewMode?: "grid" | "list"; } export function AdCard({ listing, viewMode = "grid" }: AdCardProps) { const { user } = useSession(); const queryClient = useQueryClient(); const { trackAdClick, trackFavorite } = useMarketTelemetry(); const [isFavorited, setIsFavorited] = useState(false); const [favoritesCount, setFavoritesCount] = useState(listing.favorites_count || 0); const [showPurchaseModal, setShowPurchaseModal] = useState(false); const [showBidModal, setShowBidModal] = useState(false); const [showDetailsModal, setShowDetailsModal] = useState(false); const [showChatModal, setShowChatModal] = useState(false); const invalidateCache = () => { queryClient.invalidateQueries({ queryKey: ["listings"] }); queryClient.invalidateQueries({ queryKey: ["my-listings"] }); }; // Tracking automático de visualização useAdTracking(listing.listing_id); // Verificar se está favoritado useEffect(() => { if (user?.userId && listing.listing_id) { fetch(`/api/listings/${listing.listing_id}/favorite`) .then((res) => { if (res.ok) { return res.json(); } return null; }) .then((data) => { if (data?.success) { setIsFavorited(data.favorited); } }) .catch(() => { // Silenciar erro para não gerar mensagens no console }); } }, [user?.userId, listing.listing_id]); const handleFavorite = async () => { if (!user) return; const action = isFavorited ? "unfavorite" : "favorite"; trackFavorite(listing.listing_id, action); try { const res = await fetch(`/api/listings/${listing.listing_id}/favorite`, { method: "POST", }); if (res.ok) { const data = await res.json(); setIsFavorited(data.favorited); setFavoritesCount((prev) => (data.favorited ? prev + 1 : Math.max(0, prev - 1))); } } catch (error) { console.error("Erro ao favoritar:", error); } }; const handleClick = (e?: React.MouseEvent) => { if (e) { e.stopPropagation(); } trackAdClick(listing.listing_id); setShowDetailsModal(true); }; const handlePause = async () => { try { const res = await fetch(`/api/listings/${listing.listing_id}/pause`, { method: "POST", }); if (res.ok) { setShowDetailsModal(false); invalidateCache(); } } catch (error) { console.error("Erro ao pausar anúncio:", error); } }; const handleActivate = async () => { try { const res = await fetch(`/api/listings/${listing.listing_id}/activate`, { method: "POST", }); if (res.ok) { setShowDetailsModal(false); invalidateCache(); } else { const data = await res.json(); alert(data.error || "Erro ao reativar anúncio"); } } catch (error) { console.error("Erro ao reativar anúncio:", error); alert("Erro ao reativar anúncio"); } }; const handleCancel = async () => { if (!confirm("Tem certeza que deseja cancelar este anúncio?")) { return; } try { const res = await fetch(`/api/listings/${listing.listing_id}`, { method: "DELETE", }); if (res.ok) { setShowDetailsModal(false); invalidateCache(); } else { const data = await res.json(); alert(data.error || "Erro ao cancelar anúncio"); } } catch (error) { console.error("Erro ao cancelar anúncio:", error); alert("Erro ao cancelar anúncio"); } }; const getAvailablePrices = (): PriceInfo[] => { const prices: PriceInfo[] = []; // PIX if (listing.price_pix) { const price = typeof listing.price_pix === 'string' ? parseFloat(listing.price_pix) : listing.price_pix; if (!isNaN(price)) { prices.push({ type: "pix", label: "PIX", value: `R$ ${price.toFixed(2)}`, }); } } // Tibia Coins if (listing.price_tibia_coins) { const price = typeof listing.price_tibia_coins === 'string' ? parseInt(listing.price_tibia_coins, 10) : listing.price_tibia_coins; if (!isNaN(price)) { prices.push({ type: "tibia_coins", label: "TC", value: `${price.toLocaleString()} TC`, }); } } // Gold Coins if (listing.price_gold_coins) { const price = typeof listing.price_gold_coins === 'string' ? parseInt(listing.price_gold_coins, 10) : listing.price_gold_coins; if (!isNaN(price)) { prices.push({ type: "gold_coins", label: "GP", value: `${price.toLocaleString()} GP`, }); } } // Item de Troca if (listing.trade_item_id && listing.trade_item_name && listing.trade_item_quantity) { const quantity = listing.trade_item_quantity; prices.push({ type: "trade", label: "Troca", value: `Troca por ${quantity}x ${listing.trade_item_name}`, }); } return prices; }; const timeAgo = formatTimeAgo(new Date(listing.created_at)); const availablePrices = getAvailablePrices(); const isSold = listing.listing_status === "sold"; const hasPendingIntention = Boolean(listing.has_pending_intention_for_current_user); const getHighestOfferLabel = () => { if ( listing.highest_custom_offer_amount === null || listing.highest_custom_offer_amount === undefined || !listing.highest_custom_offer_currency ) { return null; } const amount = typeof listing.highest_custom_offer_amount === "string" ? Number(listing.highest_custom_offer_amount) : listing.highest_custom_offer_amount; if (Number.isNaN(amount)) { return null; } switch (listing.highest_custom_offer_currency) { case "pix": return `Maior oferta: R$ ${amount.toFixed(2)}`; case "tibia_coins": return `Maior oferta: ${Math.round(amount).toLocaleString()} TC`; case "gold_coins": return `Maior oferta: ${Math.round(amount).toLocaleString()} GP`; default: return `Maior oferta: ${amount}`; } }; const highestOfferLabel = getHighestOfferLabel(); // Função para obter URL da imagem do item const getItemImageUrl = (): string => { // Prioridade 1: image_url (URL externa) se disponível if (listing.image_url) { return listing.image_url; } // Prioridade 2: image_local_path se disponível if (listing.image_local_path) { const normalizedPath = listing.image_local_path.replace(/\\/g, "/"); if (normalizedPath.startsWith("data/")) { return `/api/images?path=${encodeURIComponent(normalizedPath)}`; } // Construir caminho se necessário const category = listing.item_category || "other"; return `/api/images?path=${encodeURIComponent(`data/images/${category}/${normalizedPath}`)}`; } // Prioridade 3: Usar item_id_unified se disponível (com nome para validação) if (listing.item_id_unified) { const params = new URLSearchParams({ item: listing.item_id_unified.toString() }); if (listing.item_name) { params.set("name", listing.item_name); } return `/api/images?${params.toString()}`; } // Fallback: Usar item_id legado (com nome para validação) const params = new URLSearchParams({ item: listing.item_id.toString() }); if (listing.item_name) { params.set("name", listing.item_name); } return `/api/images?${params.toString()}`; }; const itemImageUrl = getItemImageUrl(); // 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
{listing.description}
)}{listing.description}
)}