Files
pagodera/components/item-card.tsx
T
Evandro Fonseca Junior 44d7f8ce81 feat(item-card): add 'market'/'catalog' mode prop with defensive price checks
Introduces optional `mode` prop ("market"/"catalog") to conditionally render components like rarity badges. Adds defensive checks for missing price data and priceHistory properties to prevent runtime errors. New icons `Badge` and `Boxes` imported from lucide-react.
2025-10-30 14:10:46 -03:00

396 lines
14 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,
} from "lucide-react";
import { 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 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: "💎",
ring: "💍",
potion: "🧪",
rune: "📜",
tool: "🔧",
ammunition: "🏹",
other: "📦",
};
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 h-full"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<MagicCard className="p-0 overflow-hidden h-full flex flex-col 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">
🔥 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" ? (
CategoryIcon
) : (
<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>
</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>
)}
</div>
{/* Proteções elementais */}
{item.protections && (
<div className="flex flex-wrap gap-2">
{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>
)}
</div>
)}
{/* Requisitos */}
{item.requirements && (
<div className="flex flex-wrap gap-2">
{item.requirements.level && (
<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.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 {item.requirements.vocation}
</div>
)}
</div>
)}
{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>
)}
{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>
);
}