Added vocation state and included it in query string builder, updated onFilter to accept a vocation parameter, improved grid layout with grid flow classes, normalized category names for consistent item icons, and refined stats mapping to handle type safety.
716 lines
27 KiB
TypeScript
716 lines
27 KiB
TypeScript
"use client";
|
|
|
|
import { TibiaItem } from "@/types/item";
|
|
import { MagicCard } from "@/components/ui/magic-card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { PriceDisplay } from "@/components/ui/price-display";
|
|
import { useTranslations } from "next-intl";
|
|
import {
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Sword,
|
|
Shield,
|
|
Weight,
|
|
Heart,
|
|
Star,
|
|
Clock,
|
|
Coins,
|
|
ArrowUp,
|
|
ArrowDown,
|
|
Minus,
|
|
Zap,
|
|
Flame,
|
|
Snowflake,
|
|
Zap as Energy,
|
|
Skull,
|
|
Cross,
|
|
Badge as BadgeIcon,
|
|
Boxes,
|
|
Gem,
|
|
FlaskConical,
|
|
Wrench,
|
|
ScrollText,
|
|
Leaf,
|
|
} from "lucide-react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import {
|
|
getRarityColor,
|
|
getRarityBgColor,
|
|
getRarityGlow,
|
|
} from "@/lib/currency-utils";
|
|
|
|
interface ItemCardProps {
|
|
item: TibiaItem;
|
|
mode?: "market" | "catalog";
|
|
}
|
|
|
|
export function ItemCard({ item, mode = "market" }: ItemCardProps) {
|
|
const t = useTranslations("home.item");
|
|
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<HTMLDivElement | null>(null);
|
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
const [isHovering, setIsHovering] = useState(false);
|
|
const autoScrollRef = useRef<number | null>(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();
|
|
};
|
|
}, []);
|
|
|
|
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 (
|
|
<div className="relative">
|
|
{/* botões removidos conforme solicitado */}
|
|
<div
|
|
ref={scrollerRef}
|
|
className={`overflow-x-auto no-scrollbar whitespace-nowrap pr-6 pl-6 select-none ${isDragging ? "cursor-grabbing" : "cursor-grab"}`}
|
|
style={{ scrollBehavior: "smooth" }}
|
|
>
|
|
<div ref={contentRef} className="inline-flex items-center gap-2">
|
|
{children}
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<div
|
|
className="group relative"
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
<MagicCard className="p-0 overflow-hidden rounded-xl border border-border/50 hover:border-primary/30 transition-all duration-300 hover:scale-[1.02] hover:shadow-xl">
|
|
{/* Header com ícone e badge de raridade */}
|
|
<div className="relative aspect-square bg-linear-to-br from-background to-muted p-6">
|
|
{/* Badge de raridade com glow (apenas em modo market) */}
|
|
{mode === "market" && (
|
|
<Badge
|
|
className={`${
|
|
rarityColors[item.rarity]
|
|
} text-white absolute top-3 left-3 z-20 shadow-lg ${getRarityGlow(
|
|
item.rarity
|
|
)} ${item.rarity === "legendary" ? "animate-pulse" : ""} ${
|
|
item.rarity === "epic" ? "animate-pulse" : ""
|
|
}`}
|
|
>
|
|
{item.rarity}
|
|
</Badge>
|
|
)}
|
|
|
|
{/* Botão de favorito */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute top-3 right-3 z-20 h-8 w-8 bg-background/80 backdrop-blur-sm hover:bg-background"
|
|
onClick={() => setIsFavorited(!isFavorited)}
|
|
>
|
|
<Heart
|
|
className={`h-4 w-4 transition-colors ${
|
|
isFavorited
|
|
? "fill-red-500 text-red-500"
|
|
: "text-muted-foreground hover:text-red-500"
|
|
}`}
|
|
/>
|
|
</Button>
|
|
|
|
{/* Hot Deal Badge (apenas em modo market) */}
|
|
{mode === "market" && isHotDeal && (
|
|
<Badge className="absolute top-3 left-1/2 transform -translate-x-1/2 z-20 bg-red-500 text-white shadow-lg animate-pulse flex items-center gap-1">
|
|
<Flame className="h-3 w-3" />
|
|
Hot Deal
|
|
</Badge>
|
|
)}
|
|
|
|
{/* Imagem real do item (se disponível) ou ícone por categoria */}
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
{item.image ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={item.image}
|
|
alt={item.name}
|
|
className="w-20 h-20 object-contain drop-shadow-sm"
|
|
onError={(e) => {
|
|
(e.currentTarget as HTMLImageElement).style.display = "none";
|
|
}}
|
|
/>
|
|
) : (
|
|
<div className="w-20 h-20 bg-muted/50 rounded-xl flex items-center justify-center text-3xl backdrop-blur-sm">
|
|
{typeof CategoryIcon === "string" ? (
|
|
null
|
|
) : (
|
|
<CategoryIcon className="w-10 h-10" />
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Conteúdo do card */}
|
|
<div className="p-6 space-y-4 flex-1 flex flex-col">
|
|
{/* Nome do item */}
|
|
<div className="space-y-2">
|
|
<h3 className="font-bold text-xl text-foreground leading-tight">
|
|
{item.name}
|
|
</h3>
|
|
|
|
{/* Timestamp */}
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<Clock className="h-3 w-3" />
|
|
<span>Atualizado há 5 min</span>
|
|
</div>
|
|
|
|
{/* Meta: ID e Categoria */}
|
|
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
{(() => {
|
|
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 ? (
|
|
<span className="px-1.5 py-0.5 rounded bg-muted/60 text-foreground/80">
|
|
{display}
|
|
</span>
|
|
) : null;
|
|
})()}
|
|
{typeof item.id === "string" && item.id && (
|
|
<span className="opacity-80">ID: {item.id}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Estatísticas agrupadas */}
|
|
{item.stats && (
|
|
<div className="space-y-2">
|
|
<div className="flex flex-wrap gap-3">
|
|
{item.stats.attack && (
|
|
<div className="flex items-center gap-1.5 text-sm">
|
|
<Sword className="h-4 w-4 text-primary" />
|
|
<span className="font-medium">
|
|
{item.stats.attack} ATK
|
|
</span>
|
|
</div>
|
|
)}
|
|
{item.stats.defense && (
|
|
<div className="flex items-center gap-1.5 text-sm">
|
|
<Shield className="h-4 w-4 text-primary" />
|
|
<span className="font-medium">
|
|
{item.stats.defense} DEF
|
|
</span>
|
|
</div>
|
|
)}
|
|
{item.stats.armor && (
|
|
<div className="flex items-center gap-1.5 text-sm">
|
|
<Shield className="h-4 w-4 text-primary" />
|
|
<span className="font-medium">
|
|
{item.stats.armor} ARM
|
|
</span>
|
|
</div>
|
|
)}
|
|
{(item as any).volume && (
|
|
<div className="flex items-center gap-1.5 text-sm">
|
|
<Boxes className="h-4 w-4 text-primary" />
|
|
<span className="font-medium">Volume {(item as any).volume}</span>
|
|
</div>
|
|
)}
|
|
{(item as any).bonus_de_skill && (
|
|
<div className="flex items-center gap-1.5 text-sm">
|
|
<Zap className="h-4 w-4 text-primary" />
|
|
<span className="font-medium">Skill +{(item as any).bonus_de_skill}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Proteções elementais */}
|
|
{item.protections && (
|
|
<ScrollRow ariaLabel="protections">
|
|
{item.protections.fire && (
|
|
<div className="flex items-center gap-1 text-xs bg-red-500/10 text-red-600 px-2 py-1 rounded">
|
|
<Flame className="h-3 w-3" />
|
|
<span>Fire +{item.protections.fire}%</span>
|
|
</div>
|
|
)}
|
|
{item.protections.ice && (
|
|
<div className="flex items-center gap-1 text-xs bg-blue-500/10 text-blue-600 px-2 py-1 rounded">
|
|
<Snowflake className="h-3 w-3" />
|
|
<span>Ice +{item.protections.ice}%</span>
|
|
</div>
|
|
)}
|
|
{item.protections.energy && (
|
|
<div className="flex items-center gap-1 text-xs bg-yellow-500/10 text-yellow-600 px-2 py-1 rounded">
|
|
<Energy className="h-3 w-3" />
|
|
<span>Energy +{item.protections.energy}%</span>
|
|
</div>
|
|
)}
|
|
</ScrollRow>
|
|
)}
|
|
|
|
{/* Requisitos */}
|
|
{item.requirements && (
|
|
<div className="flex flex-wrap gap-2">
|
|
{typeof (item as any).requirements?.level === "number" && (
|
|
<div className="flex items-center gap-1 text-xs bg-primary/10 text-primary px-2 py-1 rounded">
|
|
<Star className="h-3 w-3" />
|
|
<span>Lvl {(item as any).requirements.level}+</span>
|
|
</div>
|
|
)}
|
|
{(item as any).requirements?.skill && (
|
|
<div className="flex items-center gap-1 text-xs bg-indigo-500/10 text-indigo-600 px-2 py-1 rounded">
|
|
<Zap className="h-3 w-3" />
|
|
<span>{(item as any).requirements.skill}</span>
|
|
</div>
|
|
)}
|
|
{item.requirements.vocation && (
|
|
<div className="text-xs text-muted-foreground">
|
|
Best for {formatVocation(item.requirements.vocation)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{(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 (
|
|
<ScrollRow ariaLabel="elemental protections">
|
|
{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 (
|
|
<div key={`${type}-${idx}`} className={`flex items-center gap-1 text-xs ${chipBg} ${chipText} px-2 py-1 rounded`}>
|
|
<Icon className="h-3 w-3" />
|
|
<span className="whitespace-nowrap">{label}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</ScrollRow>
|
|
);
|
|
})()}
|
|
|
|
{item.stats.weight && (
|
|
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
<Weight className="h-4 w-4" />
|
|
<span>{item.stats.weight} oz</span>
|
|
</div>
|
|
)}
|
|
|
|
{(item as any).stats?.slots && (
|
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
<Boxes className="h-3 w-3" />
|
|
<span>Slots {(item as any).stats?.slots}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{(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 (
|
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
<Skull className="h-3 w-3" />
|
|
<span>Dropa de: Ninguém</span>
|
|
</div>
|
|
);
|
|
}
|
|
const droppers = normalized
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
return (
|
|
<div className="text-xs">
|
|
<div className="flex items-center gap-1.5 text-muted-foreground mb-1">
|
|
<Skull className="h-3 w-3" />
|
|
<span>Dropa de:</span>
|
|
</div>
|
|
<ScrollRow ariaLabel="droppers">
|
|
{droppers.map((name, idx) => (
|
|
<span
|
|
key={`dropper-${idx}-${name}`}
|
|
className="px-2 py-0.5 rounded bg-muted/60 text-foreground/80 whitespace-nowrap"
|
|
>
|
|
{name}
|
|
</span>
|
|
))}
|
|
</ScrollRow>
|
|
</div>
|
|
);
|
|
})()
|
|
)}
|
|
|
|
{mode === "market" && item.price && (
|
|
<>
|
|
{/* Divisor visual */}
|
|
<div className="border-t border-border/50" />
|
|
|
|
{/* Preços com ícones e formatação melhorada */}
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<Coins className="h-3 w-3" />
|
|
<span>{t("buyPrice")}</span>
|
|
</div>
|
|
<PriceDisplay
|
|
value={item.price.buy}
|
|
className="text-green-600 dark:text-green-400"
|
|
size="md"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<Coins className="h-3 w-3" />
|
|
<span>{t("sellPrice")}</span>
|
|
</div>
|
|
<PriceDisplay
|
|
value={item.price.sell}
|
|
className="text-red-600 dark:text-red-400"
|
|
size="md"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Diferença percentual */}
|
|
<div className="text-center">
|
|
<span
|
|
className={`text-sm font-medium ${
|
|
priceDifference > 0 ? "text-green-600" : "text-red-600"
|
|
}`}
|
|
>
|
|
{priceDifference > 0 ? "+" : ""}
|
|
{priceDifference.toFixed(1)}% diferença
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{mode === "market" && item.priceHistory && (
|
|
<>
|
|
{/* Divisor visual */}
|
|
<div className="border-t border-border/50" />
|
|
|
|
{/* Histórico de preço com sparkline */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium text-muted-foreground">
|
|
{t("priceHistory")}
|
|
</span>
|
|
<div className={`flex items-center gap-1 text-xs ${trendColor}`}>
|
|
<TrendIcon className="h-3 w-3" />
|
|
<span className="capitalize">{trend}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
|
<div className="text-center space-y-1">
|
|
<div className="flex items-center justify-center gap-1 text-green-600">
|
|
<TrendingUp className="h-3 w-3" />
|
|
<span className="font-medium">{t("highest")}</span>
|
|
</div>
|
|
<div className="font-bold">{item.priceHistory.highest.toLocaleString()} gp</div>
|
|
</div>
|
|
<div className="text-center space-y-1">
|
|
<div className="flex items-center justify-center gap-1 text-red-600">
|
|
<TrendingDown className="h-3 w-3" />
|
|
<span className="font-medium">{t("lowest")}</span>
|
|
</div>
|
|
<div className="font-bold">{item.priceHistory.lowest.toLocaleString()} gp</div>
|
|
</div>
|
|
<div className="text-center space-y-1">
|
|
<div className="flex items-center justify-center gap-1 text-muted-foreground">
|
|
<Minus className="h-3 w-3" />
|
|
<span className="font-medium">{t("average")}</span>
|
|
</div>
|
|
<div className="font-bold">{item.priceHistory.average.toLocaleString()} gp</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{mode === "market" && (
|
|
<div className="pt-4 space-y-2">
|
|
<Button
|
|
className="w-full group-hover:bg-primary group-hover:text-primary-foreground transition-colors"
|
|
variant="outline"
|
|
>
|
|
{t("viewDetails")}
|
|
</Button>
|
|
<Button className="w-full bg-green-600 hover:bg-green-700 text-white" size="sm">
|
|
{t("buyNow")}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</MagicCard>
|
|
</div>
|
|
);
|
|
}
|