style: clean up code formatting and remove trailing whitespace
refactor: reorganize imports and fix type imports
This commit is contained in:
1 parent
d3704602fd
commit
47c04de6fe
232 files changed
+7140
-4798
No files matched your search
@@ -1,12 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import Header from "@/components/Header";
|
||||
import { AdminItemCreatePage } from "@/components/admin/items/AdminItemCreatePage";
|
||||
import { getSession } from "@/lib/redis/session";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import Header from "@/components/Header";
|
||||
import { searchAdminItems } from "@/lib/prisma/queries/items";
|
||||
import { getActiveServers } from "@/lib/prisma/queries/servers";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { getSession } from "@/lib/redis/session";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
@@ -57,4 +57,3 @@ export default async function AdminNewItemPage({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+24
-11
@@ -1,12 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AdminPanelTabs } from "@/components/admin/AdminPanelTabs";
|
||||
import Header from "@/components/Header";
|
||||
import { getSession } from "@/lib/redis/session";
|
||||
import { searchAdminItems } from "@/lib/prisma/queries/items";
|
||||
import { getAllSubscriptionTiers } from "@/lib/prisma/queries/subscription-tiers";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { AdminPanelTabs } from "@/components/admin/AdminPanelTabs";
|
||||
import { searchAdminItems } from "@/lib/prisma/queries/items";
|
||||
import { getSession } from "@/lib/redis/session";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
@@ -30,7 +30,8 @@ export default async function AdminPage({
|
||||
searchParams instanceof Promise ? await searchParams : searchParams;
|
||||
const tabParam = resolvedSearchParams?.tab;
|
||||
const initialTab =
|
||||
typeof tabParam === "string" && ["overview", "subscriptions", "items"].includes(tabParam)
|
||||
typeof tabParam === "string" &&
|
||||
["overview", "subscriptions", "items"].includes(tabParam)
|
||||
? (tabParam as "overview" | "subscriptions" | "items")
|
||||
: "overview";
|
||||
|
||||
@@ -58,14 +59,24 @@ export default async function AdminPage({
|
||||
...tier,
|
||||
price_monthly: tier.price_monthly ? Number(tier.price_monthly) : null,
|
||||
price_yearly: tier.price_yearly ? Number(tier.price_yearly) : null,
|
||||
created_at: tier.created_at instanceof Date ? tier.created_at.toISOString() : tier.created_at,
|
||||
updated_at: tier.updated_at instanceof Date ? tier.updated_at.toISOString() : tier.updated_at,
|
||||
created_at:
|
||||
tier.created_at instanceof Date
|
||||
? tier.created_at.toISOString()
|
||||
: tier.created_at,
|
||||
updated_at:
|
||||
tier.updated_at instanceof Date
|
||||
? tier.updated_at.toISOString()
|
||||
: tier.updated_at,
|
||||
features: featureList.map((feature) => ({
|
||||
...feature,
|
||||
created_at:
|
||||
feature.created_at instanceof Date ? feature.created_at.toISOString() : feature.created_at ?? null,
|
||||
feature.created_at instanceof Date
|
||||
? feature.created_at.toISOString()
|
||||
: (feature.created_at ?? null),
|
||||
updated_at:
|
||||
feature.updated_at instanceof Date ? feature.updated_at.toISOString() : feature.updated_at ?? null,
|
||||
feature.updated_at instanceof Date
|
||||
? feature.updated_at.toISOString()
|
||||
: (feature.updated_at ?? null),
|
||||
})),
|
||||
};
|
||||
});
|
||||
@@ -75,9 +86,12 @@ export default async function AdminPage({
|
||||
<Header />
|
||||
<main className="container mx-auto px-4 py-10 space-y-8">
|
||||
<section className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Painel administrativo</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
Painel administrativo
|
||||
</h1>
|
||||
<p className="text-muted-foreground max-w-2xl">
|
||||
Centralize a gestão de planos, permissões e o catálogo de itens. Utilize as abas para acessar cada área temática.
|
||||
Centralize a gestão de planos, permissões e o catálogo de itens.
|
||||
Utilize as abas para acessar cada área temática.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -92,4 +106,3 @@ export default async function AdminPage({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import { redirect } from "next/navigation";
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: "Assinaturas e Permissões",
|
||||
description:
|
||||
"Redirecionando para o painel administrativo centralizado.",
|
||||
description: "Redirecionando para o painel administrativo centralizado.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,4 +16,3 @@ export default async function AdminSubscriptionsRedirect({
|
||||
const { locale } = await params;
|
||||
redirect(`/${locale}/admin?tab=subscriptions`);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import confetti from "canvas-confetti";
|
||||
import Header from "@/components/Header";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import { Heart, Share2, Home } from "lucide-react";
|
||||
import { Heart, Home, Share2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function ThankYouPage() {
|
||||
const params = useParams();
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import Header from "@/components/Header";
|
||||
import { Heart } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { DonationCard } from "@/components/donation-card";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import { NumberTicker } from "@/components/ui/number-ticker";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Header from "@/components/Header";
|
||||
import { ShareButton } from "@/components/share-button";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Heart } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import { ShareButton } from "@/components/share-button";
|
||||
import type { Metadata } from "next";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NumberTicker } from "@/components/ui/number-ticker";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -60,7 +59,7 @@ export default async function SupportPage({
|
||||
};
|
||||
|
||||
// Custos mensais REAIS
|
||||
const monthlyCosts = {
|
||||
const _monthlyCosts = {
|
||||
vps: 69, // R$ 69/mês
|
||||
devHours: 40, // média horas/mês
|
||||
devRate: 50, // R$ 50/hora
|
||||
|
||||
+13
-4
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages, setRequestLocale } from "next-intl/server";
|
||||
import Script from "next/script";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { QueryProvider } from "@/lib/providers/query-provider";
|
||||
|
||||
@@ -113,14 +113,19 @@ export default async function LocaleLayout({
|
||||
<QueryProvider>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark">
|
||||
<Script id="structured-data-organization" type="application/ld+json" strategy="afterInteractive">
|
||||
<Script
|
||||
id="structured-data-organization"
|
||||
type="application/ld+json"
|
||||
strategy="afterInteractive"
|
||||
>
|
||||
{JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
name: "Tibia Trade",
|
||||
url: baseUrl,
|
||||
logo: `${baseUrl}/logo.png`,
|
||||
description: "Marketplace multi-servidor seguro para negociação de itens de Tibia",
|
||||
description:
|
||||
"Marketplace multi-servidor seguro para negociação de itens de Tibia",
|
||||
sameAs: ["https://discord.gg/tibiatrade"],
|
||||
contactPoint: {
|
||||
"@type": "ContactPoint",
|
||||
@@ -129,7 +134,11 @@ export default async function LocaleLayout({
|
||||
},
|
||||
})}
|
||||
</Script>
|
||||
<Script id="structured-data-website" type="application/ld+json" strategy="afterInteractive">
|
||||
<Script
|
||||
id="structured-data-website"
|
||||
type="application/ld+json"
|
||||
strategy="afterInteractive"
|
||||
>
|
||||
{JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { MarketplaceFilters } from "@/components/marketplace-filters";
|
||||
import { ItemCard } from "@/components/item-card";
|
||||
import { MarketplaceFilters } from "@/components/marketplace-filters";
|
||||
|
||||
interface Item {
|
||||
id: string | number;
|
||||
@@ -38,7 +38,9 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [rarities, setRarities] = useState<string[]>([]);
|
||||
const [sort, setSort] = useState<string>("name");
|
||||
const [categoryOptions, setCategoryOptions] = useState<{ value: string; label: string }[] | undefined>(undefined);
|
||||
const [categoryOptions, setCategoryOptions] = useState<
|
||||
{ value: string; label: string }[] | undefined
|
||||
>(undefined);
|
||||
const [vocation, setVocation] = useState<string | null>(null);
|
||||
|
||||
const debounceRef = useRef<number | undefined>(undefined);
|
||||
@@ -77,7 +79,10 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data.categories)) {
|
||||
const opts = (data.categories as string[]).map((c) => ({ value: c, label: c.charAt(0).toUpperCase() + c.slice(1) }));
|
||||
const opts = (data.categories as string[]).map((c) => ({
|
||||
value: c,
|
||||
label: c.charAt(0).toUpperCase() + c.slice(1),
|
||||
}));
|
||||
setCategoryOptions(opts);
|
||||
}
|
||||
} catch {
|
||||
@@ -90,7 +95,12 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
setPage(1);
|
||||
setQuery(q);
|
||||
};
|
||||
const onFilter = (f: { rarity: string[]; category: string[]; priceRange?: { min: number; max: number }; vocation?: string | null }) => {
|
||||
const onFilter = (f: {
|
||||
rarity: string[];
|
||||
category: string[];
|
||||
priceRange?: { min: number; max: number };
|
||||
vocation?: string | null;
|
||||
}) => {
|
||||
setPage(1);
|
||||
setCategories(f.category || []);
|
||||
setRarities(f.rarity || []);
|
||||
@@ -103,7 +113,14 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<MarketplaceFilters onSearch={onSearch} onFilter={onFilter} onSort={onSort} totalItems={total} showRarity={false} categoryOptionsOverride={categoryOptions} />
|
||||
<MarketplaceFilters
|
||||
onSearch={onSearch}
|
||||
onFilter={onFilter}
|
||||
onSort={onSort}
|
||||
totalItems={total}
|
||||
showRarity={false}
|
||||
categoryOptionsOverride={categoryOptions}
|
||||
/>
|
||||
|
||||
<section className="grid gap-6 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 grid-flow-row-dense">
|
||||
{items.map((item) => {
|
||||
@@ -142,7 +159,10 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
return map[v] || v || "other";
|
||||
};
|
||||
|
||||
const categoryPt = typeof anyItem.categoria === "string" ? anyItem.categoria : undefined;
|
||||
const categoryPt =
|
||||
typeof anyItem.categoria === "string"
|
||||
? anyItem.categoria
|
||||
: undefined;
|
||||
const category = normalizeCategory(item.category ?? categoryPt);
|
||||
const name = item.name ?? anyItem.nome ?? String(item.id);
|
||||
const image = item.image ?? anyItem.image_url ?? "";
|
||||
@@ -152,39 +172,39 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
typeof item.level === "number"
|
||||
? item.level
|
||||
: typeof anyItem.nivel === "number"
|
||||
? anyItem.nivel
|
||||
: typeof anyItem.lvl === "number"
|
||||
? anyItem.lvl
|
||||
: undefined;
|
||||
? anyItem.nivel
|
||||
: typeof anyItem.lvl === "number"
|
||||
? anyItem.lvl
|
||||
: undefined;
|
||||
|
||||
const weight: number | undefined =
|
||||
typeof item.weight === "number"
|
||||
? item.weight
|
||||
: typeof anyItem.peso === "number"
|
||||
? anyItem.peso
|
||||
: typeof anyItem.peso_em_oz === "number"
|
||||
? anyItem.peso_em_oz
|
||||
: undefined;
|
||||
? anyItem.peso
|
||||
: typeof anyItem.peso_em_oz === "number"
|
||||
? anyItem.peso_em_oz
|
||||
: undefined;
|
||||
|
||||
const slots: number | undefined =
|
||||
typeof item.slots === "number"
|
||||
? item.slots
|
||||
: typeof anyItem.slots === "number"
|
||||
? anyItem.slots
|
||||
: undefined;
|
||||
? anyItem.slots
|
||||
: undefined;
|
||||
|
||||
const attack: number | undefined =
|
||||
typeof anyItem.ataque === "number"
|
||||
? anyItem.ataque
|
||||
: typeof anyItem.atq === "number"
|
||||
? anyItem.atq
|
||||
: undefined;
|
||||
? anyItem.atq
|
||||
: undefined;
|
||||
const defense: number | undefined =
|
||||
typeof anyItem.defesa === "number"
|
||||
? anyItem.defesa
|
||||
: typeof anyItem.def === "number"
|
||||
? anyItem.def
|
||||
: undefined;
|
||||
? anyItem.def
|
||||
: undefined;
|
||||
const armor: number | undefined =
|
||||
typeof anyItem.arm === "number" ? anyItem.arm : undefined;
|
||||
|
||||
@@ -192,9 +212,10 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
const skill: string | undefined =
|
||||
typeof item.skill === "string" && item.skill !== "Nenhum."
|
||||
? item.skill
|
||||
: typeof anyItem.skill_para_usar === "string" && anyItem.skill_para_usar !== "Nenhum."
|
||||
? anyItem.skill_para_usar
|
||||
: undefined;
|
||||
: typeof anyItem.skill_para_usar === "string" &&
|
||||
anyItem.skill_para_usar !== "Nenhum."
|
||||
? anyItem.skill_para_usar
|
||||
: undefined;
|
||||
const vocation: string | undefined =
|
||||
typeof anyItem.voc === "string" && anyItem.voc
|
||||
? anyItem.voc
|
||||
@@ -203,31 +224,45 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
return (
|
||||
<ItemCard
|
||||
key={`${String((anyItem as any).category)}::${String(item.id)}::${name}`}
|
||||
item={{
|
||||
id: String(item.id),
|
||||
name,
|
||||
image,
|
||||
category: category as any,
|
||||
rarity: (item.rarity as any) ?? "common",
|
||||
categoryPt,
|
||||
stats: {
|
||||
weight,
|
||||
slots,
|
||||
attack,
|
||||
defense,
|
||||
armor,
|
||||
} as any,
|
||||
requirements: {
|
||||
level,
|
||||
skill,
|
||||
vocation,
|
||||
} as any,
|
||||
// Campos específicos de aljavas (e reutilizáveis para outras categorias)
|
||||
volume: typeof anyItem.volume === "number" ? anyItem.volume : undefined,
|
||||
bonus_de_skill: typeof anyItem.bonus_de_skill === "number" ? anyItem.bonus_de_skill : undefined,
|
||||
protecao: typeof anyItem.protecao === "string" ? anyItem.protecao : undefined,
|
||||
dropa_de: typeof anyItem.dropa_de === "string" ? anyItem.dropa_de : undefined,
|
||||
} as any}
|
||||
item={
|
||||
{
|
||||
id: String(item.id),
|
||||
name,
|
||||
image,
|
||||
category: category as any,
|
||||
rarity: (item.rarity as any) ?? "common",
|
||||
categoryPt,
|
||||
stats: {
|
||||
weight,
|
||||
slots,
|
||||
attack,
|
||||
defense,
|
||||
armor,
|
||||
} as any,
|
||||
requirements: {
|
||||
level,
|
||||
skill,
|
||||
vocation,
|
||||
} as any,
|
||||
// Campos específicos de aljavas (e reutilizáveis para outras categorias)
|
||||
volume:
|
||||
typeof anyItem.volume === "number"
|
||||
? anyItem.volume
|
||||
: undefined,
|
||||
bonus_de_skill:
|
||||
typeof anyItem.bonus_de_skill === "number"
|
||||
? anyItem.bonus_de_skill
|
||||
: undefined,
|
||||
protecao:
|
||||
typeof anyItem.protecao === "string"
|
||||
? anyItem.protecao
|
||||
: undefined,
|
||||
dropa_de:
|
||||
typeof anyItem.dropa_de === "string"
|
||||
? anyItem.dropa_de
|
||||
: undefined,
|
||||
} as any
|
||||
}
|
||||
mode="catalog"
|
||||
/>
|
||||
);
|
||||
@@ -236,14 +271,18 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="px-3 py-1 rounded border text-sm hover:bg-muted disabled:opacity-50"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1}
|
||||
>
|
||||
Anterior
|
||||
</button>
|
||||
<span className="text-sm text-muted-foreground">Página {page} de {totalPages}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Página {page} de {totalPages}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="px-3 py-1 rounded border text-sm hover:bg-muted disabled:opacity-50"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages}
|
||||
@@ -254,5 +293,3 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Header from "@/components/Header";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Header from "@/components/Header";
|
||||
import LibraryClient from "./LibraryClient";
|
||||
|
||||
interface Item {
|
||||
@@ -18,17 +18,24 @@ interface Item {
|
||||
|
||||
async function fetchItems(params: URLSearchParams) {
|
||||
const qs = params.toString();
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL ?? ""}/api/items?${qs}`, {
|
||||
// Em ambiente server, revalidação curta para manter leve
|
||||
next: { revalidate: 15 },
|
||||
});
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_BASE_URL ?? ""}/api/items?${qs}`,
|
||||
{
|
||||
// Em ambiente server, revalidação curta para manter leve
|
||||
next: { revalidate: 15 },
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
return { items: [], total: 0, page: 1, pageSize: 24, totalPages: 1 };
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "underConstruction" });
|
||||
return {
|
||||
@@ -37,7 +44,10 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
||||
};
|
||||
}
|
||||
|
||||
export default async function LibraryPage({ params, searchParams }: {
|
||||
export default async function LibraryPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
@@ -69,5 +79,3 @@ export default async function LibraryPage({ params, searchParams }: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Grid, List, Search, SortAsc, SortDesc, X } from "lucide-react";
|
||||
import { useLocale } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AdCard } from "@/components/marketplace/AdCard";
|
||||
import { CreateAdModal } from "@/components/marketplace/CreateAdModal";
|
||||
import { MyAdsPanel } from "@/components/marketplace/MyAdsPanel";
|
||||
import { MyPurchaseIntentionsPanel } from "@/components/marketplace/MyPurchaseIntentionsPanel";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -11,19 +17,13 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Search, SortAsc, SortDesc, Filter, X, Grid, List } from "lucide-react";
|
||||
import { useMarketTelemetry } from "@/lib/hooks/use-telemetry";
|
||||
import { AdCard } from "@/components/marketplace/AdCard";
|
||||
import { CreateAdModal } from "@/components/marketplace/CreateAdModal";
|
||||
import { MyAdsPanel } from "@/components/marketplace/MyAdsPanel";
|
||||
import { MyPurchaseIntentionsPanel } from "@/components/marketplace/MyPurchaseIntentionsPanel";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { useListings, useInvalidateListings } from "@/lib/hooks/use-listings";
|
||||
import { useInvalidateListings, useListings } from "@/lib/hooks/use-listings";
|
||||
import { useMyPurchaseIntentions } from "@/lib/hooks/use-purchase-intentions";
|
||||
import { useLocale } from "next-intl";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { useMarketTelemetry } from "@/lib/hooks/use-telemetry";
|
||||
|
||||
interface Listing {
|
||||
listing_id: number;
|
||||
@@ -57,7 +57,7 @@ export default function MarketClient({ initial }: { initial: InitialData }) {
|
||||
const { trackSearch, trackFilter, trackPageView } = useMarketTelemetry();
|
||||
const invalidateListings = useInvalidateListings();
|
||||
const locale = useLocale();
|
||||
|
||||
|
||||
const [page, setPage] = useState<number>(initial.page || 1);
|
||||
const [pageSize] = useState<number>(initial.pageSize || 20);
|
||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
||||
@@ -73,7 +73,12 @@ export default function MarketClient({ initial }: { initial: InitialData }) {
|
||||
const [activeTab, setActiveTab] = useState<string>("explore");
|
||||
|
||||
// Usar React Query para buscar listings com atualização automática
|
||||
const { listings, total, totalPages, isLoading: loading } = useListings({
|
||||
const {
|
||||
listings,
|
||||
total,
|
||||
totalPages,
|
||||
isLoading: loading,
|
||||
} = useListings({
|
||||
search: searchQuery || undefined,
|
||||
server_name: serverName || undefined,
|
||||
price_min: priceMin || undefined,
|
||||
@@ -144,7 +149,7 @@ export default function MarketClient({ initial }: { initial: InitialData }) {
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Marketplace</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{activeTab === "explore"
|
||||
{activeTab === "explore"
|
||||
? `${total} ${total === 1 ? "anúncio" : "anúncios"} disponíveis`
|
||||
: "Gerencie seus anúncios"}
|
||||
</p>
|
||||
@@ -161,226 +166,238 @@ export default function MarketClient({ initial }: { initial: InitialData }) {
|
||||
<TabsList>
|
||||
<TabsTrigger value="explore">Explorar</TabsTrigger>
|
||||
{user && <TabsTrigger value="my-ads">Meus Anúncios</TabsTrigger>}
|
||||
{user && (
|
||||
<MyPurchaseIntentionsTabTrigger />
|
||||
)}
|
||||
{user && <MyPurchaseIntentionsTabTrigger />}
|
||||
</TabsList>
|
||||
|
||||
{/* Aba: Explorar Anúncios */}
|
||||
<TabsContent value="explore" className="space-y-6 mt-6">
|
||||
|
||||
{/* Barra de busca */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar anúncios por item ou descrição..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="pl-10 pr-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filtros e controles */}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{/* Filtro por servidor */}
|
||||
<Input
|
||||
placeholder="Filtrar por servidor"
|
||||
value={serverName}
|
||||
onChange={(e) => {
|
||||
setServerName(e.target.value);
|
||||
handleFilter({ server: e.target.value });
|
||||
}}
|
||||
className="w-48"
|
||||
/>
|
||||
|
||||
{/* Filtro por preço mínimo */}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Preço min"
|
||||
value={priceMin}
|
||||
onChange={(e) => {
|
||||
setPriceMin(e.target.value);
|
||||
handleFilter({ priceMin: e.target.value ? parseFloat(e.target.value) : undefined });
|
||||
}}
|
||||
className="w-32"
|
||||
/>
|
||||
|
||||
{/* Filtro por preço máximo */}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Preço max"
|
||||
value={priceMax}
|
||||
onChange={(e) => {
|
||||
setPriceMax(e.target.value);
|
||||
handleFilter({ priceMax: e.target.value ? parseFloat(e.target.value) : undefined });
|
||||
}}
|
||||
className="w-32"
|
||||
/>
|
||||
|
||||
{/* Ordenação */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
{(() => {
|
||||
const selectedOption = sortOptions.find((opt) => opt.value === sortBy);
|
||||
const Icon = selectedOption?.icon;
|
||||
return Icon ? <Icon className="h-4 w-4" /> : null;
|
||||
})()}
|
||||
{sortOptions.find((opt) => opt.value === sortBy)?.label || "Ordenar"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-48">
|
||||
<DropdownMenuLabel>Ordenar por</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{sortOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
onClick={() => handleSort(option.value)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{option.label}</span>
|
||||
{sortBy === option.value && (
|
||||
<div className="w-2 h-2 bg-primary rounded-full ml-auto" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Toggle de visualização */}
|
||||
<div className="flex items-center gap-1 border rounded-md p-1">
|
||||
<Button
|
||||
variant={viewMode === "grid" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("grid")}
|
||||
>
|
||||
<Grid className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === "list" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("list")}
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Limpar filtros */}
|
||||
{activeFiltersCount > 0 && (
|
||||
<Button variant="ghost" onClick={clearFilters} className="gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
Limpar Filtros
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Badges de filtros ativos */}
|
||||
{activeFiltersCount > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{searchQuery && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Busca: {searchQuery}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
{serverName && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Servidor: {serverName}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setServerName("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
{(priceMin || priceMax) && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Preço: {priceMin || "0"} - {priceMax || "∞"}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setPriceMin("");
|
||||
setPriceMax("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Listagem de anúncios */}
|
||||
{loading ? (
|
||||
<div
|
||||
className={
|
||||
viewMode === "grid"
|
||||
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
: "space-y-4"
|
||||
}
|
||||
>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-64 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : listings.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Nenhum anúncio encontrado
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm mt-2">
|
||||
Tente ajustar os filtros ou criar um novo anúncio
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
viewMode === "grid"
|
||||
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
: "space-y-4"
|
||||
}
|
||||
>
|
||||
{listings.map((listing) => (
|
||||
<AdCard key={listing.listing_id} listing={listing} viewMode={viewMode} />
|
||||
))}
|
||||
{/* Barra de busca */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar anúncios por item ou descrição..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="pl-10 pr-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Paginação */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{/* Filtros e controles */}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{/* Filtro por servidor */}
|
||||
<Input
|
||||
placeholder="Filtrar por servidor"
|
||||
value={serverName}
|
||||
onChange={(e) => {
|
||||
setServerName(e.target.value);
|
||||
handleFilter({ server: e.target.value });
|
||||
}}
|
||||
className="w-48"
|
||||
/>
|
||||
|
||||
{/* Filtro por preço mínimo */}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Preço min"
|
||||
value={priceMin}
|
||||
onChange={(e) => {
|
||||
setPriceMin(e.target.value);
|
||||
handleFilter({
|
||||
priceMin: e.target.value
|
||||
? parseFloat(e.target.value)
|
||||
: undefined,
|
||||
});
|
||||
}}
|
||||
className="w-32"
|
||||
/>
|
||||
|
||||
{/* Filtro por preço máximo */}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Preço max"
|
||||
value={priceMax}
|
||||
onChange={(e) => {
|
||||
setPriceMax(e.target.value);
|
||||
handleFilter({
|
||||
priceMax: e.target.value
|
||||
? parseFloat(e.target.value)
|
||||
: undefined,
|
||||
});
|
||||
}}
|
||||
className="w-32"
|
||||
/>
|
||||
|
||||
{/* Ordenação */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
{(() => {
|
||||
const selectedOption = sortOptions.find(
|
||||
(opt) => opt.value === sortBy,
|
||||
);
|
||||
const Icon = selectedOption?.icon;
|
||||
return Icon ? <Icon className="h-4 w-4" /> : null;
|
||||
})()}
|
||||
{sortOptions.find((opt) => opt.value === sortBy)?.label ||
|
||||
"Ordenar"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-48">
|
||||
<DropdownMenuLabel>Ordenar por</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{sortOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
onClick={() => handleSort(option.value)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{option.label}</span>
|
||||
{sortBy === option.value && (
|
||||
<div className="w-2 h-2 bg-primary rounded-full ml-auto" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Toggle de visualização */}
|
||||
<div className="flex items-center gap-1 border rounded-md p-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
variant={viewMode === "grid" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("grid")}
|
||||
>
|
||||
Anterior
|
||||
<Grid className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Página {page} de {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
variant={viewMode === "list" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("list")}
|
||||
>
|
||||
Próxima
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Limpar filtros */}
|
||||
{activeFiltersCount > 0 && (
|
||||
<Button variant="ghost" onClick={clearFilters} className="gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
Limpar Filtros
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Badges de filtros ativos */}
|
||||
{activeFiltersCount > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{searchQuery && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Busca: {searchQuery}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
{serverName && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Servidor: {serverName}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setServerName("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
{(priceMin || priceMax) && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Preço: {priceMin || "0"} - {priceMax || "∞"}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setPriceMin("");
|
||||
setPriceMax("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Listagem de anúncios */}
|
||||
{loading ? (
|
||||
<div
|
||||
className={
|
||||
viewMode === "grid"
|
||||
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
: "space-y-4"
|
||||
}
|
||||
>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-64 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : listings.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Nenhum anúncio encontrado
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm mt-2">
|
||||
Tente ajustar os filtros ou criar um novo anúncio
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
viewMode === "grid"
|
||||
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
: "space-y-4"
|
||||
}
|
||||
>
|
||||
{listings.map((listing) => (
|
||||
<AdCard
|
||||
key={listing.listing_id}
|
||||
listing={listing}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Paginação */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Página {page} de {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
>
|
||||
Próxima
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Modal de criação de anúncio */}
|
||||
{showCreateModal && (
|
||||
@@ -437,4 +454,3 @@ function MyPurchaseIntentionsTabTrigger() {
|
||||
</TabsTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import Header from "@/components/Header";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { Metadata } from "next";
|
||||
import Header from "@/components/Header";
|
||||
import { AnalyticsDashboard } from "@/components/marketplace/AnalyticsDashboard";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -38,4 +35,3 @@ export default async function AnalyticsPage({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Header from "@/components/Header";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { Metadata } from "next";
|
||||
import MarketClient from "./MarketClient";
|
||||
import Header from "@/components/Header";
|
||||
import { getActiveListings } from "@/lib/prisma/queries/listings";
|
||||
import MarketClient from "./MarketClient";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -46,12 +45,14 @@ export default async function MarketPage({
|
||||
const sp = (await searchParams) ?? {};
|
||||
|
||||
const page = typeof sp.page === "string" ? parseInt(sp.page, 10) : 1;
|
||||
const pageSize = typeof sp.pageSize === "string" ? parseInt(sp.pageSize, 10) : 20;
|
||||
const pageSize =
|
||||
typeof sp.pageSize === "string" ? parseInt(sp.pageSize, 10) : 20;
|
||||
|
||||
// Buscar anúncios ativos
|
||||
const listingsData = await getActiveListings({
|
||||
search: typeof sp.search === "string" ? sp.search : undefined,
|
||||
server_name: typeof sp.server_name === "string" ? sp.server_name : undefined,
|
||||
server_name:
|
||||
typeof sp.server_name === "string" ? sp.server_name : undefined,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy: (typeof sp.sortBy === "string" ? sp.sortBy : "created_at") as
|
||||
@@ -60,27 +61,34 @@ export default async function MarketPage({
|
||||
| "price_tibia_coins"
|
||||
| "price_gold_coins"
|
||||
| "views_count",
|
||||
sortOrder: (typeof sp.sortOrder === "string" ? sp.sortOrder : "DESC") as "ASC" | "DESC",
|
||||
sortOrder: (typeof sp.sortOrder === "string" ? sp.sortOrder : "DESC") as
|
||||
| "ASC"
|
||||
| "DESC",
|
||||
});
|
||||
|
||||
// Converter Date para string para compatibilidade com o tipo do cliente
|
||||
const serializedListings = listingsData.listings.map((listing) => ({
|
||||
...listing,
|
||||
created_at: listing.created_at instanceof Date
|
||||
? listing.created_at.toISOString()
|
||||
: listing.created_at,
|
||||
updated_at: listing.updated_at instanceof Date
|
||||
? listing.updated_at.toISOString()
|
||||
: listing.updated_at,
|
||||
sold_at: listing.sold_at instanceof Date
|
||||
? listing.sold_at.toISOString()
|
||||
: listing.sold_at,
|
||||
auction_ends_at: listing.auction_ends_at instanceof Date
|
||||
? listing.auction_ends_at.toISOString()
|
||||
: listing.auction_ends_at,
|
||||
expires_at: listing.expires_at instanceof Date
|
||||
? listing.expires_at.toISOString()
|
||||
: listing.expires_at,
|
||||
created_at:
|
||||
listing.created_at instanceof Date
|
||||
? listing.created_at.toISOString()
|
||||
: listing.created_at,
|
||||
updated_at:
|
||||
listing.updated_at instanceof Date
|
||||
? listing.updated_at.toISOString()
|
||||
: listing.updated_at,
|
||||
sold_at:
|
||||
listing.sold_at instanceof Date
|
||||
? listing.sold_at.toISOString()
|
||||
: listing.sold_at,
|
||||
auction_ends_at:
|
||||
listing.auction_ends_at instanceof Date
|
||||
? listing.auction_ends_at.toISOString()
|
||||
: listing.auction_ends_at,
|
||||
expires_at:
|
||||
listing.expires_at instanceof Date
|
||||
? listing.expires_at.toISOString()
|
||||
: listing.expires_at,
|
||||
}));
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { DonationFooterCard } from "@/components/donation-footer-card";
|
||||
import Header from "@/components/Header";
|
||||
import { EscrowBanner } from "@/components/sections/escrow-banner";
|
||||
import { EscrowExplanation } from "@/components/sections/escrow-explanation";
|
||||
import { FeaturedItems } from "@/components/sections/featured-items";
|
||||
import { HeroSection } from "@/components/sections/hero-section";
|
||||
import { HowItWorks } from "@/components/sections/how-it-works";
|
||||
import { FeaturedItems } from "@/components/sections/featured-items";
|
||||
import { MarketExample } from "@/components/sections/market-example";
|
||||
import { MarketStats } from "@/components/sections/market-stats";
|
||||
import { DonationFooterCard } from "@/components/donation-footer-card";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
<EscrowBanner />
|
||||
<main role="main" aria-label="Página principal do Tibia Trade">
|
||||
<main aria-label="Página principal do Tibia Trade">
|
||||
<HeroSection />
|
||||
<HowItWorks />
|
||||
<FeaturedItems />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Header from "@/components/Header";
|
||||
import { UnderConstruction } from "@/components/under-construction";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -9,7 +9,7 @@ export async function generateMetadata({
|
||||
params: Promise<{ locale: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "underConstruction" });
|
||||
const _t = await getTranslations({ locale, namespace: "underConstruction" });
|
||||
|
||||
const titles = {
|
||||
pt: "Preços - Estrutura de Custos",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Header from "@/components/Header";
|
||||
import { UnderConstruction } from "@/components/under-construction";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
|
||||
Reference in new issue
Block a user