"use client"; import { ArrowDown, ArrowUp, Boxes, Clock, Coins, Cross, Zap as Energy, Flame, FlaskConical, Gem, Heart, Leaf, Minus, ScrollText, Shield, Skull, Snowflake, Star, Sword, TrendingDown, TrendingUp, Weight, Wrench, Zap, } from "lucide-react"; import { useLocale, useTranslations } from "next-intl"; import { useEffect, useRef, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { MagicCard } from "@/components/ui/magic-card"; import { PriceDisplay } from "@/components/ui/price-display"; import { formatNumber } from "@/lib/utils/formatters"; import type { TibiaItem } from "@/types/item"; interface ItemCardProps { item: TibiaItem; mode?: "market" | "catalog"; } export function ItemCard({ item, mode = "market" }: ItemCardProps) { const t = useTranslations("home.item"); const locale = useLocale(); const [isFavorited, setIsFavorited] = useState(false); const [_isHovered, setIsHovered] = useState(false); const formatVocation = (v: string | undefined): string | undefined => { if (!v) return undefined; let out = String(v); // Inserir espaço quando 'and' vier colado em palavras out = out.replace(/([A-Za-z])and([A-Za-z])/g, "$1 and $2"); // Normalizar vírgulas e conectivos out = out.replace(/,/g, ", "); out = out.replace(/\s*and\s*/gi, " and "); // Colapsar espaços múltiplos out = out.replace(/\s+/g, " ").trim(); return out; }; const ScrollRow = ({ children, ariaLabel, }: { children: React.ReactNode; ariaLabel: string; }) => { const scrollerRef = useRef(null); const contentRef = useRef(null); const [_canScrollLeft, setCanScrollLeft] = useState(false); const [_canScrollRight, setCanScrollRight] = useState(false); const [isHovering, setIsHovering] = useState(false); const autoScrollRef = useRef(null); const drag = useRef<{ active: boolean; startX: number; startLeft: number }>( { active: false, startX: 0, startLeft: 0 }, ); const [isDragging, setIsDragging] = useState(false); const updateButtons = () => { const el = scrollerRef.current; if (!el) return; const base = contentRef.current?.offsetWidth || 0; const hasOverflow = el.scrollWidth > el.clientWidth + 2 && base > 0; // Em loop infinito, ambos os lados ficam disponíveis quando há overflow setCanScrollLeft(hasOverflow); setCanScrollRight(hasOverflow); }; useEffect(() => { // Inject global CSS once to hide scrollbars cross-browser const styleId = "no-scrollbar-styles"; if (!document.getElementById(styleId)) { const style = document.createElement("style"); style.id = styleId; style.textContent = ` .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } .no-scrollbar::-webkit-scrollbar { display: none; height: 0; width: 0; } `; document.head.appendChild(style); } updateButtons(); const el = scrollerRef.current; if (!el) return; const handler = () => updateButtons(); el.addEventListener("scroll", handler); const ro = new ResizeObserver(() => updateButtons()); ro.observe(el); const onEnter = () => setIsHovering(true); const onLeave = () => setIsHovering(false); el.addEventListener("mouseenter", onEnter); el.addEventListener("mouseleave", onLeave); return () => { el.removeEventListener("scroll", handler); el.removeEventListener("mouseenter", onEnter); el.removeEventListener("mouseleave", onLeave); ro.disconnect(); }; }, [updateButtons]); const _scrollBy = (dx: number) => { const el = scrollerRef.current; if (!el) return; const base = contentRef.current?.offsetWidth || 0; if (base > 0) { let next = el.scrollLeft + dx; if (next < 0) next += base; if (next >= base) next -= base; el.scrollTo({ left: next, behavior: "smooth" }); } else { el.scrollBy({ left: dx, behavior: "smooth" }); } }; // Auto-scroll when not hovering and there's overflow useEffect(() => { const tick = () => { const el = scrollerRef.current; if (!el) return; const base = contentRef.current?.offsetWidth || 0; const hasOverflow = el.scrollWidth > el.clientWidth + 2 && base > 0; if (!isHovering && hasOverflow) { // velocidade ajustada para ~36px/s @60fps el.scrollLeft += 0.6; if (base > 0 && el.scrollLeft >= base) { // wrap sem salto perceptível el.scrollLeft -= base; } } autoScrollRef.current = window.requestAnimationFrame(tick); }; autoScrollRef.current = window.requestAnimationFrame(tick); return () => { if (autoScrollRef.current) cancelAnimationFrame(autoScrollRef.current); autoScrollRef.current = null; }; }, [isHovering]); // Drag to scroll (pointer events) useEffect(() => { const el = scrollerRef.current; if (!el) return; const onPointerDown = (e: PointerEvent) => { if (e.button !== 0) return; // only left click/touch drag.current.active = true; drag.current.startX = e.clientX; drag.current.startLeft = el.scrollLeft; setIsDragging(true); el.setPointerCapture(e.pointerId); }; const onPointerMove = (e: PointerEvent) => { if (!drag.current.active) return; const dx = e.clientX - drag.current.startX; const base = contentRef.current?.offsetWidth || 0; let next = drag.current.startLeft - dx; if (base > 0) { if (next < 0) next += base; if (next >= base) next -= base; } el.scrollLeft = next; }; const onPointerUp = (e: PointerEvent) => { if (!drag.current.active) return; drag.current.active = false; setIsDragging(false); try { el.releasePointerCapture(e.pointerId); } catch {} }; el.addEventListener("pointerdown", onPointerDown); el.addEventListener("pointermove", onPointerMove); el.addEventListener("pointerup", onPointerUp); el.addEventListener("pointercancel", onPointerUp); return () => { el.removeEventListener("pointerdown", onPointerDown); el.removeEventListener("pointermove", onPointerMove); el.removeEventListener("pointerup", onPointerUp); el.removeEventListener("pointercancel", onPointerUp); }; }, []); return (
{/* botões removidos conforme solicitado */}
{children} {children}
); }; const [_showAllDroppers, _setShowAllDroppers] = useState(false); const _rarityColors = { common: "bg-gray-500", uncommon: "bg-green-500", rare: "bg-blue-500", epic: "bg-purple-500", legendary: "bg-orange-500", }; const categoryIcons = { weapon: Sword, armor: Shield, helmet: Shield, legs: Shield, boots: Shield, shield: Shield, amulet: Gem, ring: Gem, potion: FlaskConical, rune: ScrollText, tool: Wrench, ammunition: Boxes, other: Boxes, } as const; const CategoryIcon = categoryIcons[item.category]; // Calcular diferença percentual entre compra e venda (somente quando houver preço) const hasPrice = item && (item as any).price && typeof (item as any).price.buy === "number" && typeof (item as any).price.sell === "number" && (item as any).price.sell !== 0; const priceDifference = hasPrice ? (((item as any).price.buy - (item as any).price.sell) / (item as any).price.sell) * 100 : 0; const isHotDeal = hasPrice && priceDifference > 20; // Hot deal se diferença > 20% // Determinar tendência de preço const getPriceTrend = () => { const ph = (item as any).priceHistory; if ( !ph || typeof ph.highest !== "number" || typeof ph.lowest !== "number" || typeof ph.average !== "number" ) { return "stable"; } if (ph.highest > ph.average * 1.1) return "up"; if (ph.lowest < ph.average * 0.9) return "down"; return "stable"; }; const trend = getPriceTrend(); const TrendIcon = trend === "up" ? ArrowUp : trend === "down" ? ArrowDown : Minus; const trendColor = trend === "up" ? "text-green-500" : trend === "down" ? "text-red-500" : "text-gray-500"; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {/* Header com ícone e badge de raridade */}
{/* Botão de favorito */} {/* Hot Deal Badge (apenas em modo market) */} {mode === "market" && isHotDeal && ( Hot Deal )} {/* Imagem real do item (se disponível) ou ícone por categoria */}
{item.image ? ( // eslint-disable-next-line @next/next/no-img-element {item.name} { (e.currentTarget as HTMLImageElement).style.display = "none"; }} /> ) : (
{typeof CategoryIcon === "string" ? null : ( )}
)}
{/* Conteúdo do card */}
{/* Nome do item */}

{item.name}

{/* Timestamp */}
Atualizado há 5 min
{/* Meta: ID e Categoria */}
{(() => { const catPt = (item as any).categoryPt as string | undefined; const catEn = (item as any).category as string | undefined; const catRaw = (catPt && String(catPt)) || (catEn && String(catEn)) || ""; const display = catRaw ? catRaw.charAt(0).toUpperCase() + catRaw.slice(1) : ""; return display ? ( {display} ) : null; })()} {typeof item.id === "string" && item.id && ( ID: {item.id} )}
{/* Estatísticas agrupadas */} {item.stats && (
{item.stats.attack && (
{item.stats.attack} ATK
)} {item.stats.defense && (
{item.stats.defense} DEF
)} {item.stats.armor && (
{item.stats.armor} ARM
)} {(item as any).volume && (
Volume {(item as any).volume}
)} {(item as any).bonus_de_skill && (
Skill +{(item as any).bonus_de_skill}
)}
{/* Proteções elementais */} {item.protections && ( {item.protections.fire && (
Fire +{item.protections.fire}%
)} {item.protections.ice && (
Ice +{item.protections.ice}%
)} {item.protections.energy && (
Energy +{item.protections.energy}%
)}
)} {/* Requisitos */} {item.requirements && (
{typeof (item as any).requirements?.level === "number" && (
Lvl {(item as any).requirements.level}+
)} {(item as any).requirements?.skill && (
{(item as any).requirements.skill}
)} {item.requirements.vocation && (
Best for {formatVocation(item.requirements.vocation)}
)}
)} {(item as any).protecao && (() => { const raw = String((item as any).protecao || ""); const tokens = raw .split(",") .map((s) => s.trim()) .filter(Boolean) .filter((s) => !/^nenhum(a)?\.?$/i.test(s.trim())); if (tokens.length === 0) return null; return ( {tokens.map((raw: string, idx: number) => { const token = raw.trim(); const match = token.match( /^([A-Za-zçÇáÁéÉíÍóÓúÚãÃõÕ ]+)(.*)$/, ); const type = (match?.[1] || token).trim().toLowerCase(); const value = (match?.[2] || "").trim(); let Icon: any = Shield; let chipBg = "bg-amber-500/10"; let chipText = "text-amber-700"; switch (type) { case "fire": case "fogo": Icon = Flame; chipBg = "bg-red-500/10"; chipText = "text-red-600"; break; case "ice": case "gelo": Icon = Snowflake; chipBg = "bg-blue-500/10"; chipText = "text-blue-600"; break; case "energy": case "energia": Icon = Energy; chipBg = "bg-yellow-500/10"; chipText = "text-yellow-600"; break; case "earth": case "terra": Icon = Leaf; chipBg = "bg-green-500/10"; chipText = "text-green-600"; break; case "death": case "morte": Icon = Skull; chipBg = "bg-zinc-500/10"; chipText = "text-zinc-600"; break; case "holy": case "sagrado": Icon = Cross; chipBg = "bg-amber-500/10"; chipText = "text-amber-700"; break; case "physical": case "fisico": Icon = Shield; chipBg = "bg-slate-500/10"; chipText = "text-slate-600"; break; default: Icon = Shield; chipBg = "bg-amber-500/10"; chipText = "text-amber-700"; } const prettyType = type.charAt(0).toUpperCase() + type.slice(1); const label = `${prettyType.replace("fisico", "Físico")}${value ? ` ${value}` : ""}`; return (
{label}
); })}
); })()} {item.stats.weight && (
{item.stats.weight} oz
)} {(item as any).stats?.slots && (
Slots {(item as any).stats?.slots}
)}
)} {(item as any).dropa_de && (() => { const raw = String((item as any).dropa_de || ""); const normalized = raw.trim(); // Casos especiais de "nenhum/ninguém" const none = /^(nenhum|nenhuma|ningu[eé]m)\.?$/i.test(normalized); if (none) { return (
Dropa de: Ninguém
); } const droppers = normalized .split(",") .map((s) => s.trim()) .filter(Boolean); return (
Dropa de:
{droppers.map((name, idx) => ( {name} ))}
); })()} {mode === "market" && item.price && ( <> {/* Divisor visual */}
{/* Preços com ícones e formatação melhorada */}
{t("buyPrice")}
{t("sellPrice")}
{/* Diferença percentual */}
0 ? "text-green-600" : "text-red-600" }`} > {priceDifference > 0 ? "+" : ""} {priceDifference.toFixed(1)}% diferença
)} {mode === "market" && item.priceHistory && ( <> {/* Divisor visual */}
{/* Histórico de preço com sparkline */}
{t("priceHistory")}
{trend}
{t("highest")}
{formatNumber(item.priceHistory.highest, locale)} gp
{t("lowest")}
{formatNumber(item.priceHistory.lowest, locale)} gp
{t("average")}
{formatNumber(item.priceHistory.average, locale)} gp
)} {mode === "market" && (
)}
); }