Preview: add UI V2 mode with grid, filters, sort, and toasts

This commit is contained in:
Ponte
2026-02-14 23:29:41 +01:00
parent 9b7af13df4
commit 60b19a70e9
3 changed files with 465 additions and 22 deletions

249
app.js
View File

@@ -61,6 +61,16 @@ const googleRestoreBtn = document.getElementById("googleRestoreBtn");
const quickSearchInput = document.getElementById("quickSearchInput");
const quickSearchResults = document.getElementById("quickSearchResults");
const loanedFilterBtn = document.getElementById("loanedFilterBtn");
const v2Toolbar = document.getElementById("v2Toolbar");
const v2SearchInput = document.getElementById("v2SearchInput");
const v2ConsoleFilter = document.getElementById("v2ConsoleFilter");
const v2GenreFilter = document.getElementById("v2GenreFilter");
const v2SortSelect = document.getElementById("v2SortSelect");
const v2StickyBar = document.getElementById("v2StickyBar");
const v2StickyCount = document.getElementById("v2StickyCount");
const v2ToggleFormBtn = document.getElementById("v2ToggleFormBtn");
const v2QuickBackupBtn = document.getElementById("v2QuickBackupBtn");
const toastContainer = document.getElementById("toastContainer");
const scannerStatus = document.getElementById("scannerStatus");
const scannerStartBtn = document.getElementById("scannerStartBtn");
const scannerStopBtn = document.getElementById("scannerStopBtn");
@@ -80,6 +90,12 @@ let scannerRunning = false;
let scannerLoopId = null;
let scannerLastCodeValue = "";
let scannerLastCodeAt = 0;
const uiV2Enabled = new URLSearchParams(window.location.search).get("ui") === "v2";
let v2SearchTerm = "";
let v2ConsoleValue = "";
let v2GenreValue = "";
let v2SortValue = "title_asc";
let v2FormCollapsed = false;
coverFileInput.addEventListener("change", async (event) => {
const input = event.target;
@@ -203,6 +219,48 @@ loanedFilterBtn.addEventListener("click", () => {
renderGames();
});
if (v2SearchInput) {
v2SearchInput.addEventListener("input", (event) => {
v2SearchTerm = event.target.value.trim();
renderGames();
});
}
if (v2ConsoleFilter) {
v2ConsoleFilter.addEventListener("change", (event) => {
v2ConsoleValue = event.target.value;
renderGames();
});
}
if (v2GenreFilter) {
v2GenreFilter.addEventListener("change", (event) => {
v2GenreValue = event.target.value;
renderGames();
});
}
if (v2SortSelect) {
v2SortSelect.addEventListener("change", (event) => {
v2SortValue = event.target.value || "title_asc";
renderGames();
});
}
if (v2ToggleFormBtn) {
v2ToggleFormBtn.addEventListener("click", () => {
v2FormCollapsed = !v2FormCollapsed;
gameForm.classList.toggle("hidden", v2FormCollapsed);
v2ToggleFormBtn.textContent = v2FormCollapsed ? "Afficher formulaire" : "Ajouter";
});
}
if (v2QuickBackupBtn) {
v2QuickBackupBtn.addEventListener("click", () => {
backupBtn.click();
});
}
if (scannerStartBtn) {
scannerStartBtn.addEventListener("click", async () => {
await startScanner();
@@ -258,6 +316,7 @@ platformForm.addEventListener("submit", async (event) => {
gameForm.addEventListener("submit", async (event) => {
event.preventDefault();
const wasEditing = Boolean(editingGameId);
const title = titleInput.value.trim();
if (!title || !state.selectedConsole) {
@@ -297,6 +356,7 @@ gameForm.addEventListener("submit", async (event) => {
resetEditMode();
await refreshFromApi(state.selectedBrand, state.selectedConsole);
showToast(wasEditing ? "Jeu mis a jour." : "Jeu ajoute.", "success");
return;
} catch (error) {
console.error(error);
@@ -350,6 +410,7 @@ gameForm.addEventListener("submit", async (event) => {
persist();
markLocalDataForImport();
render();
showToast(wasEditing ? "Jeu mis a jour." : "Jeu ajoute.", "success");
});
brandTabs.addEventListener("click", (event) => {
@@ -496,6 +557,7 @@ gamesList.addEventListener("click", async (event) => {
persist();
markLocalDataForImport();
render();
showToast("Jeu mis a jour.", "success");
return;
}
@@ -530,6 +592,7 @@ gamesList.addEventListener("click", async (event) => {
}
await refreshFromApi(state.selectedBrand, state.selectedConsole);
showToast("Action enregistree.", "success");
return;
} catch (error) {
console.error(error);
@@ -564,6 +627,7 @@ gamesList.addEventListener("click", async (event) => {
persist();
markLocalDataForImport();
render();
showToast("Action enregistree.", "success");
});
cancelEditBtn.addEventListener("click", () => {
@@ -678,10 +742,12 @@ restoreFileInput.addEventListener("change", async (event) => {
});
function render() {
renderV2Chrome();
renderDataMode();
renderGoogleStatus();
renderBrandTabs();
renderConsoleTabs();
updateV2FilterOptions();
renderGames();
renderSearchResults();
renderCollectionStats();
@@ -837,6 +903,78 @@ function renderCollectionStats() {
totalGamesValue.textContent = `${totalValue.toFixed(2)} EUR`;
}
function showToast(message, type = "info", timeoutMs = 2600) {
if (!toastContainer || !message) {
return;
}
const toast = document.createElement("div");
toast.className = `toast ${type}`.trim();
toast.textContent = message;
toastContainer.append(toast);
window.setTimeout(() => {
toast.remove();
}, timeoutMs);
}
function renderV2Chrome() {
if (!uiV2Enabled) {
return;
}
document.body.classList.add("ui-v2");
if (v2Toolbar) {
v2Toolbar.classList.remove("hidden");
}
if (v2StickyBar) {
v2StickyBar.classList.remove("hidden");
}
}
function updateV2FilterOptions() {
if (!uiV2Enabled || !v2ConsoleFilter || !v2GenreFilter) {
return;
}
const allGames = collectAllGames();
const consoles = Array.from(new Set(allGames.map((game) => normalizeText(game.consoleName)).filter(Boolean))).sort();
const genres = Array.from(new Set(allGames.flatMap((game) => splitGenres(game.genre)))).sort();
v2ConsoleFilter.innerHTML = `<option value="">Toutes</option>${consoles
.map((consoleName) => `<option value="${escapeHtml(consoleName)}">${escapeHtml(consoleName)}</option>`)
.join("")}`;
v2GenreFilter.innerHTML = `<option value="">Tous</option>${genres
.map((genre) => `<option value="${escapeHtml(genre)}">${escapeHtml(genre)}</option>`)
.join("")}`;
v2ConsoleFilter.value = v2ConsoleValue;
v2GenreFilter.value = v2GenreValue;
v2SortSelect.value = v2SortValue;
}
function splitGenres(genreRaw) {
return normalizeText(genreRaw)
.split(/[\/,|]/)
.map((item) => normalizeText(item))
.filter(Boolean);
}
function badgeClassForGenre(genreValue) {
const normalized = normalizeText(genreValue).toLowerCase();
if (normalized.includes("rpg")) {
return "rpg";
}
if (normalized.includes("action") || normalized.includes("aventure") || normalized.includes("adventure")) {
return "action";
}
if (normalized.includes("sport") || normalized.includes("football") || normalized.includes("course")) {
return "sport";
}
if (normalized.includes("racing") || normalized.includes("kart")) {
return "racing";
}
return "default";
}
function renderSearchResults() {
if (!quickSearchResults) {
return;
@@ -941,27 +1079,76 @@ function renderConsoleTabs() {
function renderGames() {
const selectedConsole = state.selectedConsole;
gameSectionTitle.textContent = showLoanedOnly
? "Jeux pretes - Toutes consoles"
: selectedConsole
? `Jeux - ${selectedConsole}`
: "Jeux";
const inV2 = uiV2Enabled;
gameSectionTitle.textContent = inV2
? "Catalogue jeux"
: showLoanedOnly
? "Jeux pretes - Toutes consoles"
: selectedConsole
? `Jeux - ${selectedConsole}`
: "Jeux";
gamesList.innerHTML = "";
loanedFilterBtn.textContent = showLoanedOnly ? "Voir tous les jeux" : "Voir jeux pretes";
if (!showLoanedOnly && !selectedConsole) {
if (!inV2 && !showLoanedOnly && !selectedConsole) {
gamesList.innerHTML = '<p class="empty">Ajoute une section pour commencer.</p>';
return;
}
const games = showLoanedOnly
? collectAllGames().filter((game) => normalizeText(game.loanedTo))
: state.gamesByConsole[selectedConsole] || [];
let games = inV2
? collectAllGames()
: showLoanedOnly
? collectAllGames().filter((game) => normalizeText(game.loanedTo))
: state.gamesByConsole[selectedConsole] || [];
if (showLoanedOnly) {
games = games.filter((game) => normalizeText(game.loanedTo));
}
if (inV2) {
const searchTerm = v2SearchTerm.toLowerCase();
if (searchTerm) {
games = games.filter((game) => {
const fields = [
game.title,
game.publisher,
game.barcode,
game.consoleName,
game.genre,
]
.map((value) => normalizeText(value).toLowerCase())
.join(" ");
return fields.includes(searchTerm);
});
}
if (v2ConsoleValue) {
games = games.filter((game) => normalizeText(game.consoleName) === v2ConsoleValue);
}
if (v2GenreValue) {
games = games.filter((game) =>
splitGenres(game.genre).some((genre) => genre.toLowerCase() === v2GenreValue.toLowerCase()),
);
}
games.sort((a, b) => {
if (v2SortValue === "year_desc") {
return Number(b.year || 0) - Number(a.year || 0);
}
if (v2SortValue === "value_desc") {
return Number(b.value || 0) - Number(a.value || 0);
}
return normalizeText(a.title).localeCompare(normalizeText(b.title), "fr", { sensitivity: "base" });
});
if (v2StickyCount) {
v2StickyCount.textContent = `${games.length} jeu${games.length > 1 ? "x" : ""}`;
}
}
if (!games.length) {
gamesList.innerHTML = showLoanedOnly
? '<p class="empty">Aucun jeu prete actuellement.</p>'
: '<p class="empty">Aucun jeu sur cette console pour le moment.</p>';
: '<p class="empty">Aucun jeu pour ces filtres.</p>';
return;
}
@@ -974,20 +1161,38 @@ function renderGames() {
card.querySelector(".game-title").textContent = game.title;
const metaParts = [
showLoanedOnly ? `${game.brand} / ${game.consoleName}` : null,
game.barcode ? `Code: ${game.barcode}` : null,
game.version ? `Version: ${game.version}` : null,
game.genre ? `Genre: ${game.genre}` : null,
game.publisher ? `Editeur: ${game.publisher}` : null,
game.isDuplicate ? "Double: OUI" : null,
game.year ? `Annee: ${game.year}` : null,
game.purchasePrice != null ? `Prix achat: ${game.purchasePrice.toFixed(2)} EUR` : null,
game.value != null ? `Cote: ${game.value.toFixed(2)} EUR` : null,
game.condition != null ? `Etat: ${game.condition}` : null,
].filter(Boolean);
const metaParts = inV2
? [
game.publisher ? `Editeur: ${game.publisher}` : null,
game.year ? `Annee: ${game.year}` : null,
game.version ? `Version: ${game.version}` : null,
game.value != null ? `Cote: ${Number(game.value).toFixed(2)} EUR` : null,
game.consoleName ? `Console: ${game.consoleName}` : null,
].filter(Boolean)
: [
showLoanedOnly ? `${game.brand} / ${game.consoleName}` : null,
game.barcode ? `Code: ${game.barcode}` : null,
game.version ? `Version: ${game.version}` : null,
game.genre ? `Genre: ${game.genre}` : null,
game.publisher ? `Editeur: ${game.publisher}` : null,
game.isDuplicate ? "Double: OUI" : null,
game.year ? `Annee: ${game.year}` : null,
game.purchasePrice != null ? `Prix achat: ${game.purchasePrice.toFixed(2)} EUR` : null,
game.value != null ? `Cote: ${game.value.toFixed(2)} EUR` : null,
game.condition != null ? `Etat: ${game.condition}` : null,
].filter(Boolean);
card.querySelector(".game-meta").textContent = metaParts.join(" | ") || "Aucune information complementaire";
const badgesContainer = card.querySelector(".game-badges");
const genres = splitGenres(game.genre);
if (genres.length) {
badgesContainer.innerHTML = genres
.slice(0, 4)
.map((genre) => `<span class="genre-badge ${badgeClassForGenre(genre)}">${escapeHtml(genre)}</span>`)
.join("");
} else {
badgesContainer.innerHTML = "";
}
const coverEl = card.querySelector(".game-cover");
const coverUrl = normalizeText(game.coverUrl);
if (coverUrl) {