From b17715eaffeb01e343fa10b441e132a56257d33e Mon Sep 17 00:00:00 2001 From: Nathan FONTEYNE Date: Wed, 8 Jul 2026 15:27:34 +0200 Subject: [PATCH] update: autocomplete for suggestions too --- public/css/style.css | 1 + public/index.html | 1 + public/js/autocomplete.js | 91 ++++++++++++++++++++++++++++++++++++ public/js/nav.js | 2 +- public/js/repertoire.js | 97 ++++----------------------------------- public/js/suggestions.js | 29 ++++++++++++ public/suggestions.html | 13 ++++-- 7 files changed, 141 insertions(+), 93 deletions(-) create mode 100644 public/js/autocomplete.js diff --git a/public/css/style.css b/public/css/style.css index c76c0d0..d82c30e 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -112,6 +112,7 @@ nav#main-nav { align-items: center; gap: 0.4rem; margin-right: auto; + text-decoration: none; } .brand .brand-dot { diff --git a/public/index.html b/public/index.html index c173057..a09dd8e 100644 --- a/public/index.html +++ b/public/index.html @@ -44,6 +44,7 @@ + diff --git a/public/js/autocomplete.js b/public/js/autocomplete.js new file mode 100644 index 0000000..6488e6e --- /dev/null +++ b/public/js/autocomplete.js @@ -0,0 +1,91 @@ +function createTitleAutocomplete({ inputId, dropdownId, onSelect }) { + const input = document.getElementById(inputId); + const dropdown = document.getElementById(dropdownId); + let candidates = []; + let highlight = -1; + let timer = null; + + function itemTemplate(c, index) { + const img = c.artworkUrl + ? `` + : ``; + return ` +
+ ${img} +
+
${escapeHtml(c.title)}
+
${escapeHtml(c.artist)}
+
+
+ `; + } + + function render() { + if (!candidates.length) { + dropdown.style.display = 'none'; + dropdown.innerHTML = ''; + return; + } + dropdown.innerHTML = candidates.map(itemTemplate).join(''); + dropdown.style.display = 'block'; + dropdown.querySelectorAll('.autocomplete-item').forEach((el) => { + el.addEventListener('mousedown', (e) => { + e.preventDefault(); + select(candidates[parseInt(el.dataset.index, 10)]); + }); + }); + } + + function close() { + candidates = []; + highlight = -1; + render(); + } + + function select(candidate) { + close(); + onSelect(candidate); + } + + input.addEventListener('input', () => { + const query = input.value.trim(); + clearTimeout(timer); + if (query.length < 2) { + close(); + return; + } + timer = setTimeout(async () => { + try { + candidates = await api.get(`/api/music-search?q=${encodeURIComponent(query)}`); + highlight = -1; + render(); + } catch (err) { + close(); + } + }, 300); + }); + + input.addEventListener('keydown', (e) => { + if (!candidates.length) return; + if (e.key === 'ArrowDown') { + e.preventDefault(); + highlight = Math.min(highlight + 1, candidates.length - 1); + render(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + highlight = Math.max(highlight - 1, 0); + render(); + } else if (e.key === 'Enter' && highlight >= 0) { + e.preventDefault(); + select(candidates[highlight]); + } else if (e.key === 'Escape') { + close(); + } + }); + + input.addEventListener('blur', () => { + setTimeout(close, 150); + }); + + return { close }; +} diff --git a/public/js/nav.js b/public/js/nav.js index 86ad822..dd466c0 100644 --- a/public/js/nav.js +++ b/public/js/nav.js @@ -23,7 +23,7 @@ async function initNav(activePage) { const me = await api.get('/api/users/me'); const nav = document.getElementById('main-nav'); nav.innerHTML = ` - Octane + Octane