Feature: add global anti-duplicate game search

This commit is contained in:
Ponte
2026-02-11 19:40:23 +01:00
parent e678956f19
commit ce40912629
3 changed files with 147 additions and 0 deletions

99
app.js
View File

@@ -46,10 +46,13 @@ const backupBtn = document.getElementById("backupBtn");
const restoreMergeBtn = document.getElementById("restoreMergeBtn");
const restoreReplaceBtn = document.getElementById("restoreReplaceBtn");
const restoreFileInput = document.getElementById("restoreFileInput");
const quickSearchInput = document.getElementById("quickSearchInput");
const quickSearchResults = document.getElementById("quickSearchResults");
const gamesList = document.getElementById("gamesList");
const gameCardTemplate = document.getElementById("gameCardTemplate");
let editingGameId = null;
let pendingRestoreMode = "merge";
let quickSearchTerm = "";
toolsToggleBtn.addEventListener("click", () => {
toolsDrawer.classList.toggle("open");
@@ -78,6 +81,11 @@ document.addEventListener("keydown", (event) => {
}
});
quickSearchInput.addEventListener("input", (event) => {
quickSearchTerm = event.target.value.trim();
renderSearchResults();
});
platformForm.addEventListener("submit", async (event) => {
event.preventDefault();
@@ -430,6 +438,7 @@ function render() {
renderBrandTabs();
renderConsoleTabs();
renderGames();
renderSearchResults();
}
function renderDataMode() {
@@ -482,6 +491,96 @@ function renderDataMode() {
}
}
function findBrandByConsole(consoleName) {
for (const [brand, consoles] of Object.entries(state.brands)) {
if (Array.isArray(consoles) && consoles.includes(consoleName)) {
return brand;
}
}
return "INCONNUE";
}
function collectAllGames() {
const all = [];
for (const [consoleName, games] of Object.entries(state.gamesByConsole)) {
const brand = findBrandByConsole(consoleName);
for (const game of games || []) {
all.push({ ...game, consoleName, brand });
}
}
return all;
}
function renderSearchResults() {
if (!quickSearchResults) {
return;
}
const query = quickSearchTerm.trim().toLowerCase();
if (!query) {
quickSearchResults.innerHTML =
'<p class="empty">Saisis un titre pour verifier si tu possedes deja le jeu.</p>';
return;
}
const normalizedQuery = query.replace(/\s+/g, " ");
const allGames = collectAllGames();
const matches = allGames.filter((game) => {
const title = String(game.title || "").toLowerCase();
return title.includes(normalizedQuery);
});
const exactMatches = allGames.filter((game) => {
const title = String(game.title || "")
.toLowerCase()
.replace(/\s+/g, " ")
.trim();
return title === normalizedQuery;
});
if (!matches.length) {
quickSearchResults.innerHTML =
'<p class="search-status">Aucun jeu trouve dans ta collection.</p>';
return;
}
const maxShown = 20;
const shown = matches.slice(0, maxShown);
const header =
exactMatches.length > 0
? `<p class="search-status">Deja possede: OUI (${exactMatches.length} correspondance${exactMatches.length > 1 ? "s" : ""} exacte${exactMatches.length > 1 ? "s" : ""}).</p>`
: `<p class="search-status">Jeu similaire trouve: ${matches.length} resultat${matches.length > 1 ? "s" : ""}.</p>`;
const items = shown
.map((game) => {
const meta = [
`${game.brand} / ${game.consoleName}`,
game.version ? `Version: ${game.version}` : null,
game.isDuplicate ? "Double: OUI" : null,
]
.filter(Boolean)
.join(" | ");
return `<article class="search-hit"><strong>${escapeHtml(game.title || "")}</strong><p>${escapeHtml(meta)}</p></article>`;
})
.join("");
const more =
matches.length > maxShown
? `<p class="empty">+${matches.length - maxShown} autre(s) resultat(s).</p>`
: "";
quickSearchResults.innerHTML = `${header}${items}${more}`;
}
function escapeHtml(text) {
return String(text)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function renderBrandTabs() {
const brands = Object.keys(state.brands);
brandTabs.innerHTML = "";