From 23352d85d08f073316a3b1b403a184a6132d48a8 Mon Sep 17 00:00:00 2001 From: Ponte Date: Sat, 14 Feb 2026 22:55:22 +0100 Subject: [PATCH] Feature: add game cover upload with compact thumbnails --- api/server.js | 29 ++++++++++++++++++++++------- app.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 6 ++++++ styles.css | 20 ++++++++++++++++++++ 4 files changed, 98 insertions(+), 7 deletions(-) diff --git a/api/server.js b/api/server.js index 9abb8ad..083a0a7 100644 --- a/api/server.js +++ b/api/server.js @@ -121,6 +121,7 @@ async function runMigrations() { condition_score NUMERIC(4,2), loaned_to TEXT, barcode TEXT, + cover_url TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); @@ -130,6 +131,7 @@ async function runMigrations() { await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS purchase_price NUMERIC(10,2);"); await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS condition_score NUMERIC(4,2);"); await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS barcode TEXT;"); + await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS cover_url TEXT;"); await pool.query(` CREATE TABLE IF NOT EXISTS backup_snapshots ( @@ -248,6 +250,7 @@ async function getCatalogFull() { g.condition_score, g.loaned_to, g.barcode, + g.cover_url, g.created_at FROM games g JOIN consoles c ON c.id = g.console_id @@ -280,6 +283,7 @@ async function getCatalogFull() { condition: row.condition_score != null ? Number(row.condition_score) : null, loanedTo: row.loaned_to || "", barcode: row.barcode || "", + coverUrl: row.cover_url || "", createdAt: row.created_at, }); } @@ -320,6 +324,7 @@ async function exportCatalogDumpWithClient(client) { g.condition_score, g.loaned_to, g.barcode, + g.cover_url, g.created_at, g.updated_at FROM games g @@ -360,6 +365,7 @@ async function exportCatalogDumpWithClient(client) { condition: row.condition_score != null ? Number(row.condition_score) : null, loanedTo: row.loaned_to || "", barcode: row.barcode || "", + coverUrl: row.cover_url || "", createdAt: row.created_at, updatedAt: row.updated_at, })), @@ -504,6 +510,7 @@ async function restoreCatalogDump(mode, dump) { const publisher = normalizeText(gameEntry && gameEntry.publisher) || null; const loanedTo = normalizeText(gameEntry && gameEntry.loanedTo) || null; const barcode = normalizeText(gameEntry && gameEntry.barcode) || null; + const coverUrl = normalizeText(gameEntry && gameEntry.coverUrl) || null; const isDuplicate = Boolean(gameEntry && gameEntry.isDuplicate); const purchasePrice = gameEntry && gameEntry.purchasePrice != null && gameEntry.purchasePrice !== "" @@ -518,9 +525,9 @@ async function restoreCatalogDump(mode, dump) { ` INSERT INTO games( console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price, - estimated_value, condition_score, loaned_to, created_at + estimated_value, condition_score, loaned_to, barcode, cover_url, created_at ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, COALESCE($13::timestamptz, NOW())); + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, COALESCE($14::timestamptz, NOW())); `, [ consoleId, @@ -535,6 +542,7 @@ async function restoreCatalogDump(mode, dump) { condition, loanedTo, barcode, + coverUrl, createdAt, ], ); @@ -589,6 +597,7 @@ async function createGame(payload) { const isDuplicate = Boolean(payload.isDuplicate); const loanedTo = normalizeText(payload.loanedTo) || null; const barcode = normalizeText(payload.barcode) || null; + const coverUrl = normalizeText(payload.coverUrl) || null; const year = payload.year != null && payload.year !== "" ? Number(payload.year) : null; const purchasePrice = payload.purchasePrice != null && payload.purchasePrice !== "" ? Number(payload.purchasePrice) : null; @@ -599,9 +608,9 @@ async function createGame(payload) { ` INSERT INTO games( console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price, - estimated_value, condition_score, loaned_to, barcode + estimated_value, condition_score, loaned_to, barcode, cover_url ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id::text AS id; `, [ @@ -617,6 +626,7 @@ async function createGame(payload) { condition, loanedTo, barcode, + coverUrl, ], ); @@ -647,6 +657,7 @@ async function updateGame(id, payload) { const isDuplicate = Boolean(payload.isDuplicate); const loanedTo = normalizeText(payload.loanedTo) || null; const barcode = normalizeText(payload.barcode) || null; + const coverUrl = normalizeText(payload.coverUrl) || null; const year = payload.year != null && payload.year !== "" ? Number(payload.year) : null; const purchasePrice = payload.purchasePrice != null && payload.purchasePrice !== "" ? Number(payload.purchasePrice) : null; @@ -668,7 +679,8 @@ async function updateGame(id, payload) { estimated_value = $10, condition_score = $11, loaned_to = $12, - barcode = $13 + barcode = $13, + cover_url = $14 WHERE id = $1::uuid RETURNING id::text AS id; `, @@ -686,6 +698,7 @@ async function updateGame(id, payload) { condition, loanedTo, barcode, + coverUrl, ], ); @@ -904,6 +917,7 @@ async function importCatalog(payload) { const publisher = normalizeText(game && game.publisher) || null; const loanedTo = normalizeText(game && game.loanedTo) || null; const barcode = normalizeText(game && game.barcode) || null; + const coverUrl = normalizeText(game && game.coverUrl) || null; const year = game && game.year != null && game.year !== "" ? Number(game.year) : null; const purchasePrice = game && game.purchasePrice != null && game.purchasePrice !== "" ? Number(game.purchasePrice) : null; @@ -931,9 +945,9 @@ async function importCatalog(payload) { ` INSERT INTO games( console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price, - estimated_value, condition_score, loaned_to, barcode + estimated_value, condition_score, loaned_to, barcode, cover_url ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12); + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13); `, [ consoleId, @@ -948,6 +962,7 @@ async function importCatalog(payload) { condition, loanedTo, barcode, + coverUrl, ], ); diff --git a/app.js b/app.js index 0721420..3667965 100644 --- a/app.js +++ b/app.js @@ -24,6 +24,8 @@ 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"); @@ -78,6 +80,30 @@ let scannerLoopId = null; let scannerLastCodeValue = ""; let scannerLastCodeAt = 0; +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 fileToDataUrl(file); + coverUrlInput.value = dataUrl; + } catch (error) { + console.error(error); + alert("Impossible de charger cette image."); + } finally { + input.value = ""; + } +}); + toolsToggleBtn.addEventListener("click", () => { gamesDrawer.classList.remove("open"); toolsDrawer.classList.toggle("open"); @@ -246,6 +272,7 @@ gameForm.addEventListener("submit", async (event) => { 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, @@ -289,6 +316,7 @@ gameForm.addEventListener("submit", async (event) => { 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, @@ -305,6 +333,7 @@ gameForm.addEventListener("submit", async (event) => { 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, @@ -694,6 +723,7 @@ function buildGamePayload(game, brand, consoleName, overrides = {}) { 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, @@ -882,6 +912,15 @@ function renderGames() { ].filter(Boolean); card.querySelector(".game-meta").textContent = metaParts.join(" | ") || "Aucune information complementaire"; + 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}` @@ -908,6 +947,7 @@ function startEditMode(game) { versionInput.value = game.version || ""; genreInput.value = game.genre || ""; publisherInput.value = game.publisher || ""; + coverUrlInput.value = game.coverUrl || ""; isDuplicateInput.checked = Boolean(game.isDuplicate); yearInput.value = game.year || ""; purchasePriceInput.value = game.purchasePrice != null ? game.purchasePrice : ""; @@ -923,10 +963,20 @@ function startEditMode(game) { function resetEditMode() { editingGameId = null; gameForm.reset(); + coverUrlInput.value = ""; gameSubmitBtn.textContent = "Ajouter le jeu"; cancelEditBtn.classList.add("hidden"); } +function fileToDataUrl(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result || "")); + reader.onerror = () => reject(new Error("read failed")); + reader.readAsDataURL(file); + }); +} + function updateScannerStatus(message) { if (scannerStatus) { scannerStatus.textContent = message; diff --git a/index.html b/index.html index d7eebe3..422a0e5 100644 --- a/index.html +++ b/index.html @@ -167,6 +167,10 @@ Éditeur +