feat(library): add vocation filter, normalize categories, improve grid layout

Added vocation state and included it in query string builder, updated onFilter to accept a vocation parameter, improved grid layout with grid flow classes, normalized category names for consistent item icons, and refined stats mapping to handle type safety.
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-30 18:44:52 -03:00
1 parent 44d7f8ce81
commit 2b790d8eb4
19 files changed
+2638 -173

No files matched your search

+53 -18
View File
@@ -5,7 +5,14 @@ export interface SearchParams {
q?: string;
categories?: string[];
rarities?: string[];
sort?: "name" | "price-asc" | "price-desc" | "rarity" | "recent";
sort?:
| "name"
| "level-asc"
| "level-desc"
| "weight-asc"
| "weight-desc"
| "category";
vocation?: string;
page?: number;
pageSize?: number;
}
@@ -34,21 +41,22 @@ function getDatabase() {
}
// Tenta deduzir ordenação segura a partir do parâmetro de sort
function getOrderBy(sort?: SearchParams["sort"]): string {
function getOrderBy(sort: SearchParams["sort"], vocation?: string): { sql: string; bindings: any[] } {
// Ordenação sem prioridade de vocação (filtro de voc estrito cuida apenas do WHERE)
switch (sort) {
case "price-asc":
return "npc_price ASC, name ASC";
case "price-desc":
return "npc_price DESC, name ASC";
case "rarity":
// Requerer que a tabela possua coluna 'rarity'; caso não exista, recai para nome
return "rarity ASC, name ASC";
case "recent":
// Caso exista created_at/updated_at; fallback para name
return "updated_at DESC, name ASC";
case "level-asc":
return { sql: "level ASC, name ASC", bindings: [] };
case "level-desc":
return { sql: "level DESC, name ASC", bindings: [] };
case "weight-asc":
return { sql: "weight ASC, name ASC", bindings: [] };
case "weight-desc":
return { sql: "weight DESC, name ASC", bindings: [] };
case "category":
return { sql: "category ASC, name ASC", bindings: [] };
case "name":
default:
return "name ASC";
return { sql: "name ASC", bindings: [] };
}
}
@@ -61,6 +69,7 @@ export function searchItems<T = any>(params: SearchParams): SearchResult<T> {
categories = [],
rarities = [],
sort = "name",
vocation,
page = 1,
pageSize = 24,
} = params;
@@ -117,7 +126,7 @@ export function searchItems<T = any>(params: SearchParams): SearchResult<T> {
bindings.push(...rarities.map((r) => r.toLowerCase()));
}
const orderBy = getOrderBy(sort);
const { sql: orderBy, bindings: orderBindings } = getOrderBy(sort, vocation);
// Seleciona de múltiplas tabelas (PT) unificando shape padronizado
const allTables = db
@@ -141,18 +150,44 @@ export function searchItems<T = any>(params: SearchParams): SearchResult<T> {
const colNames = new Set(cols.map((c) => (c?.name || "").toLowerCase()));
// Requer colunas mínimas
if (!colNames.has("id") || !colNames.has("nome") || !colNames.has("image_url")) continue;
// Se filtro de vocação foi solicitado e a tabela não possui coluna 'voc', ignora esta tabela
if (vocation && !colNames.has("voc")) {
continue;
}
const tableWhere: string[] = [];
if (q && colNames.has("nome")) {
tableWhere.push("LOWER(nome) LIKE ?");
unionBindings.push(`%${q.toLowerCase()}%`);
}
if (vocation && colNames.has("voc")) {
const sel = vocation.toLowerCase();
// Filtro estrito: comparar normalizando espaços
tableWhere.push("REPLACE(LOWER(voc), ' ', '') = REPLACE(?, ' ', '')");
unionBindings.push(sel);
}
const whereSqlTbl = tableWhere.length ? `WHERE ${tableWhere.join(" AND ")}` : "";
// category: derivamos do nome da tabela (pt)
const levelCol = colNames.has("nivel") ? "nivel" : "NULL";
const weightCol = colNames.has("peso") ? "peso" : "NULL";
const levelCol = colNames.has("nivel") || colNames.has("lvl")
? `COALESCE(${colNames.has("nivel") ? "nivel" : "NULL"}, ${colNames.has("lvl") ? "lvl" : "NULL"})`
: "NULL";
const weightCol = colNames.has("peso") || colNames.has("peso_em_oz")
? `COALESCE(${colNames.has("peso") ? "peso" : "NULL"}, ${colNames.has("peso_em_oz") ? "peso_em_oz" : "NULL"})`
: "NULL";
const slotsCol = colNames.has("slots") ? "slots" : "NULL";
const skillCol = colNames.has("skill_para_usar") ? "skill_para_usar" : "NULL";
// Campos extras opcionais por tabela (ex.: aljavas)
const extraCols = [
"lvl",
"voc",
"volume",
"bonus_de_skill",
"protecao",
"dropa_de",
] as const;
const extraSelect = extraCols
.map((c) => (colNames.has(c) ? `${c}` : `NULL AS ${c}`))
.join(",\n ");
selects.push(
`SELECT
id,
@@ -162,7 +197,7 @@ export function searchItems<T = any>(params: SearchParams): SearchResult<T> {
${levelCol} AS level,
${weightCol} AS weight,
${slotsCol} AS slots,
${skillCol} AS skill
${skillCol} AS skill${extraSelect ? ",\n " + extraSelect : ""}
FROM ${t} ${whereSqlTbl}`
);
} catch (e) {
@@ -182,7 +217,7 @@ export function searchItems<T = any>(params: SearchParams): SearchResult<T> {
const total = totalRow?.cnt ?? 0;
const selectStmt = db.prepare(
`SELECT * FROM (${unionSql}) AS u ORDER BY name ASC LIMIT ? OFFSET ?`
`SELECT * FROM (${unionSql}) AS u ORDER BY ${orderBy} LIMIT ? OFFSET ?`
);
const items = selectStmt.all(...unionBindings, safePageSize, offset) as T[];