diff --git a/config.php b/config.php index 8b10e1b..a073478 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@ +
+ + +
diff --git a/public/js/modules/search.js b/public/js/modules/search.js index b9b4c3e..b27ef2f 100644 --- a/public/js/modules/search.js +++ b/public/js/modules/search.js @@ -403,3 +403,88 @@ replaceAllBtn?.addEventListener('click', async () => { search(input.value); setTimeout(() => { replaceStatus.style.color = ''; }, 3000); }); + +// ── Saved searches ────────────────────────────────────────────── +// localStorage-persisted list of search strings. The bookmark button +// toggles a popup that lets the user pick one (fills input + runs), +// save the current input value, or delete saved entries. + +const SAVED_KEY = 'hyprcrd-saved-searches'; +const savedBtn = $('search-saved-btn'); +const savedPopup = $('search-saved-popup'); +const savedList = $('search-saved-list'); +const savedAdd = $('search-saved-add'); + +function loadSaved() { + try { return JSON.parse(localStorage.getItem(SAVED_KEY) || '[]'); } catch { return []; } +} +function storeSaved(arr) { + localStorage.setItem(SAVED_KEY, JSON.stringify(arr)); +} + +function renderSavedList() { + if (!savedList) return; + const items = loadSaved(); + if (!items.length) { + savedList.innerHTML = '
no saved searches
'; + return; + } + savedList.innerHTML = items.map((q, i) => ` +
+ + ${esc(q)} + +
+ `).join(''); + savedList.querySelectorAll('.search-saved-row').forEach(row => { + row.addEventListener('click', (e) => { + if (e.target.closest('.search-saved-del')) return; + const q = items[parseInt(row.dataset.idx)]; + if (q !== undefined) { + input.value = q; + search(q); + closeSavedPopup(); + } + }); + }); + savedList.querySelectorAll('.search-saved-del').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const idx = parseInt(btn.dataset.idx); + const arr = loadSaved(); + arr.splice(idx, 1); + storeSaved(arr); + renderSavedList(); + }); + }); +} + +function openSavedPopup() { + renderSavedList(); + savedPopup?.classList.remove('hidden'); + const close = (e) => { + if (savedPopup?.contains(e.target) || e.target === savedBtn) return; + closeSavedPopup(); + document.removeEventListener('mousedown', close, true); + }; + setTimeout(() => document.addEventListener('mousedown', close, true), 0); +} +function closeSavedPopup() { + savedPopup?.classList.add('hidden'); +} + +savedBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + if (savedPopup?.classList.contains('hidden')) openSavedPopup(); + else closeSavedPopup(); +}); + +savedAdd?.addEventListener('click', () => { + const q = input.value.trim(); + if (!q) return; + const arr = loadSaved(); + if (arr.includes(q)) return; // no dupes + arr.unshift(q); + storeSaved(arr); + renderSavedList(); +}); diff --git a/public/plugins/tests/search.test.js b/public/plugins/tests/search.test.js index 6c4c211..41c1796 100644 --- a/public/plugins/tests/search.test.js +++ b/public/plugins/tests/search.test.js @@ -44,3 +44,125 @@ describe('Search', () => { expect($('#search')).toBeHidden(); }); }); + +describe('Search: saved searches', () => { + const SAVED_KEY = 'hyprcrd-saved-searches'; + + afterEach(async () => { + // Close overlays and restore localStorage to avoid cross-test pollution. + document.querySelector('#search-saved-popup')?.classList.add('hidden'); + await closeAll(); + }); + + it('bookmark button and popup exist', () => { + expect($('#search-saved-btn')).toExist(); + expect($('#search-saved-popup')).toExist(); + expect($('#search-saved-popup').classList.contains('hidden')).toBe(true); + }); + + it('clicking bookmark opens the popup', async () => { + click('#search-btn'); + await sleep(100); + click('#search-saved-btn'); + await sleep(40); + expect($('#search-saved-popup').classList.contains('hidden')).toBe(false); + }); + + it('saves and recalls a search', async () => { + // Seed localStorage directly to avoid UI coupling for the save path. + const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]'); + const probe = '__test_saved_search_probe__'; + try { + localStorage.setItem(SAVED_KEY, JSON.stringify([probe, ...before])); + + click('#search-btn'); + await sleep(100); + click('#search-saved-btn'); + await sleep(60); + + const popup = $('#search-saved-popup'); + const rows = popup.querySelectorAll('.search-saved-row'); + expect(rows.length).toBeGreaterThan(0); + // First row should be our probe. + expect(rows[0].textContent).toContain(probe); + + // Click the row → fills input. + click(rows[0]); + await sleep(40); + expect($('#search-input').value).toBe(probe); + } finally { + // Restore original saved searches. + localStorage.setItem(SAVED_KEY, JSON.stringify(before)); + } + }); + + it('deletes a saved search', async () => { + const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]'); + const probe = '__test_saved_del__'; + try { + localStorage.setItem(SAVED_KEY, JSON.stringify([probe])); + + click('#search-btn'); + await sleep(100); + click('#search-saved-btn'); + await sleep(60); + + const delBtn = $('#search-saved-popup').querySelector('.search-saved-del'); + expect(delBtn).toExist(); + click(delBtn); + await sleep(40); + + const after = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]'); + expect(after.includes(probe)).toBe(false); + } finally { + localStorage.setItem(SAVED_KEY, JSON.stringify(before)); + } + }); + + it('save-current button adds input value', async () => { + const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]'); + const probe = '__test_saved_add_' + Date.now() + '__'; + try { + click('#search-btn'); + await sleep(100); + const inp = $('#search-input'); + inp.value = probe; + inp.dispatchEvent(new Event('input', { bubbles: true })); + + click('#search-saved-btn'); + await sleep(40); + click('#search-saved-add'); + await sleep(40); + + const after = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]'); + expect(after.includes(probe)).toBe(true); + } finally { + localStorage.setItem(SAVED_KEY, JSON.stringify(before)); + } + }); + + it('does not add duplicates', async () => { + const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]'); + const probe = '__test_saved_dupe__'; + try { + localStorage.setItem(SAVED_KEY, JSON.stringify([probe])); + + click('#search-btn'); + await sleep(100); + const inp = $('#search-input'); + inp.value = probe; + inp.dispatchEvent(new Event('input', { bubbles: true })); + + click('#search-saved-btn'); + await sleep(40); + click('#search-saved-add'); + await sleep(40); + + const after = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]'); + const count = after.filter(q => q === probe).length; + expect(count).toBe(1); + } finally { + localStorage.setItem(SAVED_KEY, JSON.stringify(before)); + } + }); +});