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,40 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Shield, Coins } from "lucide-react";
|
||||
|
||||
export function EscrowBanner() {
|
||||
const t = useTranslations("home.escrow");
|
||||
|
||||
return (
|
||||
<section className="py-8 bg-gradient-to-r from-green-500/10 via-blue-500/10 to-green-500/10 border-b border-green-500/20">
|
||||
<div className="container">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-green-500/20 rounded-full flex items-center justify-center">
|
||||
<Shield className="w-6 h-6 text-green-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground">
|
||||
{t("title")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground">{t("description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-muted-foreground">Escrow Balance</p>
|
||||
<p className="text-lg font-bold text-foreground">🪙 0 gp</p>
|
||||
</div>
|
||||
<Button className="bg-green-600 hover:bg-green-700 text-white">
|
||||
<Coins className="w-4 h-4 mr-2" />
|
||||
{t("deposit")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { NumberTicker } from "@/components/ui/number-ticker";
|
||||
import {
|
||||
Wallet,
|
||||
Handshake,
|
||||
CheckCircle,
|
||||
Shield,
|
||||
Zap,
|
||||
BarChart,
|
||||
Lock,
|
||||
} from "lucide-react";
|
||||
|
||||
export function EscrowExplanation() {
|
||||
const t = useTranslations("home.escrowExplanation");
|
||||
|
||||
const benefits = [
|
||||
{
|
||||
icon: Shield,
|
||||
title: t("benefits.protection.title"),
|
||||
description: t("benefits.protection.description"),
|
||||
color: "text-green-500",
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: t("benefits.fast.title"),
|
||||
description: t("benefits.fast.description"),
|
||||
color: "text-blue-500",
|
||||
},
|
||||
{
|
||||
icon: BarChart,
|
||||
title: t("benefits.history.title"),
|
||||
description: t("benefits.history.description"),
|
||||
color: "text-purple-500",
|
||||
},
|
||||
{
|
||||
icon: Lock,
|
||||
title: t("benefits.trusted.title"),
|
||||
description: t("benefits.trusted.description"),
|
||||
color: "text-orange-500",
|
||||
},
|
||||
];
|
||||
|
||||
const faqItems = [
|
||||
{
|
||||
question: t("faq.q1"),
|
||||
answer: t("faq.a1"),
|
||||
},
|
||||
{
|
||||
question: t("faq.q2"),
|
||||
answer: t("faq.a2"),
|
||||
},
|
||||
{
|
||||
question: t("faq.q3"),
|
||||
answer: t("faq.a3"),
|
||||
},
|
||||
{
|
||||
question: t("faq.q4"),
|
||||
answer: t("faq.a4"),
|
||||
},
|
||||
{
|
||||
question: t("faq.q5"),
|
||||
answer: t("faq.a5"),
|
||||
},
|
||||
];
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: 208,
|
||||
suffix: "M",
|
||||
label: t("stats.volume"),
|
||||
},
|
||||
{
|
||||
value: 8763,
|
||||
suffix: "",
|
||||
label: t("stats.transactions"),
|
||||
},
|
||||
{
|
||||
value: 99.8,
|
||||
suffix: "%",
|
||||
label: t("stats.satisfaction"),
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
suffix: "min",
|
||||
label: t("stats.avgTime"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-muted/30">
|
||||
<div className="container">
|
||||
{/* Título e Subtítulo */}
|
||||
<BlurFade delay={0.1}>
|
||||
<h2 className="text-5xl font-bold text-center mb-4">{t("title")}</h2>
|
||||
<p className="text-xl text-center text-muted-foreground mb-16">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</BlurFade>
|
||||
|
||||
{/* Cards de Etapas */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-20">
|
||||
{/* Card 1: Depósito */}
|
||||
<BlurFade delay={0.2}>
|
||||
<div className="relative p-8 rounded-2xl border border-border/50 bg-card backdrop-blur-lg hover:border-orange-500/30 hover:scale-105 transition-all duration-300">
|
||||
<div className="absolute -top-4 -left-4 w-12 h-12 bg-orange-500/80 rounded-full flex items-center justify-center text-2xl font-bold text-white shadow-lg">
|
||||
1
|
||||
</div>
|
||||
<Wallet className="w-16 h-16 text-orange-500 mb-4" />
|
||||
<h3 className="text-2xl font-bold mb-3">{t("step1.title")}</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{t("step1.description")}
|
||||
</p>
|
||||
<div className="bg-muted/50 p-3 rounded-lg border border-border/30">
|
||||
<code className="text-sm text-foreground/80">
|
||||
{t("step1.example")}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
{/* Card 2: Negociação */}
|
||||
<BlurFade delay={0.3}>
|
||||
<div className="relative p-8 rounded-2xl border border-border/50 bg-card backdrop-blur-lg hover:border-blue-500/30 hover:scale-105 transition-all duration-300">
|
||||
<div className="absolute -top-4 -left-4 w-12 h-12 bg-blue-500/80 rounded-full flex items-center justify-center text-2xl font-bold text-white shadow-lg">
|
||||
2
|
||||
</div>
|
||||
<Handshake className="w-16 h-16 text-blue-500 mb-4" />
|
||||
<h3 className="text-2xl font-bold mb-3">{t("step2.title")}</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{t("step2.description")}
|
||||
</p>
|
||||
<div className="bg-muted/50 p-3 rounded-lg border border-border/30">
|
||||
<code className="text-sm text-foreground/80">
|
||||
{t("step2.example")}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
{/* Card 3: Conclusão */}
|
||||
<BlurFade delay={0.4}>
|
||||
<div className="relative p-8 rounded-2xl border border-border/50 bg-card backdrop-blur-lg hover:border-green-500/30 hover:scale-105 transition-all duration-300">
|
||||
<div className="absolute -top-4 -left-4 w-12 h-12 bg-green-500/80 rounded-full flex items-center justify-center text-2xl font-bold text-white shadow-lg">
|
||||
3
|
||||
</div>
|
||||
<CheckCircle className="w-16 h-16 text-green-500 mb-4" />
|
||||
<h3 className="text-2xl font-bold mb-3">{t("step3.title")}</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{t("step3.description")}
|
||||
</p>
|
||||
<div className="bg-muted/50 p-3 rounded-lg border border-border/30">
|
||||
<code className="text-sm text-foreground/80">
|
||||
{t("step3.example")}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
|
||||
{/* Grid de Benefícios */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-20">
|
||||
{benefits.map((benefit, idx) => (
|
||||
<BlurFade key={idx} delay={0.5 + idx * 0.1}>
|
||||
<div className="p-6 rounded-xl border border-border/50 bg-card backdrop-blur-sm hover:border-accent/50 hover:shadow-lg transition-all">
|
||||
<benefit.icon className={`w-12 h-12 mb-4 ${benefit.color}`} />
|
||||
<h4 className="text-xl font-semibold mb-2">{benefit.title}</h4>
|
||||
<p className="text-muted-foreground">{benefit.description}</p>
|
||||
</div>
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* FAQ */}
|
||||
<BlurFade delay={0.9}>
|
||||
<h3 className="text-3xl font-bold text-center mb-8">
|
||||
{t("faq.title")}
|
||||
</h3>
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="max-w-3xl mx-auto mb-20"
|
||||
>
|
||||
{faqItems.map((item, idx) => (
|
||||
<AccordionItem key={idx} value={`item-${idx}`}>
|
||||
<AccordionTrigger className="text-left text-lg font-semibold">
|
||||
{item.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground">
|
||||
{item.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</BlurFade>
|
||||
|
||||
{/* CTA Section */}
|
||||
<BlurFade delay={1.0}>
|
||||
<div className="relative p-12 rounded-2xl bg-card border border-border backdrop-blur-sm text-center">
|
||||
<Badge className="mb-4 bg-accent/20 text-accent-foreground">
|
||||
✨ Primeiras 3 transações sem taxa!
|
||||
</Badge>
|
||||
<h3 className="text-3xl font-bold mb-6">{t("cta.title")}</h3>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button size="lg" className="bg-primary hover:bg-primary/90">
|
||||
{t("cta.createOffer")}
|
||||
</Button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="border-border/50"
|
||||
>
|
||||
{t("cta.watchVideo")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<div className="aspect-video bg-muted rounded-lg flex items-center justify-center">
|
||||
<p className="text-muted-foreground">
|
||||
Vídeo explicativo aqui
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
{/* Barra de Estatísticas */}
|
||||
<div className="mt-16 grid grid-cols-2 md:grid-cols-4 gap-8 text-center">
|
||||
{stats.map((stat, idx) => (
|
||||
<BlurFade key={idx} delay={1.1 + idx * 0.1}>
|
||||
<div>
|
||||
<p className="text-3xl font-bold text-accent mb-2">
|
||||
<NumberTicker value={stat.value} />
|
||||
{stat.suffix}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{stat.label}</p>
|
||||
</div>
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ItemCard } from "@/components/item-card";
|
||||
import { MarketplaceFilters } from "@/components/marketplace-filters";
|
||||
import { featuredItems } from "@/lib/mock-data";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export function FeaturedItems() {
|
||||
const t = useTranslations("home.featured");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filters, setFilters] = useState<{
|
||||
rarity: string[];
|
||||
category: string[];
|
||||
priceRange: { min: number; max: number };
|
||||
}>({
|
||||
rarity: [],
|
||||
category: [],
|
||||
priceRange: { min: 0, max: 100000 },
|
||||
});
|
||||
const [sortBy, setSortBy] = useState("name");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Filtrar e ordenar itens
|
||||
const filteredItems = useMemo(() => {
|
||||
let filtered = [...featuredItems];
|
||||
|
||||
// Filtro por busca
|
||||
if (searchQuery) {
|
||||
filtered = filtered.filter((item) =>
|
||||
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// Filtro por raridade
|
||||
if (filters.rarity.length > 0) {
|
||||
filtered = filtered.filter((item) =>
|
||||
filters.rarity.includes(item.rarity)
|
||||
);
|
||||
}
|
||||
|
||||
// Filtro por categoria
|
||||
if (filters.category.length > 0) {
|
||||
filtered = filtered.filter((item) =>
|
||||
filters.category.includes(item.category)
|
||||
);
|
||||
}
|
||||
|
||||
// Filtro por faixa de preço
|
||||
filtered = filtered.filter(
|
||||
(item) =>
|
||||
item.price.buy >= filters.priceRange.min &&
|
||||
item.price.buy <= filters.priceRange.max
|
||||
);
|
||||
|
||||
// Ordenação
|
||||
filtered.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case "name":
|
||||
return a.name.localeCompare(b.name);
|
||||
case "price-asc":
|
||||
return a.price.buy - b.price.buy;
|
||||
case "price-desc":
|
||||
return b.price.buy - a.price.buy;
|
||||
case "rarity":
|
||||
const rarityOrder = {
|
||||
common: 0,
|
||||
uncommon: 1,
|
||||
rare: 2,
|
||||
epic: 3,
|
||||
legendary: 4,
|
||||
};
|
||||
return rarityOrder[a.rarity] - rarityOrder[b.rarity];
|
||||
case "trending":
|
||||
return b.priceHistory.highest - a.priceHistory.highest;
|
||||
case "recent":
|
||||
// Simular ordenação por data (mais recente primeiro)
|
||||
return Math.random() - 0.5;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}, [searchQuery, filters, sortBy]);
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
setSearchQuery(query);
|
||||
};
|
||||
|
||||
const handleFilter = (newFilters: any) => {
|
||||
setFilters(newFilters);
|
||||
};
|
||||
|
||||
const handleSort = (sortValue: string) => {
|
||||
setSortBy(sortValue);
|
||||
};
|
||||
|
||||
// Skeleton para loading
|
||||
const LoadingSkeleton = () => (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={index} className="space-y-4">
|
||||
<Skeleton className="h-48 w-full rounded-xl" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-6 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<Skeleton className="h-4 w-1/3" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-muted/30">
|
||||
<div className="container">
|
||||
<BlurFade delay={0.2}>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-bold mb-4">{t("title")}</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Descubra os melhores itens do servidor com preços atualizados em
|
||||
tempo real
|
||||
</p>
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
{/* Filtros e busca */}
|
||||
<div className="mb-12">
|
||||
<MarketplaceFilters
|
||||
onSearch={handleSearch}
|
||||
onFilter={handleFilter}
|
||||
onSort={handleSort}
|
||||
totalItems={filteredItems.length}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Grid de itens */}
|
||||
{isLoading ? (
|
||||
<LoadingSkeleton />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{filteredItems.map((item, index) => (
|
||||
<BlurFade key={item.id} delay={0.2 + index * 0.05}>
|
||||
<ItemCard item={item} />
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mensagem quando não há resultados */}
|
||||
{filteredItems.length === 0 && !isLoading && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🔍</div>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
Nenhum item encontrado
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Tente ajustar os filtros ou fazer uma nova busca
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
setFilters({
|
||||
rarity: [],
|
||||
category: [],
|
||||
priceRange: { min: 0, max: 100000 },
|
||||
});
|
||||
setSortBy("name");
|
||||
}}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Limpar todos os filtros
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { AnimatedGradientText } from "@/components/ui/animated-gradient-text";
|
||||
import { ShimmerButton } from "@/components/ui/shimmer-button";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import Link from "next/link";
|
||||
|
||||
export function HeroSection() {
|
||||
const t = useTranslations("home.hero");
|
||||
|
||||
return (
|
||||
<section className="relative min-h-[600px] flex items-center justify-center overflow-hidden bg-gradient-to-br from-background to-muted/20 px-4">
|
||||
<div className="w-full max-w-4xl mx-auto flex flex-col items-center justify-center text-center space-y-8">
|
||||
<BlurFade delay={0.2}>
|
||||
<AnimatedGradientText className="text-5xl md:text-7xl font-bold">
|
||||
{t("title")}
|
||||
</AnimatedGradientText>
|
||||
</BlurFade>
|
||||
|
||||
<BlurFade delay={0.4}>
|
||||
<p className="text-xl md:text-2xl text-muted-foreground max-w-2xl mx-auto">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</BlurFade>
|
||||
|
||||
<BlurFade delay={0.6}>
|
||||
<div className="flex justify-center">
|
||||
<Link href="/market">
|
||||
<ShimmerButton className="px-8 py-6 text-lg block">
|
||||
{t("cta")}
|
||||
</ShimmerButton>
|
||||
</Link>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { BentoGrid, BentoCard } from "@/components/ui/bento-grid";
|
||||
import { ShoppingBag, Coins, Shield } from "lucide-react";
|
||||
|
||||
export function HowItWorks() {
|
||||
const t = useTranslations("home.howItWorks");
|
||||
|
||||
const features = [
|
||||
{
|
||||
name: t("buy"),
|
||||
description: "Encontre os melhores itens com preços justos",
|
||||
className: "md:col-span-1",
|
||||
background: (
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-green-500/10 via-green-500/5 to-transparent" />
|
||||
),
|
||||
Icon: ShoppingBag,
|
||||
href: "/market",
|
||||
cta: "Ver Mercado",
|
||||
},
|
||||
{
|
||||
name: t("sell"),
|
||||
description: "Venda seu loot e ganhe gold rapidamente",
|
||||
className: "md:col-span-1",
|
||||
background: (
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-amber-500/10 via-amber-500/5 to-transparent" />
|
||||
),
|
||||
Icon: Coins,
|
||||
href: "/market",
|
||||
cta: "Vender Agora",
|
||||
},
|
||||
{
|
||||
name: t("trade"),
|
||||
description: "Sistema seguro de escrow protege suas transações",
|
||||
className: "md:col-span-2",
|
||||
background: (
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 via-blue-500/5 to-transparent" />
|
||||
),
|
||||
Icon: Shield,
|
||||
href: "/market",
|
||||
cta: "Negociar",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-background">
|
||||
<div className="container">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-bold mb-4">{t("title")}</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Descubra como funciona nossa plataforma de trading segura e
|
||||
eficiente
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BentoGrid>
|
||||
{features.map((feature, idx) => (
|
||||
<BentoCard key={idx} {...feature} />
|
||||
))}
|
||||
</BentoGrid>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { AnimatedList } from "@/components/ui/animated-list";
|
||||
import { OfferCard } from "@/components/offer-card";
|
||||
import { buyOffers, sellOffers } from "@/lib/mock-data";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
export function MarketOffers() {
|
||||
const t = useTranslations("home.offers");
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-background">
|
||||
<div className="container">
|
||||
<h2 className="text-4xl font-bold text-center mb-12">
|
||||
Ofertas do Mercado
|
||||
</h2>
|
||||
|
||||
<Tabs defaultValue="buying" className="w-full">
|
||||
<TabsList className="grid w-full max-w-md mx-auto grid-cols-2">
|
||||
<TabsTrigger value="buying">{t("buying")}</TabsTrigger>
|
||||
<TabsTrigger value="selling">{t("selling")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="buying" className="mt-8">
|
||||
<AnimatedList>
|
||||
{buyOffers.map((offer) => (
|
||||
<OfferCard key={offer.id} offer={offer} />
|
||||
))}
|
||||
</AnimatedList>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="selling" className="mt-8">
|
||||
<AnimatedList>
|
||||
{sellOffers.map((offer) => (
|
||||
<OfferCard key={offer.id} offer={offer} />
|
||||
))}
|
||||
</AnimatedList>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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