Feature: open and focus game card from quick search results

This commit is contained in:
Ponte
2026-03-05 19:44:18 +01:00
parent 98671d4dcd
commit 7a6c3965e8
2 changed files with 92 additions and 1 deletions

83
app.js
View File

@@ -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 `<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("");
@@ -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("&", "&amp;")
@@ -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) {

View File

@@ -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 {