296 lines
9.6 KiB
TypeScript
296 lines
9.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { ItemCard } from "@/components/item-card";
|
|
import { MarketplaceFilters } from "@/components/marketplace-filters";
|
|
|
|
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 [vocation, setVocation] = useState<string | null>(null);
|
|
|
|
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);
|
|
if (vocation) p.set("voc", vocation);
|
|
p.set("page", String(page));
|
|
p.set("pageSize", String(pageSize));
|
|
return p;
|
|
}, [query, categories, rarities, sort, page, pageSize, vocation]);
|
|
|
|
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 };
|
|
vocation?: string | null;
|
|
}) => {
|
|
setPage(1);
|
|
setCategories(f.category || []);
|
|
setRarities(f.rarity || []);
|
|
setVocation(f.vocation ?? null);
|
|
};
|
|
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 grid-flow-row-dense">
|
|
{items.map((item) => {
|
|
const anyItem = item as any;
|
|
// Normalização de categoria para os ícones do ItemCard
|
|
const normalizeCategory = (c: string | undefined): string => {
|
|
const v = (c || "").toLowerCase();
|
|
const map: Record<string, string> = {
|
|
escudos: "shield",
|
|
armaduras: "armor",
|
|
capacetes: "helmet",
|
|
calcas: "legs",
|
|
botas: "boots",
|
|
amuletos: "amulet",
|
|
aneis: "ring",
|
|
runas: "rune",
|
|
ferramentas: "tool",
|
|
municao: "ammunition",
|
|
aljavas: "ammunition",
|
|
distancia: "weapon",
|
|
espadas: "weapon",
|
|
machados: "weapon",
|
|
clavas: "weapon",
|
|
punhos: "weapon",
|
|
wands: "weapon",
|
|
rods: "weapon",
|
|
spellbooks: "other",
|
|
chaves: "other",
|
|
comidas: "other",
|
|
decoracoes: "other",
|
|
liquidos: "other",
|
|
plantas: "other",
|
|
recipientes: "other",
|
|
trofeus: "other",
|
|
};
|
|
return map[v] || v || "other";
|
|
};
|
|
|
|
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 ?? "";
|
|
|
|
// Campos numéricos normalizados
|
|
const level: number | undefined =
|
|
typeof item.level === "number"
|
|
? item.level
|
|
: typeof anyItem.nivel === "number"
|
|
? 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;
|
|
|
|
const slots: number | undefined =
|
|
typeof item.slots === "number"
|
|
? item.slots
|
|
: typeof anyItem.slots === "number"
|
|
? anyItem.slots
|
|
: undefined;
|
|
|
|
const attack: number | undefined =
|
|
typeof anyItem.ataque === "number"
|
|
? anyItem.ataque
|
|
: typeof anyItem.atq === "number"
|
|
? anyItem.atq
|
|
: undefined;
|
|
const defense: number | undefined =
|
|
typeof anyItem.defesa === "number"
|
|
? anyItem.defesa
|
|
: typeof anyItem.def === "number"
|
|
? anyItem.def
|
|
: undefined;
|
|
const armor: number | undefined =
|
|
typeof anyItem.arm === "number" ? anyItem.arm : undefined;
|
|
|
|
// Skill/requisito para usar
|
|
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;
|
|
const vocation: string | undefined =
|
|
typeof anyItem.voc === "string" && anyItem.voc
|
|
? anyItem.voc
|
|
: undefined;
|
|
|
|
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
|
|
}
|
|
mode="catalog"
|
|
/>
|
|
);
|
|
})}
|
|
</section>
|
|
|
|
<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>
|
|
<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}
|
|
>
|
|
Próxima
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|