Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a6c3965e8 | ||
|
|
98671d4dcd | ||
|
|
afabdce17c | ||
|
|
e98d8c5717 | ||
|
|
f640a3b1ee |
507
app.js
507
app.js
@@ -46,6 +46,7 @@ const toolsCloseBtn = document.getElementById("toolsCloseBtn");
|
|||||||
const gamesDrawer = document.getElementById("gamesDrawer");
|
const gamesDrawer = document.getElementById("gamesDrawer");
|
||||||
const gamesToggleBtn = document.getElementById("gamesToggleBtn");
|
const gamesToggleBtn = document.getElementById("gamesToggleBtn");
|
||||||
const gamesCloseBtn = document.getElementById("gamesCloseBtn");
|
const gamesCloseBtn = document.getElementById("gamesCloseBtn");
|
||||||
|
const drawerBackdrop = document.getElementById("drawerBackdrop");
|
||||||
const totalGamesCount = document.getElementById("totalGamesCount");
|
const totalGamesCount = document.getElementById("totalGamesCount");
|
||||||
const totalGamesValue = document.getElementById("totalGamesValue");
|
const totalGamesValue = document.getElementById("totalGamesValue");
|
||||||
const migrateBtn = document.getElementById("migrateBtn");
|
const migrateBtn = document.getElementById("migrateBtn");
|
||||||
@@ -60,7 +61,26 @@ 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 topNavButtons = document.querySelectorAll(".topnav-btn[data-view]");
|
||||||
|
const gamesPanel = document.getElementById("gamesPanel");
|
||||||
|
const statsView = document.getElementById("statsView");
|
||||||
|
const scannerZone = document.getElementById("scannerZone");
|
||||||
|
const searchZone = document.getElementById("searchZone");
|
||||||
|
const statsTotalGames = document.getElementById("statsTotalGames");
|
||||||
|
const statsTotalValue = document.getElementById("statsTotalValue");
|
||||||
|
const statsLoanedGames = document.getElementById("statsLoanedGames");
|
||||||
|
const statsConsoleCount = document.getElementById("statsConsoleCount");
|
||||||
const loanedFilterBtn = document.getElementById("loanedFilterBtn");
|
const loanedFilterBtn = document.getElementById("loanedFilterBtn");
|
||||||
|
const bulkActionsBar = document.getElementById("bulkActionsBar");
|
||||||
|
const bulkSelectPage = document.getElementById("bulkSelectPage");
|
||||||
|
const bulkSelectionInfo = document.getElementById("bulkSelectionInfo");
|
||||||
|
const bulkLoanBtn = document.getElementById("bulkLoanBtn");
|
||||||
|
const bulkReturnBtn = document.getElementById("bulkReturnBtn");
|
||||||
|
const bulkDeleteBtn = document.getElementById("bulkDeleteBtn");
|
||||||
|
const paginationBar = document.getElementById("paginationBar");
|
||||||
|
const prevPageBtn = document.getElementById("prevPageBtn");
|
||||||
|
const nextPageBtn = document.getElementById("nextPageBtn");
|
||||||
|
const pageInfo = document.getElementById("pageInfo");
|
||||||
const v2Toolbar = document.getElementById("v2Toolbar");
|
const v2Toolbar = document.getElementById("v2Toolbar");
|
||||||
const v2SearchInput = document.getElementById("v2SearchInput");
|
const v2SearchInput = document.getElementById("v2SearchInput");
|
||||||
const v2ConsoleFilter = document.getElementById("v2ConsoleFilter");
|
const v2ConsoleFilter = document.getElementById("v2ConsoleFilter");
|
||||||
@@ -90,6 +110,13 @@ let scannerRunning = false;
|
|||||||
let scannerLoopId = null;
|
let scannerLoopId = null;
|
||||||
let scannerLastCodeValue = "";
|
let scannerLastCodeValue = "";
|
||||||
let scannerLastCodeAt = 0;
|
let scannerLastCodeAt = 0;
|
||||||
|
let resizeRenderTimeout = null;
|
||||||
|
let selectedGameIds = new Set();
|
||||||
|
let currentPage = 1;
|
||||||
|
let currentPageGameIds = [];
|
||||||
|
let currentTotalPages = 1;
|
||||||
|
let currentView = "catalogue";
|
||||||
|
let pendingFocusGameId = null;
|
||||||
// V2 is now the default UI. Use ?ui=v1 to force legacy mode if needed.
|
// 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 uiParam = new URLSearchParams(window.location.search).get("ui");
|
||||||
const uiV2Enabled = uiParam !== "v1";
|
const uiV2Enabled = uiParam !== "v1";
|
||||||
@@ -126,19 +153,23 @@ coverFileInput.addEventListener("change", async (event) => {
|
|||||||
toolsToggleBtn.addEventListener("click", () => {
|
toolsToggleBtn.addEventListener("click", () => {
|
||||||
gamesDrawer.classList.remove("open");
|
gamesDrawer.classList.remove("open");
|
||||||
toolsDrawer.classList.toggle("open");
|
toolsDrawer.classList.toggle("open");
|
||||||
|
syncDrawersUi();
|
||||||
});
|
});
|
||||||
|
|
||||||
toolsCloseBtn.addEventListener("click", () => {
|
toolsCloseBtn.addEventListener("click", () => {
|
||||||
toolsDrawer.classList.remove("open");
|
toolsDrawer.classList.remove("open");
|
||||||
|
syncDrawersUi();
|
||||||
});
|
});
|
||||||
|
|
||||||
gamesToggleBtn.addEventListener("click", () => {
|
gamesToggleBtn.addEventListener("click", () => {
|
||||||
toolsDrawer.classList.remove("open");
|
toolsDrawer.classList.remove("open");
|
||||||
gamesDrawer.classList.toggle("open");
|
gamesDrawer.classList.toggle("open");
|
||||||
|
syncDrawersUi();
|
||||||
});
|
});
|
||||||
|
|
||||||
gamesCloseBtn.addEventListener("click", () => {
|
gamesCloseBtn.addEventListener("click", () => {
|
||||||
gamesDrawer.classList.remove("open");
|
gamesDrawer.classList.remove("open");
|
||||||
|
syncDrawersUi();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
@@ -157,18 +188,28 @@ document.addEventListener("click", (event) => {
|
|||||||
}
|
}
|
||||||
toolsDrawer.classList.remove("open");
|
toolsDrawer.classList.remove("open");
|
||||||
gamesDrawer.classList.remove("open");
|
gamesDrawer.classList.remove("open");
|
||||||
|
syncDrawersUi();
|
||||||
});
|
});
|
||||||
|
|
||||||
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");
|
gamesDrawer.classList.remove("open");
|
||||||
|
syncDrawersUi();
|
||||||
if (scannerRunning) {
|
if (scannerRunning) {
|
||||||
stopScanner("Camera arretee.");
|
stopScanner("Camera arretee.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (drawerBackdrop) {
|
||||||
|
drawerBackdrop.addEventListener("click", () => {
|
||||||
|
toolsDrawer.classList.remove("open");
|
||||||
|
gamesDrawer.classList.remove("open");
|
||||||
|
syncDrawersUi();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
googleConnectBtn.addEventListener("click", async () => {
|
googleConnectBtn.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
const payload = await apiRequest("/api/google/connect-url");
|
const payload = await apiRequest("/api/google/connect-url");
|
||||||
@@ -216,14 +257,135 @@ quickSearchInput.addEventListener("input", (event) => {
|
|||||||
renderSearchResults();
|
renderSearchResults();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
quickSearchResults.addEventListener("click", (event) => {
|
||||||
|
if (!(event.target instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = event.target.closest(".search-hit[data-game-id]");
|
||||||
|
if (!(target instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gameId = normalizeText(target.dataset.gameId);
|
||||||
|
if (!gameId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openGameFromSearchResult(gameId);
|
||||||
|
});
|
||||||
|
|
||||||
|
quickSearchResults.addEventListener("keydown", (event) => {
|
||||||
|
if (!(event.target instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = event.target.closest(".search-hit[data-game-id]");
|
||||||
|
if (!(target instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
const gameId = normalizeText(target.dataset.gameId);
|
||||||
|
if (!gameId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openGameFromSearchResult(gameId);
|
||||||
|
});
|
||||||
|
|
||||||
loanedFilterBtn.addEventListener("click", () => {
|
loanedFilterBtn.addEventListener("click", () => {
|
||||||
showLoanedOnly = !showLoanedOnly;
|
setCurrentView(currentView === "loans" ? "catalogue" : "loans");
|
||||||
renderGames();
|
});
|
||||||
|
|
||||||
|
for (const navButton of topNavButtons) {
|
||||||
|
navButton.addEventListener("click", () => {
|
||||||
|
const view = navButton.dataset.view;
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCurrentView(view);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulkSelectPage) {
|
||||||
|
bulkSelectPage.addEventListener("change", (event) => {
|
||||||
|
const checked = Boolean(event.target.checked);
|
||||||
|
if (checked) {
|
||||||
|
for (const id of currentPageGameIds) {
|
||||||
|
selectedGameIds.add(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const id of currentPageGameIds) {
|
||||||
|
selectedGameIds.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderGames();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulkLoanBtn) {
|
||||||
|
bulkLoanBtn.addEventListener("click", async () => {
|
||||||
|
await performBulkAction("loan");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulkReturnBtn) {
|
||||||
|
bulkReturnBtn.addEventListener("click", async () => {
|
||||||
|
await performBulkAction("return");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulkDeleteBtn) {
|
||||||
|
bulkDeleteBtn.addEventListener("click", async () => {
|
||||||
|
await performBulkAction("delete");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevPageBtn) {
|
||||||
|
prevPageBtn.addEventListener("click", () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
currentPage -= 1;
|
||||||
|
renderGames();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPageBtn) {
|
||||||
|
nextPageBtn.addEventListener("click", () => {
|
||||||
|
if (currentPage < currentTotalPages) {
|
||||||
|
currentPage += 1;
|
||||||
|
renderGames();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
// Mobile virtual keyboards trigger resize events. Avoid rerendering while editing,
|
||||||
|
// otherwise the list rebuild can jump to top and break inline input edits.
|
||||||
|
if (inlineEditingGameId || editingGameId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const active = document.activeElement;
|
||||||
|
if (
|
||||||
|
active instanceof HTMLInputElement ||
|
||||||
|
active instanceof HTMLTextAreaElement ||
|
||||||
|
active instanceof HTMLSelectElement
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resizeRenderTimeout) {
|
||||||
|
clearTimeout(resizeRenderTimeout);
|
||||||
|
}
|
||||||
|
resizeRenderTimeout = window.setTimeout(() => {
|
||||||
|
renderGames();
|
||||||
|
}, 120);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (v2SearchInput) {
|
if (v2SearchInput) {
|
||||||
v2SearchInput.addEventListener("input", (event) => {
|
v2SearchInput.addEventListener("input", (event) => {
|
||||||
v2SearchTerm = event.target.value.trim();
|
v2SearchTerm = event.target.value.trim();
|
||||||
|
resetPaging();
|
||||||
|
selectedGameIds.clear();
|
||||||
renderGames();
|
renderGames();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -231,6 +393,8 @@ if (v2SearchInput) {
|
|||||||
if (v2ConsoleFilter) {
|
if (v2ConsoleFilter) {
|
||||||
v2ConsoleFilter.addEventListener("change", (event) => {
|
v2ConsoleFilter.addEventListener("change", (event) => {
|
||||||
v2ConsoleValue = event.target.value;
|
v2ConsoleValue = event.target.value;
|
||||||
|
resetPaging();
|
||||||
|
selectedGameIds.clear();
|
||||||
renderGames();
|
renderGames();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -238,6 +402,8 @@ if (v2ConsoleFilter) {
|
|||||||
if (v2GenreFilter) {
|
if (v2GenreFilter) {
|
||||||
v2GenreFilter.addEventListener("change", (event) => {
|
v2GenreFilter.addEventListener("change", (event) => {
|
||||||
v2GenreValue = event.target.value;
|
v2GenreValue = event.target.value;
|
||||||
|
resetPaging();
|
||||||
|
selectedGameIds.clear();
|
||||||
renderGames();
|
renderGames();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -245,6 +411,8 @@ if (v2GenreFilter) {
|
|||||||
if (v2SortSelect) {
|
if (v2SortSelect) {
|
||||||
v2SortSelect.addEventListener("change", (event) => {
|
v2SortSelect.addEventListener("change", (event) => {
|
||||||
v2SortValue = event.target.value || "title_asc";
|
v2SortValue = event.target.value || "title_asc";
|
||||||
|
resetPaging();
|
||||||
|
selectedGameIds.clear();
|
||||||
renderGames();
|
renderGames();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -409,6 +577,8 @@ gameForm.addEventListener("submit", async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetEditMode();
|
resetEditMode();
|
||||||
|
resetPaging();
|
||||||
|
selectedGameIds.clear();
|
||||||
persist();
|
persist();
|
||||||
markLocalDataForImport();
|
markLocalDataForImport();
|
||||||
render();
|
render();
|
||||||
@@ -433,6 +603,8 @@ brandTabs.addEventListener("click", (event) => {
|
|||||||
const consoles = state.brands[brand] || [];
|
const consoles = state.brands[brand] || [];
|
||||||
state.selectedConsole = consoles[0] || "";
|
state.selectedConsole = consoles[0] || "";
|
||||||
resetEditMode();
|
resetEditMode();
|
||||||
|
resetPaging();
|
||||||
|
selectedGameIds.clear();
|
||||||
persist();
|
persist();
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
@@ -453,6 +625,8 @@ consoleTabs.addEventListener("click", (event) => {
|
|||||||
|
|
||||||
state.selectedConsole = consoleName;
|
state.selectedConsole = consoleName;
|
||||||
resetEditMode();
|
resetEditMode();
|
||||||
|
resetPaging();
|
||||||
|
selectedGameIds.clear();
|
||||||
persist();
|
persist();
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
@@ -574,6 +748,7 @@ gamesList.addEventListener("click", async (event) => {
|
|||||||
try {
|
try {
|
||||||
if (action === "delete") {
|
if (action === "delete") {
|
||||||
await apiRequest(`/api/catalog/games/${id}`, { method: "DELETE" });
|
await apiRequest(`/api/catalog/games/${id}`, { method: "DELETE" });
|
||||||
|
selectedGameIds.delete(id);
|
||||||
if (editingGameId === id) {
|
if (editingGameId === id) {
|
||||||
resetEditMode();
|
resetEditMode();
|
||||||
}
|
}
|
||||||
@@ -615,6 +790,7 @@ gamesList.addEventListener("click", async (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
games.splice(idx, 1);
|
games.splice(idx, 1);
|
||||||
|
selectedGameIds.delete(id);
|
||||||
if (editingGameId === id) {
|
if (editingGameId === id) {
|
||||||
resetEditMode();
|
resetEditMode();
|
||||||
}
|
}
|
||||||
@@ -643,6 +819,26 @@ gamesList.addEventListener("click", async (event) => {
|
|||||||
showToast("Action enregistree.", "success");
|
showToast("Action enregistree.", "success");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gamesList.addEventListener("change", (event) => {
|
||||||
|
if (!(event.target instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = event.target.closest('input[type="checkbox"][data-action="select"]');
|
||||||
|
if (!(target instanceof HTMLInputElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = target.dataset.id;
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target.checked) {
|
||||||
|
selectedGameIds.add(id);
|
||||||
|
} else {
|
||||||
|
selectedGameIds.delete(id);
|
||||||
|
}
|
||||||
|
renderGames();
|
||||||
|
});
|
||||||
|
|
||||||
cancelEditBtn.addEventListener("click", () => {
|
cancelEditBtn.addEventListener("click", () => {
|
||||||
resetEditMode();
|
resetEditMode();
|
||||||
});
|
});
|
||||||
@@ -756,6 +952,7 @@ restoreFileInput.addEventListener("change", async (event) => {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
renderV2Chrome();
|
renderV2Chrome();
|
||||||
|
renderViewChrome();
|
||||||
renderDataMode();
|
renderDataMode();
|
||||||
renderGoogleStatus();
|
renderGoogleStatus();
|
||||||
renderBrandTabs();
|
renderBrandTabs();
|
||||||
@@ -766,6 +963,70 @@ function render() {
|
|||||||
renderCollectionStats();
|
renderCollectionStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCurrentView(view) {
|
||||||
|
const allowed = new Set(["catalogue", "loans", "stats"]);
|
||||||
|
if (!allowed.has(view)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentView = view;
|
||||||
|
showLoanedOnly = view === "loans";
|
||||||
|
resetPaging();
|
||||||
|
selectedGameIds.clear();
|
||||||
|
toolsDrawer.classList.remove("open");
|
||||||
|
gamesDrawer.classList.remove("open");
|
||||||
|
syncDrawersUi();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDrawersUi() {
|
||||||
|
if (!drawerBackdrop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const anyDrawerOpen = toolsDrawer.classList.contains("open") || gamesDrawer.classList.contains("open");
|
||||||
|
drawerBackdrop.classList.toggle("hidden", !anyDrawerOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderViewChrome() {
|
||||||
|
const isStats = currentView === "stats";
|
||||||
|
const isLoans = currentView === "loans";
|
||||||
|
|
||||||
|
for (const navButton of topNavButtons) {
|
||||||
|
navButton.classList.toggle("active", navButton.dataset.view === currentView);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gamesPanel) {
|
||||||
|
gamesPanel.classList.toggle("hidden", isStats);
|
||||||
|
}
|
||||||
|
if (statsView) {
|
||||||
|
statsView.classList.toggle("hidden", !isStats);
|
||||||
|
}
|
||||||
|
if (gameForm) {
|
||||||
|
gameForm.classList.toggle("hidden", isLoans || isStats || v2FormCollapsed);
|
||||||
|
}
|
||||||
|
if (scannerZone) {
|
||||||
|
scannerZone.classList.toggle("hidden", isLoans || isStats);
|
||||||
|
}
|
||||||
|
if (searchZone) {
|
||||||
|
searchZone.classList.toggle("hidden", isStats);
|
||||||
|
}
|
||||||
|
if (bulkActionsBar) {
|
||||||
|
bulkActionsBar.classList.toggle("hidden", isStats);
|
||||||
|
}
|
||||||
|
if (paginationBar) {
|
||||||
|
paginationBar.classList.toggle("hidden", isStats);
|
||||||
|
}
|
||||||
|
if (v2ToggleFormBtn) {
|
||||||
|
v2ToggleFormBtn.classList.toggle("hidden", isLoans || isStats);
|
||||||
|
}
|
||||||
|
if (v2QuickBackupBtn) {
|
||||||
|
v2QuickBackupBtn.classList.toggle("hidden", isStats);
|
||||||
|
}
|
||||||
|
if (loanedFilterBtn) {
|
||||||
|
loanedFilterBtn.textContent = isLoans ? "Retour catalogue" : "Aller a la vue prets";
|
||||||
|
loanedFilterBtn.classList.toggle("hidden", isStats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderDataMode() {
|
function renderDataMode() {
|
||||||
if (!dataModeInfo) {
|
if (!dataModeInfo) {
|
||||||
return;
|
return;
|
||||||
@@ -911,9 +1172,24 @@ function renderCollectionStats() {
|
|||||||
const value = typeof game.value === "number" ? game.value : Number(game.value);
|
const value = typeof game.value === "number" ? game.value : Number(game.value);
|
||||||
return Number.isFinite(value) ? sum + value : sum;
|
return Number.isFinite(value) ? sum + value : sum;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
const loanedCount = allGames.filter((game) => normalizeText(game.loanedTo)).length;
|
||||||
|
const activeConsoles = Object.values(state.gamesByConsole).filter((games) => Array.isArray(games) && games.length > 0)
|
||||||
|
.length;
|
||||||
|
|
||||||
totalGamesCount.textContent = String(totalCount);
|
totalGamesCount.textContent = String(totalCount);
|
||||||
totalGamesValue.textContent = `${totalValue.toFixed(2)} EUR`;
|
totalGamesValue.textContent = `${totalValue.toFixed(2)} EUR`;
|
||||||
|
if (statsTotalGames) {
|
||||||
|
statsTotalGames.textContent = String(totalCount);
|
||||||
|
}
|
||||||
|
if (statsTotalValue) {
|
||||||
|
statsTotalValue.textContent = `${totalValue.toFixed(2)} EUR`;
|
||||||
|
}
|
||||||
|
if (statsLoanedGames) {
|
||||||
|
statsLoanedGames.textContent = String(loanedCount);
|
||||||
|
}
|
||||||
|
if (statsConsoleCount) {
|
||||||
|
statsConsoleCount.textContent = String(activeConsoles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(message, type = "info", timeoutMs = 2600) {
|
function showToast(message, type = "info", timeoutMs = 2600) {
|
||||||
@@ -1040,6 +1316,57 @@ function conditionBadgeClass(conditionValue) {
|
|||||||
return "status-low";
|
return "status-low";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pageSizeForViewport() {
|
||||||
|
return window.innerWidth <= 640 ? 12 : 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPaging() {
|
||||||
|
currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBulkAndPaginationUi(pageGames, totalFilteredCount) {
|
||||||
|
const pageIds = pageGames.map((game) => game.id);
|
||||||
|
const selectedOnPage = pageIds.filter((id) => selectedGameIds.has(id)).length;
|
||||||
|
const hasAnySelection = selectedGameIds.size > 0;
|
||||||
|
|
||||||
|
currentPageGameIds = pageIds;
|
||||||
|
if (bulkSelectionInfo) {
|
||||||
|
bulkSelectionInfo.textContent = `${selectedGameIds.size} selectionne${selectedGameIds.size > 1 ? "s" : ""}`;
|
||||||
|
}
|
||||||
|
if (bulkLoanBtn) {
|
||||||
|
bulkLoanBtn.disabled = !hasAnySelection;
|
||||||
|
}
|
||||||
|
if (bulkReturnBtn) {
|
||||||
|
bulkReturnBtn.disabled = !hasAnySelection;
|
||||||
|
}
|
||||||
|
if (bulkDeleteBtn) {
|
||||||
|
bulkDeleteBtn.disabled = !hasAnySelection;
|
||||||
|
}
|
||||||
|
if (bulkSelectPage) {
|
||||||
|
bulkSelectPage.checked = pageIds.length > 0 && selectedOnPage === pageIds.length;
|
||||||
|
bulkSelectPage.indeterminate = selectedOnPage > 0 && selectedOnPage < pageIds.length;
|
||||||
|
bulkSelectPage.disabled = pageIds.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageInfo) {
|
||||||
|
pageInfo.textContent = `Page ${currentPage}/${currentTotalPages}`;
|
||||||
|
}
|
||||||
|
if (prevPageBtn) {
|
||||||
|
prevPageBtn.disabled = currentPage <= 1;
|
||||||
|
}
|
||||||
|
if (nextPageBtn) {
|
||||||
|
nextPageBtn.disabled = currentPage >= currentTotalPages;
|
||||||
|
}
|
||||||
|
if (paginationBar) {
|
||||||
|
const shouldHidePagination = currentView === "stats" || totalFilteredCount <= pageSizeForViewport();
|
||||||
|
paginationBar.classList.toggle("hidden", shouldHidePagination);
|
||||||
|
}
|
||||||
|
if (bulkActionsBar) {
|
||||||
|
const shouldHideBulk = currentView === "stats" || totalFilteredCount === 0;
|
||||||
|
bulkActionsBar.classList.toggle("hidden", shouldHideBulk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderSearchResults() {
|
function renderSearchResults() {
|
||||||
if (!quickSearchResults) {
|
if (!quickSearchResults) {
|
||||||
return;
|
return;
|
||||||
@@ -1089,7 +1416,7 @@ function renderSearchResults() {
|
|||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" | ");
|
.join(" | ");
|
||||||
return `<article class="search-hit"><strong>${escapeHtml(game.title || "")}</strong><p>${escapeHtml(meta)}</p></article>`;
|
return `<article class="search-hit" data-game-id="${escapeHtml(game.id)}" role="button" tabindex="0" title="Ouvrir la fiche du jeu"><strong>${escapeHtml(game.title || "")}</strong><p>${escapeHtml(meta)}</p></article>`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
@@ -1101,6 +1428,37 @@ function renderSearchResults() {
|
|||||||
quickSearchResults.innerHTML = `${header}${items}${more}`;
|
quickSearchResults.innerHTML = `${header}${items}${more}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openGameFromSearchResult(gameId) {
|
||||||
|
const allGames = collectAllGames();
|
||||||
|
const game = allGames.find((item) => item.id === gameId);
|
||||||
|
if (!game) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentView("catalogue");
|
||||||
|
if (game.brand && state.brands[game.brand]) {
|
||||||
|
state.selectedBrand = game.brand;
|
||||||
|
}
|
||||||
|
if (game.consoleName) {
|
||||||
|
state.selectedConsole = game.consoleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrow the catalogue to quickly open the target card and start inline edit.
|
||||||
|
v2ConsoleValue = normalizeText(game.consoleName);
|
||||||
|
v2GenreValue = "";
|
||||||
|
v2SearchTerm = normalizeText(game.title);
|
||||||
|
if (v2SearchInput) {
|
||||||
|
v2SearchInput.value = v2SearchTerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingFocusGameId = gameId;
|
||||||
|
inlineEditingGameId = gameId;
|
||||||
|
editingGameId = null;
|
||||||
|
resetPaging();
|
||||||
|
persist();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
return String(text)
|
return String(text)
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
@@ -1142,21 +1500,111 @@ function renderConsoleTabs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function performBulkAction(action) {
|
||||||
|
const selectedIds = Array.from(selectedGameIds);
|
||||||
|
if (!selectedIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let loanedTo = "";
|
||||||
|
if (action === "loan") {
|
||||||
|
const borrower = window.prompt("Nom de la personne a qui tu pretes ces jeux :");
|
||||||
|
if (borrower === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loanedTo = borrower.trim();
|
||||||
|
if (!loanedTo) {
|
||||||
|
alert("Le nom est obligatoire pour marquer les jeux comme pretes.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "delete") {
|
||||||
|
const confirmed = window.confirm(`Supprimer ${selectedIds.length} jeu(x) selectionne(s) ?`);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiReachable && dataMode !== "local-pending-import") {
|
||||||
|
try {
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
const gameRef = findGameById(id);
|
||||||
|
if (!gameRef) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { game, brand, consoleName } = gameRef;
|
||||||
|
if (action === "delete") {
|
||||||
|
await apiRequest(`/api/catalog/games/${id}`, { method: "DELETE" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (action === "return") {
|
||||||
|
if (!game.loanedTo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const payload = buildGamePayload(game, brand, consoleName, { loanedTo: "" });
|
||||||
|
await apiRequest(`/api/catalog/games/${id}`, { method: "PUT", body: payload });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (action === "loan") {
|
||||||
|
const payload = buildGamePayload(game, brand, consoleName, { loanedTo });
|
||||||
|
await apiRequest(`/api/catalog/games/${id}`, { method: "PUT", body: payload });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedGameIds.clear();
|
||||||
|
await refreshFromApi(state.selectedBrand, state.selectedConsole);
|
||||||
|
showToast("Action groupée enregistree.", "success");
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert("Action groupée impossible via l'API.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
const gameRef = findGameById(id);
|
||||||
|
if (!gameRef) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { games, idx } = gameRef;
|
||||||
|
if (action === "delete") {
|
||||||
|
games.splice(idx, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (action === "return") {
|
||||||
|
games[idx].loanedTo = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (action === "loan") {
|
||||||
|
games[idx].loanedTo = loanedTo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedGameIds.clear();
|
||||||
|
persist();
|
||||||
|
markLocalDataForImport();
|
||||||
|
render();
|
||||||
|
showToast("Action groupée enregistree.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
function renderGames() {
|
function renderGames() {
|
||||||
const selectedConsole = state.selectedConsole;
|
const selectedConsole = state.selectedConsole;
|
||||||
const inV2 = uiV2Enabled;
|
const inV2 = uiV2Enabled;
|
||||||
gameSectionTitle.textContent = inV2
|
gameSectionTitle.textContent =
|
||||||
? "Catalogue jeux"
|
currentView === "loans"
|
||||||
: showLoanedOnly
|
? "Jeux pretes"
|
||||||
? "Jeux pretes - Toutes consoles"
|
: inV2
|
||||||
: selectedConsole
|
? "Catalogue jeux"
|
||||||
? `Jeux - ${selectedConsole}`
|
: selectedConsole
|
||||||
: "Jeux";
|
? `Jeux - ${selectedConsole}`
|
||||||
|
: "Jeux";
|
||||||
gamesList.innerHTML = "";
|
gamesList.innerHTML = "";
|
||||||
loanedFilterBtn.textContent = showLoanedOnly ? "Voir tous les jeux" : "Voir jeux pretes";
|
|
||||||
|
|
||||||
if (!inV2 && !showLoanedOnly && !selectedConsole) {
|
if (!inV2 && !showLoanedOnly && !selectedConsole) {
|
||||||
gamesList.innerHTML = '<p class="empty">Ajoute une section pour commencer.</p>';
|
gamesList.innerHTML = '<p class="empty">Ajoute une section pour commencer.</p>';
|
||||||
|
updateBulkAndPaginationUi([], 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1210,16 +1658,36 @@ function renderGames() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!games.length) {
|
const allCurrentIds = new Set(collectAllGames().map((game) => game.id));
|
||||||
|
selectedGameIds = new Set(Array.from(selectedGameIds).filter((id) => allCurrentIds.has(id)));
|
||||||
|
|
||||||
|
const totalFilteredCount = games.length;
|
||||||
|
const pageSize = pageSizeForViewport();
|
||||||
|
if (pendingFocusGameId) {
|
||||||
|
const focusIndex = games.findIndex((game) => game.id === pendingFocusGameId);
|
||||||
|
if (focusIndex >= 0) {
|
||||||
|
currentPage = Math.floor(focusIndex / pageSize) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentTotalPages = Math.max(1, Math.ceil(totalFilteredCount / pageSize));
|
||||||
|
if (currentPage > currentTotalPages) {
|
||||||
|
currentPage = currentTotalPages;
|
||||||
|
}
|
||||||
|
const startIdx = (currentPage - 1) * pageSize;
|
||||||
|
const pageGames = games.slice(startIdx, startIdx + pageSize);
|
||||||
|
updateBulkAndPaginationUi(pageGames, totalFilteredCount);
|
||||||
|
|
||||||
|
if (!totalFilteredCount) {
|
||||||
gamesList.innerHTML = showLoanedOnly
|
gamesList.innerHTML = showLoanedOnly
|
||||||
? '<p class="empty">Aucun jeu prete actuellement.</p>'
|
? '<p class="empty">Aucun jeu prete actuellement.</p>'
|
||||||
: '<p class="empty">Aucun jeu pour ces filtres.</p>';
|
: '<p class="empty">Aucun jeu pour ces filtres.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const game of games) {
|
for (const game of pageGames) {
|
||||||
const card = gameCardTemplate.content.cloneNode(true);
|
const card = gameCardTemplate.content.cloneNode(true);
|
||||||
const article = card.querySelector(".game-card");
|
const article = card.querySelector(".game-card");
|
||||||
|
article.dataset.gameId = game.id;
|
||||||
article.classList.add(consoleThemeClass(game.consoleName));
|
article.classList.add(consoleThemeClass(game.consoleName));
|
||||||
if (inlineEditingGameId === game.id) {
|
if (inlineEditingGameId === game.id) {
|
||||||
article.classList.add("editing");
|
article.classList.add("editing");
|
||||||
@@ -1282,6 +1750,7 @@ function renderGames() {
|
|||||||
const editBtn = card.querySelector('[data-action="edit"]');
|
const editBtn = card.querySelector('[data-action="edit"]');
|
||||||
const toggleBtn = card.querySelector('[data-action="toggle-loan"]');
|
const toggleBtn = card.querySelector('[data-action="toggle-loan"]');
|
||||||
const deleteBtn = card.querySelector('[data-action="delete"]');
|
const deleteBtn = card.querySelector('[data-action="delete"]');
|
||||||
|
const selectInput = card.querySelector('input[type="checkbox"][data-action="select"]');
|
||||||
|
|
||||||
editBtn.dataset.id = game.id;
|
editBtn.dataset.id = game.id;
|
||||||
editBtn.textContent = "✏️ Editer";
|
editBtn.textContent = "✏️ Editer";
|
||||||
@@ -1296,6 +1765,10 @@ function renderGames() {
|
|||||||
deleteBtn.textContent = "🗑️ Supprimer";
|
deleteBtn.textContent = "🗑️ Supprimer";
|
||||||
deleteBtn.title = "Supprimer ce jeu";
|
deleteBtn.title = "Supprimer ce jeu";
|
||||||
deleteBtn.setAttribute("aria-label", "Supprimer ce jeu");
|
deleteBtn.setAttribute("aria-label", "Supprimer ce jeu");
|
||||||
|
if (selectInput instanceof HTMLInputElement) {
|
||||||
|
selectInput.dataset.id = game.id;
|
||||||
|
selectInput.checked = selectedGameIds.has(game.id);
|
||||||
|
}
|
||||||
|
|
||||||
if (inlineEditingGameId === game.id) {
|
if (inlineEditingGameId === game.id) {
|
||||||
const editor = document.createElement("div");
|
const editor = document.createElement("div");
|
||||||
@@ -1322,6 +1795,14 @@ function renderGames() {
|
|||||||
|
|
||||||
gamesList.append(card);
|
gamesList.append(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pendingFocusGameId) {
|
||||||
|
const focusedCard = gamesList.querySelector(`.game-card[data-game-id="${pendingFocusGameId}"]`);
|
||||||
|
if (focusedCard instanceof HTMLElement) {
|
||||||
|
focusedCard.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
}
|
||||||
|
pendingFocusGameId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startEditMode(game) {
|
function startEditMode(game) {
|
||||||
|
|||||||
106
index.html
106
index.html
@@ -13,8 +13,20 @@
|
|||||||
<link rel="stylesheet" href="styles.css" />
|
<link rel="stylesheet" href="styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<button id="toolsToggleBtn" type="button" class="tools-toggle-btn">Outils</button>
|
<header class="topbar">
|
||||||
|
<div class="topbar-brand">
|
||||||
|
<p class="eyebrow">Collection Manager</p>
|
||||||
|
<h1>Collection Jeux Video</h1>
|
||||||
|
</div>
|
||||||
|
<nav class="topbar-nav" aria-label="Navigation principale">
|
||||||
|
<button type="button" class="topnav-btn active" data-view="catalogue">Catalogue</button>
|
||||||
|
<button type="button" class="topnav-btn" data-view="loans">Prets</button>
|
||||||
|
<button type="button" class="topnav-btn" data-view="stats">Statistiques</button>
|
||||||
|
<button id="toolsToggleBtn" type="button" class="topnav-btn">Outils</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
<button id="gamesToggleBtn" type="button" class="games-toggle-btn">Jeux</button>
|
<button id="gamesToggleBtn" type="button" class="games-toggle-btn">Jeux</button>
|
||||||
|
<div id="drawerBackdrop" class="drawer-backdrop hidden" aria-hidden="true"></div>
|
||||||
<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>
|
||||||
@@ -60,7 +72,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="app-shell">
|
<main class="app-shell v3-layout">
|
||||||
|
<aside class="v3-sidebar panel">
|
||||||
|
<section class="sidebar-block">
|
||||||
|
<h2 class="sidebar-title">Plateformes et consoles</h2>
|
||||||
|
<form id="platformForm" class="grid-form">
|
||||||
|
<label>
|
||||||
|
Marque (ex: SONY)
|
||||||
|
<input id="brandInput" required placeholder="SONY" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Console (ex: PlayStation 5)
|
||||||
|
<input id="consoleInput" required placeholder="PlayStation 5" />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Ajouter section</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
<section class="sidebar-block">
|
||||||
|
<div id="brandTabs" class="tabs" aria-label="Onglets marques"></div>
|
||||||
|
<div id="consoleTabs" class="tabs secondary" aria-label="Onglets consoles"></div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="v3-main">
|
||||||
<div id="v2StickyBar" class="v2-sticky hidden">
|
<div id="v2StickyBar" class="v2-sticky hidden">
|
||||||
<div class="v2-sticky-title">Vue catalogue</div>
|
<div class="v2-sticky-title">Vue catalogue</div>
|
||||||
<div id="v2StickyCount" class="v2-sticky-count">0 jeu affiche</div>
|
<div id="v2StickyCount" class="v2-sticky-count">0 jeu affiche</div>
|
||||||
@@ -73,32 +107,34 @@
|
|||||||
<header class="hero">
|
<header class="hero">
|
||||||
<div class="hero-copy">
|
<div class="hero-copy">
|
||||||
<p class="eyebrow">Catalogue perso</p>
|
<p class="eyebrow">Catalogue perso</p>
|
||||||
<h1>Collection Jeux Video</h1>
|
<p class="subtitle">Gere ta collection, tes prets et la valeur de tes jeux en un seul endroit.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<section id="statsView" class="panel stats-view hidden">
|
||||||
<section class="panel platform-panel">
|
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>Plateformes et consoles</h2>
|
<h2>Statistiques collection</h2>
|
||||||
|
</div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<article class="stat-card">
|
||||||
|
<p class="stat-label">Nombre total de jeux</p>
|
||||||
|
<p id="statsTotalGames" class="stat-value">0</p>
|
||||||
|
</article>
|
||||||
|
<article class="stat-card">
|
||||||
|
<p class="stat-label">Valeur totale estimee</p>
|
||||||
|
<p id="statsTotalValue" class="stat-value">0.00 EUR</p>
|
||||||
|
</article>
|
||||||
|
<article class="stat-card">
|
||||||
|
<p class="stat-label">Jeux pretes</p>
|
||||||
|
<p id="statsLoanedGames" class="stat-value">0</p>
|
||||||
|
</article>
|
||||||
|
<article class="stat-card">
|
||||||
|
<p class="stat-label">Consoles actives</p>
|
||||||
|
<p id="statsConsoleCount" class="stat-value">0</p>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="platformForm" class="grid-form">
|
|
||||||
<label>
|
|
||||||
Marque (ex: SONY)
|
|
||||||
<input id="brandInput" required placeholder="SONY" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Console (ex: PlayStation 5)
|
|
||||||
<input id="consoleInput" required placeholder="PlayStation 5" />
|
|
||||||
</label>
|
|
||||||
<button type="submit">Ajouter section</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="brandTabs" class="tabs" aria-label="Onglets marques"></div>
|
|
||||||
<div id="consoleTabs" class="tabs secondary" aria-label="Onglets consoles"></div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel games-panel">
|
<section id="gamesPanel" class="panel games-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2 id="gameSectionTitle">Jeux</h2>
|
<h2 id="gameSectionTitle">Jeux</h2>
|
||||||
<p id="dataModeInfo" class="data-mode"></p>
|
<p id="dataModeInfo" class="data-mode"></p>
|
||||||
@@ -130,9 +166,19 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="games-actions-bar">
|
<div class="games-actions-bar">
|
||||||
<button id="loanedFilterBtn" type="button" class="btn-secondary">Voir jeux pretes</button>
|
<button id="loanedFilterBtn" type="button" class="btn-secondary">Basculer vue prets</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="scanner-zone">
|
<div id="bulkActionsBar" class="bulk-actions-bar">
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input id="bulkSelectPage" type="checkbox" />
|
||||||
|
Tout selectionner (page)
|
||||||
|
</label>
|
||||||
|
<span id="bulkSelectionInfo" class="bulk-selection-info">0 selectionne</span>
|
||||||
|
<button id="bulkLoanBtn" type="button" class="btn-secondary" disabled>Marquer prete</button>
|
||||||
|
<button id="bulkReturnBtn" type="button" class="btn-secondary" disabled>Marquer rendu</button>
|
||||||
|
<button id="bulkDeleteBtn" type="button" class="btn-inline danger" disabled>Supprimer</button>
|
||||||
|
</div>
|
||||||
|
<div id="scannerZone" class="scanner-zone">
|
||||||
<div class="scanner-header">
|
<div class="scanner-header">
|
||||||
<strong>Scan camera (mobile)</strong>
|
<strong>Scan camera (mobile)</strong>
|
||||||
<p id="scannerStatus" class="scanner-status">Camera inactive.</p>
|
<p id="scannerStatus" class="scanner-status">Camera inactive.</p>
|
||||||
@@ -145,7 +191,7 @@
|
|||||||
<p id="scannerLastCode" class="scanner-last-code hidden"></p>
|
<p id="scannerLastCode" class="scanner-last-code hidden"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-zone">
|
<div id="searchZone" class="search-zone">
|
||||||
<label>
|
<label>
|
||||||
Recherche rapide anti-doublon
|
Recherche rapide anti-doublon
|
||||||
<input id="quickSearchInput" placeholder="Ex: Destiny, Final Fantasy, Zelda..." />
|
<input id="quickSearchInput" placeholder="Ex: Destiny, Final Fantasy, Zelda..." />
|
||||||
@@ -210,12 +256,22 @@
|
|||||||
<input id="coverUrlInput" type="hidden" />
|
<input id="coverUrlInput" type="hidden" />
|
||||||
|
|
||||||
<div id="gamesList" class="games-list"></div>
|
<div id="gamesList" class="games-list"></div>
|
||||||
|
<div id="paginationBar" class="pagination-bar">
|
||||||
|
<button id="prevPageBtn" type="button" class="btn-secondary">Precedent</button>
|
||||||
|
<span id="pageInfo" class="page-info">Page 1/1</span>
|
||||||
|
<button id="nextPageBtn" type="button" class="btn-secondary">Suivant</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
||||||
|
|
||||||
<template id="gameCardTemplate">
|
<template id="gameCardTemplate">
|
||||||
<article class="game-card">
|
<article class="game-card">
|
||||||
|
<label class="game-select">
|
||||||
|
<input type="checkbox" data-action="select" />
|
||||||
|
<span>Selection</span>
|
||||||
|
</label>
|
||||||
<img class="game-cover hidden" alt="Pochette du jeu" loading="lazy" />
|
<img class="game-cover hidden" alt="Pochette du jeu" loading="lazy" />
|
||||||
<div class="game-main">
|
<div class="game-main">
|
||||||
<h3 class="game-title"></h3>
|
<h3 class="game-title"></h3>
|
||||||
|
|||||||
189
styles.css
189
styles.css
@@ -27,6 +27,49 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 60;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.8rem;
|
||||||
|
padding: 0.7rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: rgba(248, 250, 253, 0.92);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-brand h1 {
|
||||||
|
margin: 0.1rem 0 0;
|
||||||
|
font-size: clamp(1.05rem, 1.8vw, 1.35rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav-btn {
|
||||||
|
background: #dde7f3;
|
||||||
|
color: #1f324a;
|
||||||
|
border: 1px solid #cfd9e7;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.48rem 0.72rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav-btn.active {
|
||||||
|
background: #0f6ab8;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #0f6ab8;
|
||||||
|
}
|
||||||
|
|
||||||
.tools-toggle-btn {
|
.tools-toggle-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
@@ -65,6 +108,13 @@ body {
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drawer-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(6, 12, 22, 0.42);
|
||||||
|
z-index: 35;
|
||||||
|
}
|
||||||
|
|
||||||
.games-drawer {
|
.games-drawer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -161,12 +211,36 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
width: min(1100px, 94vw);
|
width: min(1320px, 95vw);
|
||||||
margin: 2rem auto;
|
margin: 1rem auto 2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v3-layout {
|
||||||
|
grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v3-main {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v3-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-block + .sidebar-block {
|
||||||
|
margin-top: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
margin: 0 0 0.7rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.hero,
|
.hero,
|
||||||
.panel {
|
.panel {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
@@ -206,6 +280,12 @@ h1 {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
@@ -244,6 +324,24 @@ h1 {
|
|||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bulk-actions-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
padding: 0.55rem 0.65rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f7faff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-selection-info {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.scanner-zone {
|
.scanner-zone {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -300,6 +398,16 @@ h1 {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 0.5rem 0.6rem;
|
padding: 0.5rem 0.6rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-hit:hover,
|
||||||
|
.search-hit:focus-visible {
|
||||||
|
border-color: #9fc3e6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(70, 130, 180, 0.16);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-hit strong {
|
.search-hit strong {
|
||||||
@@ -439,6 +547,20 @@ button {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.game-select {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-select input {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.console-theme-default {
|
.console-theme-default {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
@@ -520,6 +642,21 @@ button {
|
|||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-bar {
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-info {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--muted);
|
||||||
|
min-width: 98px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-editor {
|
.inline-editor {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
border-top: 1px dashed #ccd7e4;
|
border-top: 1px dashed #ccd7e4;
|
||||||
@@ -750,6 +887,10 @@ body.ui-v2 .game-card {
|
|||||||
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.ui-v2 .game-select span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
body.ui-v2 .game-card:hover {
|
body.ui-v2 .game-card:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
border-color: #d3cef9;
|
border-color: #d3cef9;
|
||||||
@@ -768,7 +909,7 @@ body.ui-v2 .game-actions {
|
|||||||
|
|
||||||
body.ui-v2 .hero {
|
body.ui-v2 .hero {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 4.2rem;
|
top: 5.3rem;
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,10 +951,35 @@ body.ui-v2 .hero {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
.topbar {
|
||||||
|
padding: 0.6rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v3-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v3-sidebar {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.v2-toolbar {
|
.v2-toolbar {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bulk-actions-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-selection-info {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.grid-form,
|
.grid-form,
|
||||||
.game-form {
|
.game-form {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -821,6 +987,19 @@ body.ui-v2 .hero {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
.topbar-nav {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav-btn {
|
||||||
|
flex: 1 1 calc(50% - 0.3rem);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.grid-form,
|
.grid-form,
|
||||||
.game-form {
|
.game-form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -864,4 +1043,8 @@ body.ui-v2 .hero {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-bar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user