Feature: improve loan flow and add games quick dashboard
This commit is contained in:
157
app.js
157
app.js
@@ -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);
|
||||||
|
|||||||
21
index.html
21
index.html
@@ -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>
|
||||||
|
|||||||
65
styles.css
65
styles.css
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user