From 4c12adafd08199140d9c5bb6e9864fb92d99ca93 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:38:59 +0300 Subject: [PATCH] Make searching orgs, weapons less fussy --- app/db/sql.ts | 9 ++++ ...TournamentOrganizationRepository.server.ts | 8 +++- app/modules/in-game-lists/utils.test.ts | 47 +++++++++++++++++++ app/modules/in-game-lists/utils.ts | 6 ++- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 app/modules/in-game-lists/utils.test.ts diff --git a/app/db/sql.ts b/app/db/sql.ts index 7974f3fdc..7f3f2b3a8 100644 --- a/app/db/sql.ts +++ b/app/db/sql.ts @@ -37,6 +37,15 @@ sql.pragma("mmap_size = 3221225472"); // connections; pair with a periodic `PRAGMA optimize;` (see OptimizeDatabase routine) sql.pragma("optimize = 0x10002"); +// Strips diacritics so accent-insensitive name searches are possible +// (e.g. "cafe" matches "Café"). Combined with LIKE's built-in ASCII +// case-insensitivity this also folds case for the resulting latin letters. +sql.function("unaccent", { deterministic: true }, (value) => + typeof value === "string" + ? value.normalize("NFD").replace(/\p{M}/gu, "") + : value, +); + export const db = new Kysely({ dialect: new SqliteDialect({ database: sql, diff --git a/app/features/tournament-organization/TournamentOrganizationRepository.server.ts b/app/features/tournament-organization/TournamentOrganizationRepository.server.ts index 16eca1d31..f5be8ed33 100644 --- a/app/features/tournament-organization/TournamentOrganizationRepository.server.ts +++ b/app/features/tournament-organization/TournamentOrganizationRepository.server.ts @@ -200,7 +200,13 @@ export function searchByName({ "avatarUrl", ), ]) - .where("TournamentOrganization.name", "like", `%${query}%`) + .where(({ eb, ref }) => + eb( + sql`unaccent(${ref("TournamentOrganization.name")})`, + "like", + sql`unaccent(${`%${query}%`})`, + ), + ) .orderBy("TournamentOrganization.name", "asc") .limit(limit) .execute(); diff --git a/app/modules/in-game-lists/utils.test.ts b/app/modules/in-game-lists/utils.test.ts new file mode 100644 index 000000000..aa63d6c92 --- /dev/null +++ b/app/modules/in-game-lists/utils.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "vitest"; +import type { MainWeaponId } from "./types"; +import { filterWeapon } from "./utils"; + +describe("filterWeapon", () => { + const sBlast = { type: "MAIN" as const, id: 260 as MainWeaponId }; + + test("matches ignoring hyphens (e.g. 's blast' finds 'S-BLAST')", () => { + expect( + filterWeapon({ + weapon: sBlast, + weaponName: "S-BLAST '92", + searchTerm: "s blast", + }), + ).toBe(true); + }); + + test("matches with the hyphen still present", () => { + expect( + filterWeapon({ + weapon: sBlast, + weaponName: "S-BLAST '92", + searchTerm: "s-blast", + }), + ).toBe(true); + }); + + test("matches ignoring case", () => { + expect( + filterWeapon({ + weapon: sBlast, + weaponName: "S-BLAST '92", + searchTerm: "SBLAST", + }), + ).toBe(true); + }); + + test("does not match unrelated weapon", () => { + expect( + filterWeapon({ + weapon: sBlast, + weaponName: "S-BLAST '92", + searchTerm: "splattershot", + }), + ).toBe(false); + }); +}); diff --git a/app/modules/in-game-lists/utils.ts b/app/modules/in-game-lists/utils.ts index 2a5333a3c..1f60aab1e 100644 --- a/app/modules/in-game-lists/utils.ts +++ b/app/modules/in-game-lists/utils.ts @@ -8,7 +8,11 @@ export function isAbility(value: string): value is Ability { } const normalizeTerm = (term: string): string => { - return term.trim().toLocaleLowerCase(); + return term + .normalize("NFD") + .replace(/\p{M}/gu, "") + .replace(/[^\p{L}\p{N}]/gu, "") + .toLocaleLowerCase(); }; export function filterWeapon({