Introduces optional `mode` prop ("market"/"catalog") to conditionally render components like rarity badges. Adds defensive checks for missing price data and priceHistory properties to prevent runtime errors. New icons `Badge` and `Boxes` imported from lucide-react.
151 lines
5.0 KiB
TypeScript
151 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { MarketplaceFilters } from "@/components/marketplace-filters";
|
|
import { ItemCard } from "@/components/item-card";
|
|
|
|
interface Item {
|
|
id: string | number;
|
|
name: string;
|
|
category: string;
|
|
image?: string;
|
|
rarity?: string;
|
|
npcPrice?: number;
|
|
price?: { buy: number; sell: number };
|
|
priceHistory?: { highest: number; lowest: number; average: number };
|
|
level?: number;
|
|
weight?: number;
|
|
slots?: number;
|
|
skill?: string;
|
|
}
|
|
|
|
interface InitialData {
|
|
items: Item[];
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
totalPages: number;
|
|
}
|
|
|
|
export default function LibraryClient({ initial }: { initial: InitialData }) {
|
|
const [items, setItems] = useState<Item[]>(initial.items);
|
|
const [total, setTotal] = useState<number>(initial.total);
|
|
const [page, setPage] = useState<number>(initial.page || 1);
|
|
const [pageSize] = useState<number>(initial.pageSize || 24);
|
|
const [totalPages, setTotalPages] = useState<number>(initial.totalPages || 1);
|
|
|
|
const [query, setQuery] = useState<string>("");
|
|
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 debounceRef = useRef<number | undefined>(undefined);
|
|
|
|
const searchParams = useMemo(() => {
|
|
const p = new URLSearchParams();
|
|
if (query) p.set("q", query);
|
|
if (categories.length > 0) p.set("category", categories.join(","));
|
|
if (rarities.length > 0) p.set("rarity", rarities.join(","));
|
|
if (sort) p.set("sort", sort);
|
|
p.set("page", String(page));
|
|
p.set("pageSize", String(pageSize));
|
|
return p;
|
|
}, [query, categories, rarities, sort, page, pageSize]);
|
|
|
|
useEffect(() => {
|
|
// Debounce para evitar chamadas excessivas
|
|
window.clearTimeout(debounceRef.current);
|
|
debounceRef.current = window.setTimeout(async () => {
|
|
const res = await fetch(`/api/items?${searchParams.toString()}`);
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
setItems(data.items);
|
|
setTotal(data.total);
|
|
setTotalPages(data.totalPages);
|
|
}, 250);
|
|
return () => window.clearTimeout(debounceRef.current);
|
|
}, [searchParams]);
|
|
|
|
// Carregar categorias dinâmicas a partir das tabelas do banco
|
|
useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
const res = await fetch("/api/items/categories");
|
|
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) }));
|
|
setCategoryOptions(opts);
|
|
}
|
|
} catch {
|
|
// no-op
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
const onSearch = (q: string) => {
|
|
setPage(1);
|
|
setQuery(q);
|
|
};
|
|
const onFilter = (f: { rarity: string[]; category: string[]; priceRange?: { min: number; max: number } }) => {
|
|
setPage(1);
|
|
setCategories(f.category || []);
|
|
setRarities(f.rarity || []);
|
|
};
|
|
const onSort = (s: string) => {
|
|
setPage(1);
|
|
setSort(s);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<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">
|
|
{items.map((item) => (
|
|
<ItemCard
|
|
key={`${String(item.category || "")}::${String(item.id)}`}
|
|
item={{
|
|
id: String(item.id),
|
|
name: item.name,
|
|
image: item.image ?? "",
|
|
category: (item.category as any) ?? "other",
|
|
rarity: (item.rarity as any) ?? "common",
|
|
stats: {
|
|
weight: typeof item.weight === "number" ? item.weight : undefined,
|
|
slots: typeof item.slots === "number" ? item.slots : undefined,
|
|
} as any,
|
|
requirements: {
|
|
level: typeof item.level === "number" ? item.level : undefined,
|
|
skill: typeof item.skill === "string" && item.skill !== "Nenhum." ? item.skill : undefined,
|
|
} as any,
|
|
} as any}
|
|
mode="catalog"
|
|
/>)
|
|
)}
|
|
</section>
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
<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>
|
|
<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}
|
|
>
|
|
Próxima
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|