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 = "";

View File

@@ -71,6 +71,16 @@
<p id="dataModeInfo" class="data-mode"></p>
</div>
<div class="search-zone">
<label>
Recherche rapide anti-doublon
<input id="quickSearchInput" placeholder="Ex: Destiny, Final Fantasy, Zelda..." />
</label>
<div id="quickSearchResults" class="quick-search-results">
<p class="empty">Saisis un titre pour verifier si tu possedes deja le jeu.</p>
</div>
</div>
<form id="gameForm" class="grid-form game-form">
<label>
Titre

View File

@@ -153,6 +153,44 @@ h1 {
margin-bottom: 0.9rem;
}
.search-zone {
border: 1px solid var(--border);
border-radius: 12px;
padding: 0.75rem;
margin-bottom: 0.9rem;
background: #f9fbff;
}
.quick-search-results {
margin-top: 0.65rem;
display: grid;
gap: 0.4rem;
}
.search-hit {
border: 1px solid #d9e3ef;
border-radius: 10px;
padding: 0.5rem 0.6rem;
background: #ffffff;
}
.search-hit strong {
display: inline-block;
margin-bottom: 0.15rem;
}
.search-hit p {
margin: 0;
font-size: 0.88rem;
color: var(--muted);
}
.search-status {
font-weight: 600;
color: #1f4466;
margin: 0;
}
.grid-form {
display: grid;
gap: 0.7rem;