Armería: búsqueda insensible a mayúsculas, autocompletar y proxy del visor 3D

Tres arreglos:

1) Búsqueda insensible a mayúsculas/minúsculas. Las consultas usaban LIKE directo
   (sensible según la colación de la tabla), así que 'test' no encontraba 'Test'.
   Ahora comparan con LOWER(col) LIKE LOWER(?).

2) Autocompletar tipo wowhead. Nuevo /api/armory/suggest + ArmorySearchBox
   reescrito: al teclear (≥1 carácter, con debounce) muestra un desplegable con
   personajes, ítems y hermandades que coinciden; clic va directo a la ficha/wowhead.
   Enter o el botón siguen haciendo la búsqueda completa.

3) Visor 3D «no disponible». Causa: el contenido de wow.zamimg.com/modelviewer no
   envía cabeceras CORS (el propio README de wow-model-viewer pide un proxy). Se
   añade un rewrite en next.config (/modelviewer/* -> zamimg) para servirlo
   same-origin, y el visor usa esas rutas. Además, si falla la resolución de ítems
   se renderiza el personaje sin equipo, y los errores se registran en consola para
   diagnóstico.

Verificado: 'test' encuentra 'Test'; el autocompletar responde; el proxy
/modelviewer/live/viewer/viewer.min.js sirve 200 same-origin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-16 14:41:06 +00:00
parent 297257b753
commit 65f6776969
6 changed files with 221 additions and 30 deletions
+19 -6
View File
@@ -46,8 +46,8 @@ const LIMIT = 25
export async function searchCharacters(q: string, limit = LIMIT): Promise<CharacterResult[]> {
try {
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
'SELECT guid, name, race, gender, class, level FROM characters WHERE name LIKE ? ORDER BY level DESC, name LIMIT ?',
[`%${q}%`, limit],
'SELECT guid, name, race, gender, class, level FROM characters WHERE LOWER(name) LIKE ? ORDER BY level DESC, name LIMIT ?',
[`%${q.toLowerCase()}%`, limit],
)
return rows.map((r) => ({
guid: r.guid,
@@ -66,8 +66,8 @@ export async function searchGuilds(q: string, limit = LIMIT): Promise<GuildResul
try {
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
`SELECT g.guildid, g.name, (SELECT COUNT(*) FROM guild_member gm WHERE gm.guildid = g.guildid) AS members
FROM guild g WHERE g.name LIKE ? ORDER BY members DESC LIMIT ?`,
[`%${q}%`, limit],
FROM guild g WHERE LOWER(g.name) LIKE ? ORDER BY members DESC LIMIT ?`,
[`%${q.toLowerCase()}%`, limit],
)
return rows.map((r) => ({ guildid: r.guildid, name: r.name, members: Number(r.members ?? 0) }))
} catch {
@@ -80,8 +80,8 @@ export async function searchItems(q: string, locale: string, limit = LIMIT): Pro
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
`SELECT entry, COALESCE(${col}, name_en) AS name, quality, item_level, inventory_type
FROM item_data WHERE ${col} LIKE ? OR name_en LIKE ? ORDER BY item_level DESC LIMIT ?`,
[`%${q}%`, `%${q}%`, limit],
FROM item_data WHERE LOWER(${col}) LIKE ? OR LOWER(name_en) LIKE ? ORDER BY item_level DESC LIMIT ?`,
[`%${q.toLowerCase()}%`, `%${q.toLowerCase()}%`, limit],
)
return rows.map((r) => ({
entry: r.entry,
@@ -95,6 +95,19 @@ export async function searchItems(q: string, locale: string, limit = LIMIT): Pro
}
}
/** Sugerencias para el autocompletar: pocos resultados de cada tipo. */
export async function suggest(
q: string,
locale: string,
): Promise<{ characters: CharacterResult[]; items: ItemResult[]; guilds: GuildResult[] }> {
const [characters, items, guilds] = await Promise.all([
searchCharacters(q, 6),
searchItems(q, locale, 6),
searchGuilds(q, 5),
])
return { characters, items, guilds }
}
export async function getCharacter(guid: number): Promise<
(CharacterResult & { guildName: string | null; guildId: number | null; online: boolean }) | null
> {