style: clean up code formatting and remove trailing whitespace

refactor: reorganize imports and fix type imports
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-11 03:08:36 -03:00
1 parent d3704602fd
commit 47c04de6fe
232 files changed
+7140 -4798

No files matched your search

+3 -4
View File
@@ -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
View File
@@ -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>
);
}
+1 -3
View File
@@ -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`);
}
+6 -6
View File
@@ -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();
+10 -11
View File
@@ -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
View File
@@ -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",
+88 -51
View File
@@ -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>
);
}
+17 -9
View File
@@ -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>
);
}
+238 -222
View File
@@ -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 -5
View File
@@ -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>
);
}
+29 -21
View File
@@ -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 (
+3 -3
View File
@@ -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 />
+3 -3
View File
@@ -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",
+2 -2
View File
@@ -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,