feat: integrate Tailwind CSS with custom theme and animations
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.
This commit is contained in:
1 parent
55c169bd6e
commit
5c1ccbc69a
67 files changed
+8984
-160
No files matched your search
@@ -0,0 +1,209 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { NumberTicker } from "@/components/ui/number-ticker";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
ShoppingCart,
|
||||
TrendingUp,
|
||||
Users,
|
||||
DollarSign,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
} from "lucide-react";
|
||||
import { AreaChart, Area, ResponsiveContainer } from "recharts";
|
||||
import { buyOffers, sellOffers } from "@/lib/mock-data";
|
||||
|
||||
interface StatData {
|
||||
icon: typeof ShoppingCart;
|
||||
value: number;
|
||||
label: string;
|
||||
change: number; // percentual
|
||||
trend: "up" | "down";
|
||||
sparklineData: number[]; // 7 pontos
|
||||
iconColor: string;
|
||||
iconBg: string;
|
||||
description: string;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
// Componente Mini Sparkline
|
||||
function MiniSparkline({ data, color }: { data: number[]; color: string }) {
|
||||
const chartData = data.map((value, index) => ({
|
||||
value,
|
||||
index,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="h-8 w-full min-h-[32px] min-w-[120px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
margin={{ top: 2, right: 2, left: 2, bottom: 2 }}
|
||||
width={120}
|
||||
height={32}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`gradient-${color}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.3} />
|
||||
<stop offset="100%" stopColor={color} stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
fill={`url(#gradient-${color})`}
|
||||
dot={false}
|
||||
activeDot={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Componente StatCard individual
|
||||
function StatCard({ stat }: { stat: StatData }) {
|
||||
const TrendIcon = stat.trend === "up" ? ArrowUp : ArrowDown;
|
||||
const isPositive = stat.trend === "up";
|
||||
|
||||
return (
|
||||
<div className="group relative p-8 text-center rounded-xl border border-border/50 bg-linear-to-br from-card via-card/95 to-muted/30 backdrop-blur-lg transition-all duration-300 ease-out hover:scale-105 hover:shadow-2xl hover:border-accent/50 hover:shadow-accent/20">
|
||||
{/* Ícone com círculo colorido */}
|
||||
<div
|
||||
className={`w-12 h-12 mx-auto mb-4 rounded-full ${stat.iconBg} flex items-center justify-center group-hover:animate-pulse`}
|
||||
>
|
||||
<stat.icon className={`w-6 h-6 ${stat.iconColor}`} />
|
||||
</div>
|
||||
|
||||
{/* Número principal */}
|
||||
<div className="mb-2">
|
||||
<p className="text-6xl font-extrabold font-mono bg-linear-to-br from-foreground to-foreground/70 bg-clip-text">
|
||||
<NumberTicker value={stat.value} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Badge de variação */}
|
||||
<div className="mb-3">
|
||||
<Badge
|
||||
variant={isPositive ? "default" : "destructive"}
|
||||
className={`${
|
||||
isPositive
|
||||
? "bg-green-500/10 text-green-600 border-green-500/20"
|
||||
: "bg-red-500/10 text-red-600 border-red-500/20"
|
||||
} gap-1`}
|
||||
>
|
||||
<TrendIcon className="w-3 h-3" />
|
||||
{stat.change}%
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Label e descrição */}
|
||||
<div className="mb-4">
|
||||
<p className="text-lg font-semibold text-foreground">{stat.label}</p>
|
||||
<p className="text-sm text-muted-foreground">{stat.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Mini Sparkline */}
|
||||
<div className="mb-3">
|
||||
<MiniSparkline
|
||||
data={stat.sparklineData}
|
||||
color={stat.iconColor.replace("text-", "").replace("-500", "")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Timestamp */}
|
||||
<p className="text-xs text-muted-foreground">{stat.lastUpdated}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MarketStats() {
|
||||
const t = useTranslations("home.stats");
|
||||
|
||||
// Calcular volume total baseado nos dados mock
|
||||
const calculateTotalVolume = () => {
|
||||
const buyVolume = buyOffers.reduce(
|
||||
(sum, offer) => sum + offer.price * offer.quantity,
|
||||
0
|
||||
);
|
||||
const sellVolume = sellOffers.reduce(
|
||||
(sum, offer) => sum + offer.price * offer.quantity,
|
||||
0
|
||||
);
|
||||
return Math.round((buyVolume + sellVolume) / 1000); // Dividir por 1000 para número mais legível
|
||||
};
|
||||
|
||||
const stats: StatData[] = [
|
||||
{
|
||||
icon: ShoppingCart,
|
||||
value: 1542,
|
||||
label: t("activeOffers"),
|
||||
change: 8.5,
|
||||
trend: "up",
|
||||
sparklineData: [1200, 1300, 1400, 1350, 1450, 1500, 1542],
|
||||
iconColor: "text-orange-500",
|
||||
iconBg: "bg-orange-500/10",
|
||||
description: "esta semana",
|
||||
lastUpdated: `${t("updated")} 5 ${t("minutes")}`,
|
||||
},
|
||||
{
|
||||
icon: TrendingUp,
|
||||
value: 8763,
|
||||
label: t("itemsSold"),
|
||||
change: 15.3,
|
||||
trend: "up",
|
||||
sparklineData: [7200, 7500, 7800, 8000, 8200, 8500, 8763],
|
||||
iconColor: "text-green-500",
|
||||
iconBg: "bg-green-500/10",
|
||||
description: `${t("averagePerDay")}: 292 ${t("salesPerDay")}`,
|
||||
lastUpdated: `${t("updated")} 5 ${t("minutes")}`,
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
value: 3421,
|
||||
label: t("users"),
|
||||
change: 22.1,
|
||||
trend: "up",
|
||||
sparklineData: [3100, 3200, 3250, 3300, 3350, 3400, 3421],
|
||||
iconColor: "text-blue-500",
|
||||
iconBg: "bg-blue-500/10",
|
||||
description: `${t("onlineNow")}: 847 ${t("traders")}`,
|
||||
lastUpdated: `${t("updated")} 2 ${t("minutes")}`,
|
||||
},
|
||||
{
|
||||
icon: DollarSign,
|
||||
value: 208,
|
||||
label: t("totalVolume"),
|
||||
change: 18.7,
|
||||
trend: "up",
|
||||
sparklineData: [180, 200, 220, 240, 260, 280, 300],
|
||||
iconColor: "text-purple-500",
|
||||
iconBg: "bg-purple-500/10",
|
||||
description: "em gold pieces",
|
||||
lastUpdated: `${t("today")}: 12.5M gp`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-muted/30">
|
||||
<div className="container">
|
||||
<h2 className="text-4xl font-bold text-center mb-12">{t("title")}</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{stats.map((stat, index) => (
|
||||
<StatCard key={index} stat={stat} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user