From 2785b849398179f491e415eafaeb423597e5dc93 Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Sun, 22 Mar 2026 02:39:53 +0100 Subject: [PATCH] feat(settings): add JS module for localStorage preferences and panel Co-Authored-By: Claude Opus 4.6 --- internal/views/static/js/settings.js | 266 +++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 internal/views/static/js/settings.js diff --git a/internal/views/static/js/settings.js b/internal/views/static/js/settings.js new file mode 100644 index 0000000..77c9f7a --- /dev/null +++ b/internal/views/static/js/settings.js @@ -0,0 +1,266 @@ +var ALL_ENGINES = [ + 'wikipedia', 'arxiv', 'crossref', 'braveapi', + 'qwant', 'duckduckgo', 'github', 'reddit', 'bing' +]; + +var DEFAULT_PREFS = { + theme: 'system', + engines: ALL_ENGINES.slice(), + safeSearch: 'moderate', + format: 'html' +}; + +var STORAGE_KEY = 'kafka_prefs'; + +function loadPrefs() { + var stored = localStorage.getItem(STORAGE_KEY); + var prefs = DEFAULT_PREFS; + if (stored) { + try { + var parsed = JSON.parse(stored); + prefs = { + theme: parsed.theme || DEFAULT_PREFS.theme, + engines: parsed.engines || DEFAULT_PREFS.engines.slice(), + safeSearch: parsed.safeSearch || DEFAULT_PREFS.safeSearch, + format: parsed.format || DEFAULT_PREFS.format + }; + } catch (e) { + prefs = DEFAULT_PREFS; + } + } + return prefs; +} + +function savePrefs(prefs) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs)); +} + +function applyTheme(theme) { + if (theme === 'system') { + document.documentElement.removeAttribute('data-theme'); + } else { + document.documentElement.setAttribute('data-theme', theme); + } +} + +function syncEngineInput(prefs) { + var input = document.getElementById('engines-input'); + if (input) { + input.value = prefs.engines.join(','); + } +} + +function closePanel() { + var popover = document.getElementById('settings-popover'); + var trigger = document.getElementById('settings-trigger'); + if (popover) { + popover.setAttribute('data-open', 'false'); + } + if (trigger) { + trigger.setAttribute('aria-expanded', 'false'); + } + if (trigger) { + trigger.focus(); + } +} + +function openPanel() { + var popover = document.getElementById('settings-popover'); + var trigger = document.getElementById('settings-trigger'); + if (popover) { + popover.setAttribute('data-open', 'true'); + } + if (trigger) { + trigger.setAttribute('aria-expanded', 'true'); + } + var firstFocusable = popover ? popover.querySelector('button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])') : null; + if (firstFocusable) { + firstFocusable.focus(); + } +} + +function escapeHtml(str) { + return str + .replace(/&/g, '&') + .replace(//g, '>'); +} + +function renderPanel(prefs) { + var themeIcons = { light: '☀️', dark: '🌙', system: '⌨' }; + var safeOptions = ['moderate', 'strict', 'off']; + var formatOptions = ['html', 'json', 'csv', 'rss']; + + var html = '
'; + html += '

Settings

'; + + // Theme buttons + html += '
Theme'; + html += '
'; + ['light', 'dark', 'system'].forEach(function(t) { + var checked = prefs.theme === t ? ' checked' : ''; + var icon = escapeHtml(themeIcons[t]); + html += ''; + }); + html += '
'; + + // Engine checkboxes + html += '
Engines'; + html += '
'; + ALL_ENGINES.forEach(function(engine) { + var checked = prefs.engines.indexOf(engine) !== -1 ? ' checked' : ''; + html += ''; + }); + html += '
'; + + html += '

Engine changes apply to your next search.

'; + + // Safe search dropdown + html += ''; + html += ''; + + // Format dropdown + html += ''; + html += ''; + + html += ''; + html += '
'; + + document.body.innerHTML = html; + + // Attach listeners to theme buttons + var themeButtons = document.querySelectorAll('input[name="theme"]'); + themeButtons.forEach(function(btn) { + btn.addEventListener('click', function() { + var newPrefs = loadPrefs(); + newPrefs.theme = btn.value; + savePrefs(newPrefs); + applyTheme(newPrefs.theme); + }); + }); + + // Attach listeners to engine checkboxes + var engineCheckboxes = document.querySelectorAll('input[name="engine"]'); + engineCheckboxes.forEach(function(cb) { + cb.addEventListener('change', function() { + var newPrefs = loadPrefs(); + if (cb.checked) { + if (newPrefs.engines.indexOf(cb.value) === -1) { + newPrefs.engines.push(cb.value); + } + } else { + newPrefs.engines = newPrefs.engines.filter(function(e) { return e !== cb.value; }); + } + savePrefs(newPrefs); + syncEngineInput(newPrefs); + }); + }); + + // Attach listener to safe search dropdown + var safeSelect = document.getElementById('safe-search-select'); + if (safeSelect) { + safeSelect.addEventListener('change', function() { + var newPrefs = loadPrefs(); + newPrefs.safeSearch = safeSelect.value; + savePrefs(newPrefs); + }); + } + + // Attach listener to format dropdown + var formatSelect = document.getElementById('format-select'); + if (formatSelect) { + formatSelect.addEventListener('change', function() { + var newPrefs = loadPrefs(); + newPrefs.format = formatSelect.value; + savePrefs(newPrefs); + }); + } + + // Attach listener to close button + var closeBtn = document.getElementById('settings-close'); + if (closeBtn) { + closeBtn.addEventListener('click', closePanel); + } +} + +function initSettings() { + var prefs = loadPrefs(); + applyTheme(prefs.theme); + syncEngineInput(prefs); + renderPanel(prefs); + + var trigger = document.getElementById('settings-trigger'); + var triggerMobile = document.getElementById('settings-trigger-mobile'); + + function togglePanel() { + var popover = document.getElementById('settings-popover'); + if (popover && popover.getAttribute('data-open') === 'true') { + closePanel(); + } else { + openPanel(); + } + } + + if (trigger) { + trigger.addEventListener('click', togglePanel); + } + if (triggerMobile) { + triggerMobile.addEventListener('click', togglePanel); + } +} + +// Escape key handler +document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + var popover = document.getElementById('settings-popover'); + if (popover && popover.getAttribute('data-open') === 'true') { + closePanel(); + } + } +}); + +// Click outside handler +document.addEventListener('click', function(e) { + var popover = document.getElementById('settings-popover'); + var trigger = document.getElementById('settings-trigger'); + if (popover && popover.getAttribute('data-open') === 'true') { + if (!popover.contains(e.target) && (!trigger || !trigger.contains(e.target))) { + closePanel(); + } + } +}); + +// Focus trap +document.addEventListener('keydown', function(e) { + if (e.key === 'Tab') { + var popover = document.getElementById('settings-popover'); + if (popover && popover.getAttribute('data-open') === 'true') { + var focusableElements = popover.querySelectorAll('button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])'); + var firstEl = focusableElements[0]; + var lastEl = focusableElements[focusableElements.length - 1]; + if (e.shiftKey && document.activeElement === firstEl) { + e.preventDefault(); + lastEl.focus(); + } else if (!e.shiftKey && document.activeElement === lastEl) { + e.preventDefault(); + firstEl.focus(); + } + } + } +}); + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initSettings); +} else { + initSettings(); +}