729 lines
27 KiB
TypeScript
729 lines
27 KiB
TypeScript
"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<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();
|
|
};
|
|
}, [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 (
|
|
<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">
|
|
{/* 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">
|
|
{formatNumber(item.priceHistory.highest, locale)} 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">
|
|
{formatNumber(item.priceHistory.lowest, locale)} 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">
|
|
{formatNumber(item.priceHistory.average, locale)} 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>
|
|
);
|
|
}
|