Feature: improve loan flow and add games quick dashboard
This commit is contained in:
155
app.js
155
app.js
@@ -40,6 +40,11 @@ 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");
|
||||
@@ -52,14 +57,17 @@ 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 gamesList = document.getElementById("gamesList");
|
||||
const gameCardTemplate = document.getElementById("gameCardTemplate");
|
||||
let editingGameId = null;
|
||||
let pendingRestoreMode = "merge";
|
||||
let quickSearchTerm = "";
|
||||
let googleStatus = { configured: false, connected: false, email: "" };
|
||||
let showLoanedOnly = false;
|
||||
|
||||
toolsToggleBtn.addEventListener("click", () => {
|
||||
gamesDrawer.classList.remove("open");
|
||||
toolsDrawer.classList.toggle("open");
|
||||
});
|
||||
|
||||
@@ -67,22 +75,37 @@ 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;
|
||||
}
|
||||
if (!toolsDrawer.classList.contains("open")) {
|
||||
const toolsOpen = toolsDrawer.classList.contains("open");
|
||||
const gamesOpen = gamesDrawer.classList.contains("open");
|
||||
if (!toolsOpen && !gamesOpen) {
|
||||
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;
|
||||
}
|
||||
toolsDrawer.classList.remove("open");
|
||||
gamesDrawer.classList.remove("open");
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
toolsDrawer.classList.remove("open");
|
||||
gamesDrawer.classList.remove("open");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,6 +156,11 @@ quickSearchInput.addEventListener("input", (event) => {
|
||||
renderSearchResults();
|
||||
});
|
||||
|
||||
loanedFilterBtn.addEventListener("click", () => {
|
||||
showLoanedOnly = !showLoanedOnly;
|
||||
renderGames();
|
||||
});
|
||||
|
||||
platformForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -317,18 +345,22 @@ gamesList.addEventListener("click", async (event) => {
|
||||
|
||||
const action = target.dataset.action;
|
||||
const id = target.dataset.id;
|
||||
if (!action || !id || !state.selectedConsole) {
|
||||
if (!action || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const games = state.gamesByConsole[state.selectedConsole] || [];
|
||||
const idx = games.findIndex((game) => game.id === id);
|
||||
if (idx === -1) {
|
||||
const gameRef = findGameById(id);
|
||||
if (!gameRef) {
|
||||
return;
|
||||
}
|
||||
const { game, games, idx, consoleName, brand } = gameRef;
|
||||
|
||||
if (action === "edit") {
|
||||
startEditMode(games[idx]);
|
||||
state.selectedBrand = brand;
|
||||
state.selectedConsole = consoleName;
|
||||
startEditMode(game);
|
||||
persist();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,7 +374,24 @@ gamesList.addEventListener("click", async (event) => {
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -361,7 +410,20 @@ gamesList.addEventListener("click", async (event) => {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -487,6 +549,7 @@ function render() {
|
||||
renderConsoleTabs();
|
||||
renderGames();
|
||||
renderSearchResults();
|
||||
renderCollectionStats();
|
||||
}
|
||||
|
||||
function renderDataMode() {
|
||||
@@ -576,6 +639,40 @@ function findBrandByConsole(consoleName) {
|
||||
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() {
|
||||
const all = [];
|
||||
for (const [consoleName, games] of Object.entries(state.gamesByConsole)) {
|
||||
@@ -587,6 +684,22 @@ function collectAllGames() {
|
||||
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() {
|
||||
if (!quickSearchResults) {
|
||||
return;
|
||||
@@ -691,17 +804,27 @@ function renderConsoleTabs() {
|
||||
|
||||
function renderGames() {
|
||||
const selectedConsole = state.selectedConsole;
|
||||
gameSectionTitle.textContent = selectedConsole ? `Jeux - ${selectedConsole}` : "Jeux";
|
||||
gameSectionTitle.textContent = showLoanedOnly
|
||||
? "Jeux pretes - Toutes consoles"
|
||||
: selectedConsole
|
||||
? `Jeux - ${selectedConsole}`
|
||||
: "Jeux";
|
||||
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>';
|
||||
return;
|
||||
}
|
||||
|
||||
const games = state.gamesByConsole[selectedConsole] || [];
|
||||
const games = showLoanedOnly
|
||||
? collectAllGames().filter((game) => normalizeText(game.loanedTo))
|
||||
: state.gamesByConsole[selectedConsole] || [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -715,6 +838,7 @@ function renderGames() {
|
||||
card.querySelector(".game-title").textContent = game.title;
|
||||
|
||||
const metaParts = [
|
||||
showLoanedOnly ? `${game.brand} / ${game.consoleName}` : null,
|
||||
game.version ? `Version: ${game.version}` : null,
|
||||
game.genre ? `Genre: ${game.genre}` : null,
|
||||
game.publisher ? `Editeur: ${game.publisher}` : null,
|
||||
@@ -770,6 +894,13 @@ function resetEditMode() {
|
||||
cancelEditBtn.classList.add("hidden");
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return String(value).trim();
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
21
index.html
21
index.html
@@ -14,6 +14,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<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">
|
||||
<div class="tools-header">
|
||||
<h2>Outils</h2>
|
||||
@@ -41,6 +42,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<header class="hero">
|
||||
@@ -102,6 +120,9 @@
|
||||
<h2 id="gameSectionTitle">Jeux</h2>
|
||||
<p id="dataModeInfo" class="data-mode"></p>
|
||||
</div>
|
||||
<div class="games-actions-bar">
|
||||
<button id="loanedFilterBtn" type="button" class="btn-secondary">Voir jeux pretes</button>
|
||||
</div>
|
||||
|
||||
<div class="search-zone">
|
||||
<label>
|
||||
|
||||
65
styles.css
65
styles.css
@@ -33,6 +33,15 @@ body {
|
||||
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 {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -53,6 +62,26 @@ body {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -75,6 +104,32 @@ body {
|
||||
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 {
|
||||
margin: 0 0 0.9rem;
|
||||
font-size: 0.88rem;
|
||||
@@ -217,6 +272,10 @@ h1 {
|
||||
background: #f9fbff;
|
||||
}
|
||||
|
||||
.games-actions-bar {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.quick-search-results {
|
||||
margin-top: 0.65rem;
|
||||
display: grid;
|
||||
@@ -442,6 +501,12 @@ button {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.games-toggle-btn {
|
||||
top: auto;
|
||||
bottom: 4.4rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.game-card {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user