Feature: add global anti-duplicate game search
This commit is contained in:
99
app.js
99
app.js
@@ -46,10 +46,13 @@ const backupBtn = document.getElementById("backupBtn");
|
|||||||
const restoreMergeBtn = document.getElementById("restoreMergeBtn");
|
const restoreMergeBtn = document.getElementById("restoreMergeBtn");
|
||||||
const restoreReplaceBtn = document.getElementById("restoreReplaceBtn");
|
const restoreReplaceBtn = document.getElementById("restoreReplaceBtn");
|
||||||
const restoreFileInput = document.getElementById("restoreFileInput");
|
const restoreFileInput = document.getElementById("restoreFileInput");
|
||||||
|
const quickSearchInput = document.getElementById("quickSearchInput");
|
||||||
|
const quickSearchResults = document.getElementById("quickSearchResults");
|
||||||
const gamesList = document.getElementById("gamesList");
|
const gamesList = document.getElementById("gamesList");
|
||||||
const gameCardTemplate = document.getElementById("gameCardTemplate");
|
const gameCardTemplate = document.getElementById("gameCardTemplate");
|
||||||
let editingGameId = null;
|
let editingGameId = null;
|
||||||
let pendingRestoreMode = "merge";
|
let pendingRestoreMode = "merge";
|
||||||
|
let quickSearchTerm = "";
|
||||||
|
|
||||||
toolsToggleBtn.addEventListener("click", () => {
|
toolsToggleBtn.addEventListener("click", () => {
|
||||||
toolsDrawer.classList.toggle("open");
|
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) => {
|
platformForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -430,6 +438,7 @@ function render() {
|
|||||||
renderBrandTabs();
|
renderBrandTabs();
|
||||||
renderConsoleTabs();
|
renderConsoleTabs();
|
||||||
renderGames();
|
renderGames();
|
||||||
|
renderSearchResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDataMode() {
|
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("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
function renderBrandTabs() {
|
function renderBrandTabs() {
|
||||||
const brands = Object.keys(state.brands);
|
const brands = Object.keys(state.brands);
|
||||||
brandTabs.innerHTML = "";
|
brandTabs.innerHTML = "";
|
||||||
|
|||||||
10
index.html
10
index.html
@@ -71,6 +71,16 @@
|
|||||||
<p id="dataModeInfo" class="data-mode"></p>
|
<p id="dataModeInfo" class="data-mode"></p>
|
||||||
</div>
|
</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">
|
<form id="gameForm" class="grid-form game-form">
|
||||||
<label>
|
<label>
|
||||||
Titre
|
Titre
|
||||||
|
|||||||
38
styles.css
38
styles.css
@@ -153,6 +153,44 @@ h1 {
|
|||||||
margin-bottom: 0.9rem;
|
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 {
|
.grid-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.7rem;
|
gap: 0.7rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user