Feature: improve loan flow and add games quick dashboard

This commit is contained in:
Ponte
2026-02-12 23:51:52 +01:00
parent 37912557d6
commit e31ec831b3
3 changed files with 230 additions and 13 deletions

157
app.js
View File

@@ -40,6 +40,11 @@ const storageState = document.getElementById("storageState");
const toolsDrawer = document.getElementById("toolsDrawer"); const toolsDrawer = document.getElementById("toolsDrawer");
const toolsToggleBtn = document.getElementById("toolsToggleBtn"); const toolsToggleBtn = document.getElementById("toolsToggleBtn");
const toolsCloseBtn = document.getElementById("toolsCloseBtn"); 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 migrateBtn = document.getElementById("migrateBtn");
const backupControls = document.getElementById("backupControls"); const backupControls = document.getElementById("backupControls");
const backupBtn = document.getElementById("backupBtn"); const backupBtn = document.getElementById("backupBtn");
@@ -52,14 +57,17 @@ const googleBackupBtn = document.getElementById("googleBackupBtn");
const googleRestoreBtn = document.getElementById("googleRestoreBtn"); const googleRestoreBtn = document.getElementById("googleRestoreBtn");
const quickSearchInput = document.getElementById("quickSearchInput"); const quickSearchInput = document.getElementById("quickSearchInput");
const quickSearchResults = document.getElementById("quickSearchResults"); const quickSearchResults = document.getElementById("quickSearchResults");
const loanedFilterBtn = document.getElementById("loanedFilterBtn");
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 = ""; let quickSearchTerm = "";
let googleStatus = { configured: false, connected: false, email: "" }; let googleStatus = { configured: false, connected: false, email: "" };
let showLoanedOnly = false;
toolsToggleBtn.addEventListener("click", () => { toolsToggleBtn.addEventListener("click", () => {
gamesDrawer.classList.remove("open");
toolsDrawer.classList.toggle("open"); toolsDrawer.classList.toggle("open");
}); });
@@ -67,22 +75,37 @@ toolsCloseBtn.addEventListener("click", () => {
toolsDrawer.classList.remove("open"); 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) => { document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) { if (!(event.target instanceof Element)) {
return; return;
} }
if (!toolsDrawer.classList.contains("open")) { const toolsOpen = toolsDrawer.classList.contains("open");
const gamesOpen = gamesDrawer.classList.contains("open");
if (!toolsOpen && !gamesOpen) {
return; return;
} }
if (toolsDrawer.contains(event.target) || toolsToggleBtn.contains(event.target)) { const clickedInsideTools = toolsDrawer.contains(event.target) || toolsToggleBtn.contains(event.target);
const clickedInsideGames = gamesDrawer.contains(event.target) || gamesToggleBtn.contains(event.target);
if (clickedInsideTools || clickedInsideGames) {
return; return;
} }
toolsDrawer.classList.remove("open"); toolsDrawer.classList.remove("open");
gamesDrawer.classList.remove("open");
}); });
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
if (event.key === "Escape") { if (event.key === "Escape") {
toolsDrawer.classList.remove("open"); toolsDrawer.classList.remove("open");
gamesDrawer.classList.remove("open");
} }
}); });
@@ -133,6 +156,11 @@ quickSearchInput.addEventListener("input", (event) => {
renderSearchResults(); renderSearchResults();
}); });
loanedFilterBtn.addEventListener("click", () => {
showLoanedOnly = !showLoanedOnly;
renderGames();
});
platformForm.addEventListener("submit", async (event) => { platformForm.addEventListener("submit", async (event) => {
event.preventDefault(); event.preventDefault();
@@ -317,18 +345,22 @@ gamesList.addEventListener("click", async (event) => {
const action = target.dataset.action; const action = target.dataset.action;
const id = target.dataset.id; const id = target.dataset.id;
if (!action || !id || !state.selectedConsole) { if (!action || !id) {
return; return;
} }
const games = state.gamesByConsole[state.selectedConsole] || []; const gameRef = findGameById(id);
const idx = games.findIndex((game) => game.id === id); if (!gameRef) {
if (idx === -1) {
return; return;
} }
const { game, games, idx, consoleName, brand } = gameRef;
if (action === "edit") { if (action === "edit") {
startEditMode(games[idx]); state.selectedBrand = brand;
state.selectedConsole = consoleName;
startEditMode(game);
persist();
render();
return; return;
} }
@@ -342,7 +374,24 @@ gamesList.addEventListener("click", async (event) => {
} }
if (action === "toggle-loan") { if (action === "toggle-loan") {
await apiRequest(`/api/catalog/games/${id}/toggle-loan`, { method: "POST" }); 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); await refreshFromApi(state.selectedBrand, state.selectedConsole);
@@ -361,7 +410,20 @@ gamesList.addEventListener("click", async (event) => {
} }
if (action === "toggle-loan") { if (action === "toggle-loan") {
games[idx].loanedTo = games[idx].loanedTo ? "" : "A renseigner"; 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(); persist();
@@ -487,6 +549,7 @@ function render() {
renderConsoleTabs(); renderConsoleTabs();
renderGames(); renderGames();
renderSearchResults(); renderSearchResults();
renderCollectionStats();
} }
function renderDataMode() { function renderDataMode() {
@@ -576,6 +639,40 @@ function findBrandByConsole(consoleName) {
return "INCONNUE"; 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 || "",
version: game.version || "",
genre: game.genre || "",
publisher: game.publisher || "",
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() { function collectAllGames() {
const all = []; const all = [];
for (const [consoleName, games] of Object.entries(state.gamesByConsole)) { for (const [consoleName, games] of Object.entries(state.gamesByConsole)) {
@@ -587,6 +684,22 @@ function collectAllGames() {
return all; 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 renderSearchResults() { function renderSearchResults() {
if (!quickSearchResults) { if (!quickSearchResults) {
return; return;
@@ -691,17 +804,27 @@ function renderConsoleTabs() {
function renderGames() { function renderGames() {
const selectedConsole = state.selectedConsole; const selectedConsole = state.selectedConsole;
gameSectionTitle.textContent = selectedConsole ? `Jeux - ${selectedConsole}` : "Jeux"; gameSectionTitle.textContent = showLoanedOnly
? "Jeux pretes - Toutes consoles"
: selectedConsole
? `Jeux - ${selectedConsole}`
: "Jeux";
gamesList.innerHTML = ""; gamesList.innerHTML = "";
loanedFilterBtn.textContent = showLoanedOnly ? "Voir tous les jeux" : "Voir jeux pretes";
if (!selectedConsole) { if (!showLoanedOnly && !selectedConsole) {
gamesList.innerHTML = '<p class="empty">Ajoute une section pour commencer.</p>'; gamesList.innerHTML = '<p class="empty">Ajoute une section pour commencer.</p>';
return; return;
} }
const games = state.gamesByConsole[selectedConsole] || []; const games = showLoanedOnly
? collectAllGames().filter((game) => normalizeText(game.loanedTo))
: state.gamesByConsole[selectedConsole] || [];
if (!games.length) { if (!games.length) {
gamesList.innerHTML = '<p class="empty">Aucun jeu sur cette console pour le moment.</p>'; gamesList.innerHTML = showLoanedOnly
? '<p class="empty">Aucun jeu prete actuellement.</p>'
: '<p class="empty">Aucun jeu sur cette console pour le moment.</p>';
return; return;
} }
@@ -715,6 +838,7 @@ function renderGames() {
card.querySelector(".game-title").textContent = game.title; card.querySelector(".game-title").textContent = game.title;
const metaParts = [ const metaParts = [
showLoanedOnly ? `${game.brand} / ${game.consoleName}` : null,
game.version ? `Version: ${game.version}` : null, game.version ? `Version: ${game.version}` : null,
game.genre ? `Genre: ${game.genre}` : null, game.genre ? `Genre: ${game.genre}` : null,
game.publisher ? `Editeur: ${game.publisher}` : null, game.publisher ? `Editeur: ${game.publisher}` : null,
@@ -770,6 +894,13 @@ function resetEditMode() {
cancelEditBtn.classList.add("hidden"); cancelEditBtn.classList.add("hidden");
} }
function normalizeText(value) {
if (value == null) {
return "";
}
return String(value).trim();
}
function loadState() { function loadState() {
try { try {
const raw = localStorage.getItem(STORAGE_KEY); const raw = localStorage.getItem(STORAGE_KEY);

View File

@@ -14,6 +14,7 @@
</head> </head>
<body> <body>
<button id="toolsToggleBtn" type="button" class="tools-toggle-btn">Outils</button> <button id="toolsToggleBtn" type="button" class="tools-toggle-btn">Outils</button>
<button id="gamesToggleBtn" type="button" class="games-toggle-btn">Jeux</button>
<aside id="toolsDrawer" class="tools-drawer" aria-label="Menu outils"> <aside id="toolsDrawer" class="tools-drawer" aria-label="Menu outils">
<div class="tools-header"> <div class="tools-header">
<h2>Outils</h2> <h2>Outils</h2>
@@ -41,6 +42,23 @@
</div> </div>
</div> </div>
</aside> </aside>
<aside id="gamesDrawer" class="games-drawer" aria-label="Menu jeux">
<div class="tools-header">
<h2>Jeux</h2>
<button id="gamesCloseBtn" type="button" class="btn-secondary tools-close-btn">Fermer</button>
</div>
<p class="tools-subtitle">Vue rapide globale de ta collection.</p>
<div class="quick-stats">
<article class="stat-card">
<p class="stat-label">Nombre total de jeux</p>
<p id="totalGamesCount" class="stat-value">0</p>
</article>
<article class="stat-card">
<p class="stat-label">Valeur totale estimee</p>
<p id="totalGamesValue" class="stat-value">0.00 EUR</p>
</article>
</div>
</aside>
<main class="app-shell"> <main class="app-shell">
<header class="hero"> <header class="hero">
@@ -102,6 +120,9 @@
<h2 id="gameSectionTitle">Jeux</h2> <h2 id="gameSectionTitle">Jeux</h2>
<p id="dataModeInfo" class="data-mode"></p> <p id="dataModeInfo" class="data-mode"></p>
</div> </div>
<div class="games-actions-bar">
<button id="loanedFilterBtn" type="button" class="btn-secondary">Voir jeux pretes</button>
</div>
<div class="search-zone"> <div class="search-zone">
<label> <label>

View File

@@ -33,6 +33,15 @@ body {
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
.games-toggle-btn {
position: fixed;
top: 4.4rem;
right: 1rem;
z-index: 30;
background: #1e725b;
box-shadow: var(--shadow);
}
.tools-drawer { .tools-drawer {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -53,6 +62,26 @@ body {
transform: translateX(0); transform: translateX(0);
} }
.games-drawer {
position: fixed;
top: 0;
right: 0;
width: min(360px, 92vw);
height: 100vh;
background: #f9fff9;
border-left: 1px solid var(--border);
box-shadow: -8px 0 24px rgba(17, 36, 57, 0.14);
padding: 1rem;
transform: translateX(102%);
transition: transform 180ms ease;
z-index: 41;
overflow: auto;
}
.games-drawer.open {
transform: translateX(0);
}
.tools-header { .tools-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -75,6 +104,32 @@ body {
font-size: 0.9rem; font-size: 0.9rem;
} }
.quick-stats {
display: grid;
gap: 0.7rem;
}
.stat-card {
margin: 0;
padding: 0.75rem;
border: 1px solid #d2e2d9;
border-radius: 12px;
background: #ffffff;
}
.stat-label {
margin: 0 0 0.35rem;
font-size: 0.86rem;
color: var(--muted);
}
.stat-value {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #184537;
}
.storage-state { .storage-state {
margin: 0 0 0.9rem; margin: 0 0 0.9rem;
font-size: 0.88rem; font-size: 0.88rem;
@@ -217,6 +272,10 @@ h1 {
background: #f9fbff; background: #f9fbff;
} }
.games-actions-bar {
margin-bottom: 0.8rem;
}
.quick-search-results { .quick-search-results {
margin-top: 0.65rem; margin-top: 0.65rem;
display: grid; display: grid;
@@ -442,6 +501,12 @@ button {
right: 1rem; right: 1rem;
} }
.games-toggle-btn {
top: auto;
bottom: 4.4rem;
right: 1rem;
}
.game-card { .game-card {
align-items: flex-start; align-items: flex-start;
flex-direction: column; flex-direction: column;