diff --git a/app.js b/app.js index 95a98d9..15093c7 100644 --- a/app.js +++ b/app.js @@ -116,6 +116,7 @@ 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. const uiParam = new URLSearchParams(window.location.search).get("ui"); const uiV2Enabled = uiParam !== "v1"; @@ -256,6 +257,40 @@ quickSearchInput.addEventListener("input", (event) => { 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", () => { setCurrentView(currentView === "loans" ? "catalogue" : "loans"); }); @@ -1381,7 +1416,7 @@ function renderSearchResults() { ] .filter(Boolean) .join(" | "); - return `
${escapeHtml(game.title || "")}

${escapeHtml(meta)}

`; + return `
${escapeHtml(game.title || "")}

${escapeHtml(meta)}

`; }) .join(""); @@ -1393,6 +1428,37 @@ function renderSearchResults() { 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) { return String(text) .replaceAll("&", "&") @@ -1597,6 +1663,12 @@ function renderGames() { 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; @@ -1615,6 +1687,7 @@ function renderGames() { for (const game of pageGames) { const card = gameCardTemplate.content.cloneNode(true); const article = card.querySelector(".game-card"); + article.dataset.gameId = game.id; article.classList.add(consoleThemeClass(game.consoleName)); if (inlineEditingGameId === game.id) { article.classList.add("editing"); @@ -1722,6 +1795,14 @@ function renderGames() { 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) { diff --git a/styles.css b/styles.css index aa79ba2..c85931d 100644 --- a/styles.css +++ b/styles.css @@ -398,6 +398,16 @@ h1 { border-radius: 10px; padding: 0.5rem 0.6rem; 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 {