Added Tailwind CSS and tw-animate-css imports to globals.css. Introduced a custom dark variant and an extensive @theme inline block with CSS variables for colors, radii, and animations. Included keyframes for shimmer-slide, spin-around, gradient, shine, and background-position-spin to enable consistent theming and dynamic UI effects across the application.
247 lines
8.0 KiB
TypeScript
247 lines
8.0 KiB
TypeScript
"use client";
|
|
|
|
import { MarketOffer } from "@/types/item";
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
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 {
|
|
Clock,
|
|
User,
|
|
Coins,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Star,
|
|
Heart,
|
|
Shield,
|
|
Sword,
|
|
} from "lucide-react";
|
|
import { useState } from "react";
|
|
import { getRarityColor, getRarityBgColor } from "@/lib/currency-utils";
|
|
|
|
interface OfferCardProps {
|
|
offer: MarketOffer;
|
|
}
|
|
|
|
export function OfferCard({ offer }: OfferCardProps) {
|
|
const t = useTranslations("home.stats");
|
|
const tButtons = useTranslations("home.buttons");
|
|
const [isFavorited, setIsFavorited] = useState(false);
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
|
|
const formatTimeAgo = (timestamp: Date) => {
|
|
const now = new Date();
|
|
const diff = now.getTime() - timestamp.getTime();
|
|
const minutes = Math.floor(diff / (1000 * 60));
|
|
|
|
if (minutes < 1) return "Agora";
|
|
if (minutes < 60) return `há ${minutes} ${t("minutes")}`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return `há ${hours} ${t("hours")}`;
|
|
const days = Math.floor(hours / 24);
|
|
return `há ${days} dias`;
|
|
};
|
|
|
|
const getInitials = (name: string) => {
|
|
return name
|
|
.split(" ")
|
|
.map((word) => word[0])
|
|
.join("")
|
|
.toUpperCase()
|
|
.slice(0, 2);
|
|
};
|
|
|
|
const getCategoryIcon = (category: string) => {
|
|
switch (category) {
|
|
case "weapon":
|
|
return <Sword className="w-6 h-6" />;
|
|
case "armor":
|
|
return <Shield className="w-6 h-6" />;
|
|
default:
|
|
return "📦";
|
|
}
|
|
};
|
|
|
|
const getRarityColor = (rarity: string) => {
|
|
switch (rarity) {
|
|
case "common":
|
|
return "bg-gray-500";
|
|
case "uncommon":
|
|
return "bg-green-500";
|
|
case "rare":
|
|
return "bg-blue-500";
|
|
case "epic":
|
|
return "bg-purple-500";
|
|
case "legendary":
|
|
return "bg-orange-500";
|
|
default:
|
|
return "bg-gray-500";
|
|
}
|
|
};
|
|
|
|
const isHotDeal = offer.price < offer.item.priceHistory.average * 0.8;
|
|
const isNewOffer =
|
|
new Date().getTime() - offer.timestamp.getTime() < 1000 * 60 * 30; // 30 min
|
|
|
|
return (
|
|
<div
|
|
className="group relative flex items-center justify-between p-6 border border-border/50 rounded-xl hover:border-primary/30 hover:shadow-lg transition-all duration-300 hover:scale-[1.01] bg-background/50 backdrop-blur-sm"
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
{/* Badges de status */}
|
|
<div className="absolute top-3 left-3 flex gap-2">
|
|
{isNewOffer && (
|
|
<Badge className="bg-green-500 text-white text-xs animate-pulse">
|
|
⭐ {t("status.new")}
|
|
</Badge>
|
|
)}
|
|
{isHotDeal && (
|
|
<Badge className="bg-red-500 text-white text-xs animate-bounce">
|
|
🔥 {t("status.hotDeal")}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{/* Botão de favorito */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute top-3 right-3 h-8 w-8 bg-background/80 backdrop-blur-sm hover:bg-background opacity-0 group-hover:opacity-100 transition-opacity"
|
|
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>
|
|
|
|
<div className="flex items-center gap-6 flex-1">
|
|
{/* Ícone do item com raridade */}
|
|
<div className="relative">
|
|
<div className="w-16 h-16 bg-muted/50 rounded-xl flex items-center justify-center text-2xl backdrop-blur-sm">
|
|
{getCategoryIcon(offer.item.category)}
|
|
</div>
|
|
<Badge
|
|
className={`${getRarityColor(
|
|
offer.item.rarity
|
|
)} text-white absolute -top-2 -right-2 text-xs shadow-lg`}
|
|
>
|
|
{offer.item.rarity}
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* Informações do item */}
|
|
<div className="flex-1 space-y-2">
|
|
<div className="space-y-1">
|
|
<h3 className="font-bold text-lg text-foreground">
|
|
{offer.item.name}
|
|
</h3>
|
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
|
<div className="flex items-center gap-1">
|
|
<Avatar className="w-5 h-5">
|
|
<AvatarFallback className="text-xs">
|
|
{getInitials(offer.seller)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span className="font-medium">{offer.seller}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="w-4 h-4" />
|
|
<span>{formatTimeAgo(offer.timestamp)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Estatísticas do item */}
|
|
{offer.item.stats && (
|
|
<div className="flex flex-wrap gap-3 text-sm">
|
|
{offer.item.stats.attack && (
|
|
<div className="flex items-center gap-1 text-muted-foreground">
|
|
<Sword className="h-3 w-3" />
|
|
<span>{offer.item.stats.attack} ATK</span>
|
|
</div>
|
|
)}
|
|
{offer.item.stats.defense && (
|
|
<div className="flex items-center gap-1 text-muted-foreground">
|
|
<Shield className="h-3 w-3" />
|
|
<span>{offer.item.stats.defense} DEF</span>
|
|
</div>
|
|
)}
|
|
{offer.item.stats.armor && (
|
|
<div className="flex items-center gap-1 text-muted-foreground">
|
|
<Shield className="h-3 w-3" />
|
|
<span>{offer.item.stats.armor} ARM</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Preço e ações */}
|
|
<div className="flex items-center gap-6">
|
|
<div className="text-right space-y-2">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
|
<Coins className="h-4 w-4" />
|
|
<span>Preço</span>
|
|
</div>
|
|
<PriceDisplay
|
|
value={offer.price}
|
|
size="lg"
|
|
className="text-foreground"
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
{offer.item.stackable
|
|
? `Stack: ${offer.quantity}x`
|
|
: `Qtd: ${offer.quantity}`}
|
|
{offer.item.stackable && offer.quantity > 1 && (
|
|
<span className="block text-xs">
|
|
{Math.round(offer.price / offer.quantity).toLocaleString()}{" "}
|
|
gp cada
|
|
</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<Badge
|
|
variant={offer.type === "buy" ? "default" : "secondary"}
|
|
className={`${
|
|
offer.type === "buy"
|
|
? "bg-green-500 hover:bg-green-600"
|
|
: "bg-blue-500 hover:bg-blue-600"
|
|
} text-white transition-colors`}
|
|
>
|
|
{offer.type === "buy" ? "Comprando" : "Vendendo"}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Button
|
|
size="sm"
|
|
className={`w-full transition-all duration-200 ${
|
|
offer.type === "buy"
|
|
? "bg-green-600 hover:bg-green-700"
|
|
: "bg-blue-600 hover:bg-blue-700"
|
|
} text-white`}
|
|
>
|
|
{tButtons("negotiate")}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
{tButtons("viewProfile")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|