${escapeHtml(meta)}
const STORAGE_KEY = "video_game_collection_v1"; const initialState = { brands: { SONY: ["PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4", "PlayStation 5"], MICROSOFT: ["Xbox", "Xbox 360", "Xbox One", "Xbox One X", "Xbox Series X"], }, gamesByConsole: {}, selectedBrand: "SONY", selectedConsole: "PlayStation", }; const state = loadState(); let pendingLocalImport = payloadHasCatalogData(state) ? structuredClone(state) : null; let dataMode = "local"; let apiReachable = false; const platformForm = document.getElementById("platformForm"); const gameForm = document.getElementById("gameForm"); const brandInput = document.getElementById("brandInput"); const consoleInput = document.getElementById("consoleInput"); const titleInput = document.getElementById("titleInput"); const barcodeInput = document.getElementById("barcodeInput"); const versionInput = document.getElementById("versionInput"); const genreInput = document.getElementById("genreInput"); const publisherInput = document.getElementById("publisherInput"); const coverFileInput = document.getElementById("coverFileInput"); const coverUrlInput = document.getElementById("coverUrlInput"); const yearInput = document.getElementById("yearInput"); const valueInput = document.getElementById("valueInput"); const purchasePriceInput = document.getElementById("purchasePriceInput"); const conditionInput = document.getElementById("conditionInput"); const isDuplicateInput = document.getElementById("isDuplicateInput"); const loanedToInput = document.getElementById("loanedToInput"); const gameSubmitBtn = document.getElementById("gameSubmitBtn"); const cancelEditBtn = document.getElementById("cancelEditBtn"); const brandTabs = document.getElementById("brandTabs"); const consoleTabs = document.getElementById("consoleTabs"); const gameSectionTitle = document.getElementById("gameSectionTitle"); const dataModeInfo = document.getElementById("dataModeInfo"); const storageState = document.getElementById("storageState"); const toolsDrawer = document.getElementById("toolsDrawer"); const toolsToggleBtn = document.getElementById("toolsToggleBtn"); const toolsCloseBtn = document.getElementById("toolsCloseBtn"); const gamesDrawer = document.getElementById("gamesDrawer"); const gamesToggleBtn = document.getElementById("gamesToggleBtn"); const gamesCloseBtn = document.getElementById("gamesCloseBtn"); const totalGamesCount = document.getElementById("totalGamesCount"); const totalGamesValue = document.getElementById("totalGamesValue"); const migrateBtn = document.getElementById("migrateBtn"); const backupControls = document.getElementById("backupControls"); const backupBtn = document.getElementById("backupBtn"); const restoreMergeBtn = document.getElementById("restoreMergeBtn"); const restoreReplaceBtn = document.getElementById("restoreReplaceBtn"); const restoreFileInput = document.getElementById("restoreFileInput"); const googleDriveState = document.getElementById("googleDriveState"); const googleConnectBtn = document.getElementById("googleConnectBtn"); const googleBackupBtn = document.getElementById("googleBackupBtn"); const googleRestoreBtn = document.getElementById("googleRestoreBtn"); const quickSearchInput = document.getElementById("quickSearchInput"); const quickSearchResults = document.getElementById("quickSearchResults"); const loanedFilterBtn = document.getElementById("loanedFilterBtn"); const v2Toolbar = document.getElementById("v2Toolbar"); const v2SearchInput = document.getElementById("v2SearchInput"); const v2ConsoleFilter = document.getElementById("v2ConsoleFilter"); const v2GenreFilter = document.getElementById("v2GenreFilter"); const v2SortSelect = document.getElementById("v2SortSelect"); const v2StickyBar = document.getElementById("v2StickyBar"); const v2StickyCount = document.getElementById("v2StickyCount"); const v2ToggleFormBtn = document.getElementById("v2ToggleFormBtn"); const v2QuickBackupBtn = document.getElementById("v2QuickBackupBtn"); const toastContainer = document.getElementById("toastContainer"); const scannerStatus = document.getElementById("scannerStatus"); const scannerStartBtn = document.getElementById("scannerStartBtn"); const scannerStopBtn = document.getElementById("scannerStopBtn"); const scannerVideo = document.getElementById("scannerVideo"); const scannerLastCode = document.getElementById("scannerLastCode"); const gamesList = document.getElementById("gamesList"); const gameCardTemplate = document.getElementById("gameCardTemplate"); let editingGameId = null; let inlineEditingGameId = null; let pendingRestoreMode = "merge"; let quickSearchTerm = ""; let googleStatus = { configured: false, connected: false, email: "" }; let showLoanedOnly = false; let scannerDetector = null; let scannerStream = null; let scannerRunning = false; let scannerLoopId = null; let scannerLastCodeValue = ""; let scannerLastCodeAt = 0; // V2 is now the default UI. Use ?ui=v1 to force legacy mode if needed. const uiParam = new URLSearchParams(window.location.search).get("ui"); const uiV2Enabled = uiParam !== "v1"; let v2SearchTerm = ""; let v2ConsoleValue = ""; let v2GenreValue = ""; let v2SortValue = "title_asc"; let v2FormCollapsed = false; coverFileInput.addEventListener("change", async (event) => { const input = event.target; const file = input.files && input.files[0] ? input.files[0] : null; if (!file) { return; } if (!file.type.startsWith("image/")) { alert("Le fichier doit etre une image."); input.value = ""; return; } try { const dataUrl = await imageFileToOptimizedDataUrl(file); coverUrlInput.value = dataUrl; } catch (error) { console.error(error); alert("Impossible de charger/comprimer cette image."); } finally { input.value = ""; } }); toolsToggleBtn.addEventListener("click", () => { gamesDrawer.classList.remove("open"); toolsDrawer.classList.toggle("open"); }); toolsCloseBtn.addEventListener("click", () => { toolsDrawer.classList.remove("open"); }); gamesToggleBtn.addEventListener("click", () => { toolsDrawer.classList.remove("open"); gamesDrawer.classList.toggle("open"); }); gamesCloseBtn.addEventListener("click", () => { gamesDrawer.classList.remove("open"); }); document.addEventListener("click", (event) => { if (!(event.target instanceof Element)) { return; } const toolsOpen = toolsDrawer.classList.contains("open"); const gamesOpen = gamesDrawer.classList.contains("open"); if (!toolsOpen && !gamesOpen) { return; } const clickedInsideTools = toolsDrawer.contains(event.target) || toolsToggleBtn.contains(event.target); const clickedInsideGames = gamesDrawer.contains(event.target) || gamesToggleBtn.contains(event.target); if (clickedInsideTools || clickedInsideGames) { return; } toolsDrawer.classList.remove("open"); gamesDrawer.classList.remove("open"); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { toolsDrawer.classList.remove("open"); gamesDrawer.classList.remove("open"); if (scannerRunning) { stopScanner("Camera arretee."); } } }); googleConnectBtn.addEventListener("click", async () => { try { const payload = await apiRequest("/api/google/connect-url"); if (!payload.url) { throw new Error("Missing Google connect URL"); } window.location.href = payload.url; } catch (error) { console.error(error); alert("Connexion Google indisponible. Verifie la configuration OAuth."); } }); googleBackupBtn.addEventListener("click", async () => { try { const payload = await apiRequest("/api/google/backup/upload", { method: "POST" }); await refreshGoogleStatus(); alert(`Sauvegarde Drive OK: ${payload.fileName || "fichier cree"}.`); } catch (error) { console.error(error); alert("Echec sauvegarde Google Drive."); } }); googleRestoreBtn.addEventListener("click", async () => { const confirmed = window.confirm("Restaurer depuis le dernier backup Google Drive ?"); if (!confirmed) { return; } try { const payload = await apiRequest("/api/google/backup/restore", { method: "POST", body: { mode: "merge" }, }); await refreshFromApi(state.selectedBrand, state.selectedConsole); alert(`Restauration Drive OK (${payload.mode})`); } catch (error) { console.error(error); alert("Echec restauration Google Drive."); } }); quickSearchInput.addEventListener("input", (event) => { quickSearchTerm = event.target.value.trim(); renderSearchResults(); }); loanedFilterBtn.addEventListener("click", () => { showLoanedOnly = !showLoanedOnly; renderGames(); }); if (v2SearchInput) { v2SearchInput.addEventListener("input", (event) => { v2SearchTerm = event.target.value.trim(); renderGames(); }); } if (v2ConsoleFilter) { v2ConsoleFilter.addEventListener("change", (event) => { v2ConsoleValue = event.target.value; renderGames(); }); } if (v2GenreFilter) { v2GenreFilter.addEventListener("change", (event) => { v2GenreValue = event.target.value; renderGames(); }); } if (v2SortSelect) { v2SortSelect.addEventListener("change", (event) => { v2SortValue = event.target.value || "title_asc"; renderGames(); }); } if (v2ToggleFormBtn) { v2ToggleFormBtn.addEventListener("click", () => { v2FormCollapsed = !v2FormCollapsed; gameForm.classList.toggle("hidden", v2FormCollapsed); v2ToggleFormBtn.textContent = v2FormCollapsed ? "Afficher formulaire" : "Ajouter"; }); } if (v2QuickBackupBtn) { v2QuickBackupBtn.addEventListener("click", () => { backupBtn.click(); }); } if (scannerStartBtn) { scannerStartBtn.addEventListener("click", async () => { await startScanner(); }); } if (scannerStopBtn) { scannerStopBtn.addEventListener("click", () => { stopScanner("Camera arretee."); }); } platformForm.addEventListener("submit", async (event) => { event.preventDefault(); const brand = brandInput.value.trim().toUpperCase(); const consoleName = consoleInput.value.trim(); if (!brand || !consoleName) { return; } if (apiReachable && dataMode !== "local-pending-import") { try { await apiRequest("/api/catalog/consoles", { method: "POST", body: { brand, consoleName }, }); platformForm.reset(); await refreshFromApi(brand, consoleName); return; } catch (error) { console.error(error); alert("Impossible d'ajouter cette section via l'API."); } } state.brands[brand] = state.brands[brand] || []; if (!state.brands[brand].includes(consoleName)) { state.brands[brand].push(consoleName); } state.selectedBrand = brand; state.selectedConsole = consoleName; state.gamesByConsole[consoleName] = state.gamesByConsole[consoleName] || []; platformForm.reset(); persist(); markLocalDataForImport(); render(); }); gameForm.addEventListener("submit", async (event) => { event.preventDefault(); const wasEditing = Boolean(editingGameId); const title = titleInput.value.trim(); if (!title || !state.selectedConsole) { return; } if (apiReachable && dataMode !== "local-pending-import") { const payload = { brand: state.selectedBrand, consoleName: state.selectedConsole, title, barcode: barcodeInput.value.trim(), version: versionInput.value.trim(), genre: genreInput.value.trim(), publisher: publisherInput.value.trim(), coverUrl: coverUrlInput.value.trim(), isDuplicate: isDuplicateInput.checked, year: yearInput.value ? Number(yearInput.value) : null, purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null, value: valueInput.value ? Number(valueInput.value) : null, condition: conditionInput.value ? Number(conditionInput.value) : null, loanedTo: loanedToInput.value.trim(), }; try { if (editingGameId) { await apiRequest(`/api/catalog/games/${editingGameId}`, { method: "PUT", body: payload, }); } else { await apiRequest("/api/catalog/games", { method: "POST", body: payload, }); } resetEditMode(); await refreshFromApi(state.selectedBrand, state.selectedConsole); showToast(wasEditing ? "Jeu mis a jour." : "Jeu ajoute.", "success"); return; } catch (error) { console.error(error); alert(`Impossible d'enregistrer ce jeu via l'API: ${error.message}`); } } state.gamesByConsole[state.selectedConsole] = state.gamesByConsole[state.selectedConsole] || []; if (editingGameId) { const games = state.gamesByConsole[state.selectedConsole]; const idx = games.findIndex((game) => game.id === editingGameId); if (idx !== -1) { games[idx] = { ...games[idx], title, barcode: barcodeInput.value.trim(), version: versionInput.value.trim(), genre: genreInput.value.trim(), publisher: publisherInput.value.trim(), coverUrl: coverUrlInput.value.trim(), isDuplicate: isDuplicateInput.checked, year: yearInput.value ? Number(yearInput.value) : null, purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null, value: valueInput.value ? Number(valueInput.value) : null, condition: conditionInput.value ? Number(conditionInput.value) : null, loanedTo: loanedToInput.value.trim(), }; } } else { const game = { id: crypto.randomUUID(), title, barcode: barcodeInput.value.trim(), version: versionInput.value.trim(), genre: genreInput.value.trim(), publisher: publisherInput.value.trim(), coverUrl: coverUrlInput.value.trim(), isDuplicate: isDuplicateInput.checked, year: yearInput.value ? Number(yearInput.value) : null, purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null, value: valueInput.value ? Number(valueInput.value) : null, condition: conditionInput.value ? Number(conditionInput.value) : null, loanedTo: loanedToInput.value.trim(), createdAt: new Date().toISOString(), }; state.gamesByConsole[state.selectedConsole].unshift(game); } resetEditMode(); persist(); markLocalDataForImport(); render(); showToast(wasEditing ? "Jeu mis a jour." : "Jeu ajoute.", "success"); }); brandTabs.addEventListener("click", (event) => { if (!(event.target instanceof Element)) { return; } const target = event.target.closest("button[data-brand]"); if (!(target instanceof HTMLButtonElement)) { return; } const brand = target.dataset.brand; if (!brand) { return; } state.selectedBrand = brand; const consoles = state.brands[brand] || []; state.selectedConsole = consoles[0] || ""; resetEditMode(); persist(); render(); }); consoleTabs.addEventListener("click", (event) => { if (!(event.target instanceof Element)) { return; } const target = event.target.closest("button[data-console]"); if (!(target instanceof HTMLButtonElement)) { return; } const consoleName = target.dataset.console; if (!consoleName) { return; } state.selectedConsole = consoleName; resetEditMode(); persist(); render(); }); gamesList.addEventListener("click", async (event) => { if (!(event.target instanceof Element)) { return; } const target = event.target.closest("button[data-action]"); if (!(target instanceof HTMLButtonElement)) { return; } const action = target.dataset.action; const id = target.dataset.id; if (!action || !id) { return; } const gameRef = findGameById(id); if (!gameRef) { return; } const { game, games, idx, consoleName, brand } = gameRef; if (action === "edit") { inlineEditingGameId = inlineEditingGameId === id ? null : id; render(); return; } if (action === "inline-cancel") { inlineEditingGameId = null; renderGames(); return; } if (action === "inline-save") { const article = target.closest(".game-card"); if (!(article instanceof HTMLElement)) { return; } const getInlineValue = (key) => { const input = article.querySelector(`[data-inline="${key}"]`); return input instanceof HTMLInputElement ? input.value.trim() : ""; }; const getInlineNumber = (key) => { const value = getInlineValue(key); if (!value) { return null; } const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; }; const getInlineChecked = (key) => { const input = article.querySelector(`[data-inline="${key}"]`); return input instanceof HTMLInputElement ? input.checked : false; }; const title = getInlineValue("title"); if (!title) { alert("Le titre est obligatoire."); return; } const updatedFields = { title, barcode: getInlineValue("barcode"), version: getInlineValue("version"), genre: getInlineValue("genre"), publisher: getInlineValue("publisher"), year: getInlineNumber("year"), purchasePrice: getInlineNumber("purchasePrice"), value: getInlineNumber("value"), condition: getInlineNumber("condition"), loanedTo: getInlineValue("loanedTo"), isDuplicate: getInlineChecked("isDuplicate"), coverUrl: game.coverUrl || "", }; if (apiReachable && dataMode !== "local-pending-import") { try { const payload = buildGamePayload(game, brand, consoleName, updatedFields); await apiRequest(`/api/catalog/games/${id}`, { method: "PUT", body: payload, }); inlineEditingGameId = null; await refreshFromApi(state.selectedBrand, state.selectedConsole); return; } catch (error) { console.error(error); alert("Mise a jour impossible via l'API."); return; } } games[idx] = { ...games[idx], ...updatedFields, }; inlineEditingGameId = null; persist(); markLocalDataForImport(); render(); showToast("Jeu mis a jour.", "success"); return; } if (apiReachable && dataMode !== "local-pending-import") { if (action === "delete") { const confirmed = window.confirm("Etes-vous sur de vouloir supprimer ce jeu ?"); if (!confirmed) { return; } } try { if (action === "delete") { await apiRequest(`/api/catalog/games/${id}`, { method: "DELETE" }); if (editingGameId === id) { resetEditMode(); } } if (action === "toggle-loan") { if (game.loanedTo) { await apiRequest(`/api/catalog/games/${id}/toggle-loan`, { method: "POST" }); } else { const borrower = window.prompt("Nom de la personne a qui tu pretes ce jeu :"); if (borrower === null) { return; } const loanedTo = borrower.trim(); if (!loanedTo) { alert("Le nom est obligatoire pour marquer le jeu comme prete."); return; } const payload = buildGamePayload(game, brand, consoleName, { loanedTo }); await apiRequest(`/api/catalog/games/${id}`, { method: "PUT", body: payload, }); } } await refreshFromApi(state.selectedBrand, state.selectedConsole); showToast("Action enregistree.", "success"); return; } catch (error) { console.error(error); alert("Action impossible via l'API."); } } if (action === "delete") { const confirmed = window.confirm("Etes-vous sur de vouloir supprimer ce jeu ?"); if (!confirmed) { return; } games.splice(idx, 1); if (editingGameId === id) { resetEditMode(); } } if (action === "toggle-loan") { if (games[idx].loanedTo) { games[idx].loanedTo = ""; } else { const borrower = window.prompt("Nom de la personne a qui tu pretes ce jeu :"); if (borrower === null) { return; } const loanedTo = borrower.trim(); if (!loanedTo) { alert("Le nom est obligatoire pour marquer le jeu comme prete."); return; } games[idx].loanedTo = loanedTo; } } persist(); markLocalDataForImport(); render(); showToast("Action enregistree.", "success"); }); cancelEditBtn.addEventListener("click", () => { resetEditMode(); }); migrateBtn.addEventListener("click", async () => { if (!apiReachable || !pendingLocalImport || !payloadHasCatalogData(pendingLocalImport)) { return; } const confirmed = window.confirm( "Importer les donnees locales dans la base de donnees ? (deduplication par console + titre + annee)", ); if (!confirmed) { return; } try { const result = await apiRequest("/api/catalog/import", { method: "POST", body: pendingLocalImport, }); pendingLocalImport = null; await refreshFromApi(state.selectedBrand, state.selectedConsole); alert(`Migration terminee: ${result.insertedGames || 0} jeu(x) importe(s).`); } catch (error) { console.error(error); alert("Echec de la migration locale vers DB."); } }); backupBtn.addEventListener("click", async () => { if (!apiReachable) { alert("API indisponible. Sauvegarde JSON impossible."); return; } try { const dump = await apiRequest("/api/backup/export"); const blob = new Blob([JSON.stringify(dump, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); const stamp = new Date().toISOString().replace(/[:.]/g, "-"); a.href = url; a.download = `video-games-backup-${stamp}.json`; document.body.append(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch (error) { console.error(error); alert("Echec de la sauvegarde JSON."); } }); restoreMergeBtn.addEventListener("click", () => { pendingRestoreMode = "merge"; restoreFileInput.click(); }); restoreReplaceBtn.addEventListener("click", () => { pendingRestoreMode = "replace"; restoreFileInput.click(); }); restoreFileInput.addEventListener("change", async (event) => { const input = event.target; const file = input.files && input.files[0] ? input.files[0] : null; input.value = ""; if (!file) { return; } if (!apiReachable) { alert("API indisponible. Restauration impossible."); return; } try { const fileText = await file.text(); const dump = JSON.parse(fileText); if (pendingRestoreMode === "replace") { const confirmed = window.confirm( "Mode remplacement: la base actuelle sera remplacee. Une sauvegarde pre-restore sera creee. Continuer ?", ); if (!confirmed) { return; } } const result = await apiRequest("/api/backup/restore", { method: "POST", body: { mode: pendingRestoreMode, dump, }, }); pendingLocalImport = null; await refreshFromApi(state.selectedBrand, state.selectedConsole); alert( `Restauration terminee (${result.mode}): ${result.insertedGames || 0} jeu(x), ${result.insertedConsoles || 0} console(s).`, ); } catch (error) { console.error(error); alert("Echec de la restauration JSON."); } }); function render() { renderV2Chrome(); renderDataMode(); renderGoogleStatus(); renderBrandTabs(); renderConsoleTabs(); updateV2FilterOptions(); renderGames(); renderSearchResults(); renderCollectionStats(); } function renderDataMode() { if (!dataModeInfo) { return; } if (dataMode === "api") { dataModeInfo.textContent = "Source: API (lecture/ecriture active sur la base de donnees)."; if (storageState) { storageState.textContent = "Etat: Base de donnees active"; } } else if (dataMode === "api-empty") { dataModeInfo.textContent = "Source: API (base vide). Ajoute une section pour demarrer."; if (storageState) { storageState.textContent = "Etat: Base de donnees active (vide)"; } } else if (dataMode === "local-pending-import") { dataModeInfo.textContent = "Source: localStorage detectee. Migration vers DB disponible."; if (storageState) { storageState.textContent = "Etat: LocalStorage (migration en attente)"; } } else if (dataMode === "local-fallback") { dataModeInfo.textContent = "Source: localStorage (fallback). API indisponible."; if (storageState) { storageState.textContent = "Etat: LocalStorage (API indisponible)"; } } else { dataModeInfo.textContent = "Source: localStorage"; if (storageState) { storageState.textContent = "Etat: LocalStorage"; } } const showMigrateBtn = dataMode === "local-pending-import" && apiReachable && pendingLocalImport && payloadHasCatalogData(pendingLocalImport); if (showMigrateBtn) { migrateBtn.classList.remove("hidden"); } else { migrateBtn.classList.add("hidden"); } if (apiReachable) { backupControls.classList.remove("hidden"); } else { backupControls.classList.add("hidden"); } } function renderGoogleStatus() { if (!googleDriveState) { return; } if (!googleStatus.configured) { googleDriveState.textContent = "Etat Google Drive: non configure (OAuth manquant)."; googleConnectBtn.disabled = true; googleBackupBtn.disabled = true; googleRestoreBtn.disabled = true; return; } if (!googleStatus.connected) { googleDriveState.textContent = "Etat Google Drive: non connecte."; googleConnectBtn.disabled = false; googleBackupBtn.disabled = true; googleRestoreBtn.disabled = true; return; } const email = googleStatus.email ? ` (${googleStatus.email})` : ""; googleDriveState.textContent = `Etat Google Drive: connecte${email}.`; googleConnectBtn.disabled = false; googleBackupBtn.disabled = false; googleRestoreBtn.disabled = false; } function findBrandByConsole(consoleName) { for (const [brand, consoles] of Object.entries(state.brands)) { if (Array.isArray(consoles) && consoles.includes(consoleName)) { return brand; } } return "INCONNUE"; } function findGameById(id) { for (const [consoleName, games] of Object.entries(state.gamesByConsole)) { const idx = (games || []).findIndex((game) => game.id === id); if (idx !== -1) { return { consoleName, brand: findBrandByConsole(consoleName), games, idx, game: games[idx], }; } } return null; } function buildGamePayload(game, brand, consoleName, overrides = {}) { return { brand, consoleName, title: game.title || "", barcode: game.barcode || "", version: game.version || "", genre: game.genre || "", publisher: game.publisher || "", coverUrl: game.coverUrl || "", isDuplicate: Boolean(game.isDuplicate), year: game.year != null ? Number(game.year) : null, purchasePrice: game.purchasePrice != null ? Number(game.purchasePrice) : null, value: game.value != null ? Number(game.value) : null, condition: game.condition != null ? Number(game.condition) : null, loanedTo: game.loanedTo || "", ...overrides, }; } 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 renderCollectionStats() { if (!totalGamesCount || !totalGamesValue) { return; } const allGames = collectAllGames(); const totalCount = allGames.length; const totalValue = allGames.reduce((sum, game) => { const value = typeof game.value === "number" ? game.value : Number(game.value); return Number.isFinite(value) ? sum + value : sum; }, 0); totalGamesCount.textContent = String(totalCount); totalGamesValue.textContent = `${totalValue.toFixed(2)} EUR`; } function showToast(message, type = "info", timeoutMs = 2600) { if (!toastContainer || !message) { return; } const toast = document.createElement("div"); toast.className = `toast ${type}`.trim(); toast.textContent = message; toastContainer.append(toast); window.setTimeout(() => { toast.remove(); }, timeoutMs); } function renderV2Chrome() { if (!uiV2Enabled) { return; } document.body.classList.add("ui-v2"); if (v2Toolbar) { v2Toolbar.classList.remove("hidden"); } if (v2StickyBar) { v2StickyBar.classList.remove("hidden"); } } function updateV2FilterOptions() { if (!uiV2Enabled || !v2ConsoleFilter || !v2GenreFilter) { return; } const allGames = collectAllGames(); const consoles = Array.from(new Set(allGames.map((game) => normalizeText(game.consoleName)).filter(Boolean))).sort(); const genres = Array.from(new Set(allGames.flatMap((game) => splitGenres(game.genre)))).sort(); v2ConsoleFilter.innerHTML = `${consoles .map((consoleName) => ``) .join("")}`; v2GenreFilter.innerHTML = `${genres .map((genre) => ``) .join("")}`; v2ConsoleFilter.value = v2ConsoleValue; v2GenreFilter.value = v2GenreValue; v2SortSelect.value = v2SortValue; } function splitGenres(genreRaw) { return normalizeText(genreRaw) .split(/[\/,|]/) .map((item) => normalizeText(item)) .filter(Boolean); } function badgeClassForGenre(genreValue) { const normalized = normalizeText(genreValue).toLowerCase(); if (normalized.includes("rpg")) { return "rpg"; } if (normalized.includes("action") || normalized.includes("aventure") || normalized.includes("adventure")) { return "action"; } if (normalized.includes("sport") || normalized.includes("football") || normalized.includes("course")) { return "sport"; } if (normalized.includes("racing") || normalized.includes("kart")) { return "racing"; } return "default"; } function consoleThemeClass(consoleName) { const normalized = normalizeText(consoleName).toLowerCase(); if (normalized.includes("playstation 5") || normalized === "ps5") { return "console-theme-ps5"; } if (normalized.includes("playstation 4") || normalized === "ps4") { return "console-theme-ps4"; } if (normalized.includes("playstation 3") || normalized === "ps3") { return "console-theme-ps3"; } if (normalized.includes("playstation 2") || normalized === "ps2") { return "console-theme-ps2"; } if (normalized.includes("playstation") || normalized === "ps1") { return "console-theme-ps1"; } if (normalized.includes("switch")) { return "console-theme-switch"; } if (normalized.includes("xbox")) { return "console-theme-xbox"; } if (normalized.includes("wii")) { return "console-theme-wii"; } if (normalized.includes("snes")) { return "console-theme-snes"; } if (normalized.includes("nes")) { return "console-theme-nes"; } return "console-theme-default"; } function conditionBadgeClass(conditionValue) { if (conditionValue == null || Number.isNaN(Number(conditionValue))) { return "status-neutral"; } const value = Number(conditionValue); if (value >= 9) { return "status-good"; } if (value >= 7) { return "status-medium"; } if (value >= 5) { return "status-warning"; } return "status-low"; } function renderSearchResults() { if (!quickSearchResults) { return; } const query = quickSearchTerm.trim().toLowerCase(); if (!query) { quickSearchResults.innerHTML = '
Saisis un titre pour verifier si tu possedes deja le jeu.
'; 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 = 'Aucun jeu trouve dans ta collection.
'; return; } const maxShown = 20; const shown = matches.slice(0, maxShown); const header = exactMatches.length > 0 ? `Deja possede: OUI (${exactMatches.length} correspondance${exactMatches.length > 1 ? "s" : ""} exacte${exactMatches.length > 1 ? "s" : ""}).
` : `Jeu similaire trouve: ${matches.length} resultat${matches.length > 1 ? "s" : ""}.
`; 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 `${escapeHtml(meta)}
+${matches.length - maxShown} autre(s) resultat(s).
` : ""; quickSearchResults.innerHTML = `${header}${items}${more}`; } function escapeHtml(text) { return String(text) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function renderBrandTabs() { const brands = Object.keys(state.brands); brandTabs.innerHTML = ""; for (const brand of brands) { const button = document.createElement("button"); button.type = "button"; button.className = `tab ${state.selectedBrand === brand ? "active" : ""}`; button.textContent = brand; button.dataset.brand = brand; brandTabs.append(button); } } function renderConsoleTabs() { const consoles = state.brands[state.selectedBrand] || []; if (!consoles.includes(state.selectedConsole)) { state.selectedConsole = consoles[0] || ""; } consoleTabs.innerHTML = ""; for (const consoleName of consoles) { const button = document.createElement("button"); button.type = "button"; button.className = `tab ${state.selectedConsole === consoleName ? "active" : ""}`; button.dataset.console = consoleName; const count = (state.gamesByConsole[consoleName] || []).length; button.innerHTML = `${consoleName}${count}`; consoleTabs.append(button); } } function renderGames() { const selectedConsole = state.selectedConsole; const inV2 = uiV2Enabled; gameSectionTitle.textContent = inV2 ? "Catalogue jeux" : showLoanedOnly ? "Jeux pretes - Toutes consoles" : selectedConsole ? `Jeux - ${selectedConsole}` : "Jeux"; gamesList.innerHTML = ""; loanedFilterBtn.textContent = showLoanedOnly ? "Voir tous les jeux" : "Voir jeux pretes"; if (!inV2 && !showLoanedOnly && !selectedConsole) { gamesList.innerHTML = 'Ajoute une section pour commencer.
'; return; } let games = inV2 ? collectAllGames() : showLoanedOnly ? collectAllGames().filter((game) => normalizeText(game.loanedTo)) : state.gamesByConsole[selectedConsole] || []; if (showLoanedOnly) { games = games.filter((game) => normalizeText(game.loanedTo)); } if (inV2) { const searchTerm = v2SearchTerm.toLowerCase(); if (searchTerm) { games = games.filter((game) => { const fields = [ game.title, game.publisher, game.barcode, game.consoleName, game.genre, ] .map((value) => normalizeText(value).toLowerCase()) .join(" "); return fields.includes(searchTerm); }); } if (v2ConsoleValue) { games = games.filter((game) => normalizeText(game.consoleName) === v2ConsoleValue); } if (v2GenreValue) { games = games.filter((game) => splitGenres(game.genre).some((genre) => genre.toLowerCase() === v2GenreValue.toLowerCase()), ); } games.sort((a, b) => { if (v2SortValue === "year_desc") { return Number(b.year || 0) - Number(a.year || 0); } if (v2SortValue === "value_desc") { return Number(b.value || 0) - Number(a.value || 0); } return normalizeText(a.title).localeCompare(normalizeText(b.title), "fr", { sensitivity: "base" }); }); if (v2StickyCount) { v2StickyCount.textContent = `${games.length} jeu${games.length > 1 ? "x" : ""} affiche${games.length > 1 ? "s" : ""}`; } } if (!games.length) { gamesList.innerHTML = showLoanedOnly ? 'Aucun jeu prete actuellement.
' : 'Aucun jeu pour ces filtres.
'; return; } for (const game of games) { const card = gameCardTemplate.content.cloneNode(true); const article = card.querySelector(".game-card"); article.classList.add(consoleThemeClass(game.consoleName)); if (inlineEditingGameId === game.id) { article.classList.add("editing"); } card.querySelector(".game-title").textContent = game.title; const metaParts = inV2 ? [ game.publisher ? `Editeur: ${game.publisher}` : null, game.year ? `Annee: ${game.year}` : null, game.version ? `Version: ${game.version}` : null, game.value != null ? `Cote: ${Number(game.value).toFixed(2)} EUR` : null, game.consoleName ? `Console: ${game.consoleName}` : null, ].filter(Boolean) : [ showLoanedOnly ? `${game.brand} / ${game.consoleName}` : null, game.barcode ? `Code: ${game.barcode}` : null, game.version ? `Version: ${game.version}` : null, game.genre ? `Genre: ${game.genre}` : null, game.publisher ? `Editeur: ${game.publisher}` : null, game.isDuplicate ? "Double: OUI" : null, game.year ? `Annee: ${game.year}` : null, game.purchasePrice != null ? `Prix achat: ${game.purchasePrice.toFixed(2)} EUR` : null, game.value != null ? `Cote: ${game.value.toFixed(2)} EUR` : null, game.condition != null ? `Etat: ${game.condition}` : null, ].filter(Boolean); card.querySelector(".game-meta").textContent = metaParts.join(" | ") || "Aucune information complementaire"; const badgesContainer = card.querySelector(".game-badges"); const genres = splitGenres(game.genre); if (genres.length) { badgesContainer.innerHTML = genres .slice(0, 4) .map((genre) => `${escapeHtml(genre)}`) .join(""); } else { badgesContainer.innerHTML = ""; } if (game.condition != null && !Number.isNaN(Number(game.condition))) { badgesContainer.insertAdjacentHTML( "beforeend", `Etat ${escapeHtml(String(game.condition))}/10`, ); } const coverEl = card.querySelector(".game-cover"); const coverUrl = normalizeText(game.coverUrl); if (coverUrl) { coverEl.src = coverUrl; coverEl.classList.remove("hidden"); } else { coverEl.removeAttribute("src"); coverEl.classList.add("hidden"); } card.querySelector(".game-loan").textContent = game.loanedTo ? `Pret en cours: ${game.loanedTo}` : "Disponible dans ta collection"; const editBtn = card.querySelector('[data-action="edit"]'); const toggleBtn = card.querySelector('[data-action="toggle-loan"]'); const deleteBtn = card.querySelector('[data-action="delete"]'); editBtn.dataset.id = game.id; editBtn.textContent = "✏️ Editer"; editBtn.title = "Editer ce jeu"; editBtn.setAttribute("aria-label", "Editer ce jeu"); toggleBtn.dataset.id = game.id; toggleBtn.textContent = game.loanedTo ? "📥 Rendu" : "📤 Preter"; toggleBtn.title = game.loanedTo ? "Marquer comme rendu" : "Marquer comme prete"; toggleBtn.setAttribute("aria-label", toggleBtn.title); deleteBtn.dataset.id = game.id; deleteBtn.textContent = "🗑️ Supprimer"; deleteBtn.title = "Supprimer ce jeu"; deleteBtn.setAttribute("aria-label", "Supprimer ce jeu"); if (inlineEditingGameId === game.id) { const editor = document.createElement("div"); editor.className = "inline-editor"; editor.innerHTML = `