From 0af49f91b75d3e31b8efd60bb8f47df0a489d61f Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Sun, 22 Mar 2026 13:29:39 +0100 Subject: [PATCH 01/10] feat(frontend): add CSS layout framework for three-column results and preferences page Co-Authored-By: Claude Opus 4.6 --- internal/views/static/css/kafka.css | 300 ++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) diff --git a/internal/views/static/css/kafka.css b/internal/views/static/css/kafka.css index 8ae97ea..d23ea7a 100644 --- a/internal/views/static/css/kafka.css +++ b/internal/views/static/css/kafka.css @@ -975,6 +975,306 @@ a:focus-visible { background: var(--border-focus); } +/* ============================================================ + Three-Column Results Layout + ============================================================ */ + +.results-layout { + display: grid; + grid-template-columns: 200px 1fr 240px; + gap: 2rem; + align-items: start; +} + +.results-layout .left-sidebar { + position: sticky; + top: calc(var(--header-height) + 1.5rem); + max-height: calc(100vh - var(--header-height) - 3rem); + overflow-y: auto; +} + +.results-layout .right-sidebar { + position: sticky; + top: calc(var(--header-height) + 1.5rem); + max-height: calc(100vh - var(--header-height) - 3rem); + overflow-y: auto; +} + +.results-layout .results-column { + min-width: 0; +} + +/* Tablet: hide left sidebar, two columns */ +@media (min-width: 769px) and (max-width: 1024px) { + .results-layout { + grid-template-columns: 1fr 220px; + } + .results-layout .left-sidebar { + display: none; + } +} + +/* Mobile: single column, no sidebars */ +@media (max-width: 768px) { + .results-layout { + grid-template-columns: 1fr; + } + .results-layout .left-sidebar, + .results-layout .right-sidebar { + display: none; + } +} + +/* ============================================================ + Preferences Page Layout + ============================================================ */ + +.preferences-layout { + display: grid; + grid-template-columns: 200px 1fr; + gap: 2rem; + align-items: start; + padding: 2rem 0; +} + +.preferences-nav { + position: sticky; + top: calc(var(--header-height) + 1.5rem); +} + +.preferences-nav-item { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.6rem 0.75rem; + border-radius: var(--radius-sm); + color: var(--text-secondary); + text-decoration: none; + font-size: 0.9rem; + transition: background 0.15s, color 0.15s; + cursor: pointer; +} + +.preferences-nav-item:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.preferences-nav-item.active { + background: var(--accent-soft); + color: var(--accent); + font-weight: 500; +} + +.preferences-content { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.5rem; +} + +@media (max-width: 768px) { + .preferences-layout { + grid-template-columns: 1fr; + } + .preferences-nav { + position: static; + display: flex; + overflow-x: auto; + gap: 0.5rem; + padding-bottom: 0.5rem; + } + .preferences-nav-item { + white-space: nowrap; + } +} + +/* ============================================================ + Category Tiles + ============================================================ */ + +.category-tiles { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 1rem; + margin-top: 2rem; +} + +.category-tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 1rem 0.5rem; + border-radius: var(--radius-md); + text-decoration: none; + color: var(--text-secondary); + font-size: 0.85rem; + transition: background 0.15s, color 0.15s, transform 0.15s, box-shadow 0.15s; +} + +.category-tile:hover { + background: var(--bg-tertiary); + color: var(--text-primary); + transform: translateY(-2px); + box-shadow: var(--shadow-sm); +} + +.category-tile-icon { + font-size: 1.5rem; + line-height: 1; +} + +.category-tile.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +@media (max-width: 768px) { + .category-tiles { + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + gap: 0.75rem; + } + .category-tile { + padding: 0.75rem 0.25rem; + font-size: 0.75rem; + } + .category-tile-icon { + font-size: 1.25rem; + } +} + +/* ============================================================ + Left Sidebar (Results Page) + ============================================================ */ + +.left-sidebar { + padding: 0; +} + +.sidebar-nav { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.sidebar-nav-title { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + padding: 0.5rem 0.75rem; + margin-top: 0.5rem; +} + +.sidebar-nav-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + border-radius: var(--radius-sm); + color: var(--text-secondary); + text-decoration: none; + font-size: 0.875rem; + transition: background 0.15s, color 0.15s; +} + +.sidebar-nav-item:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.sidebar-nav-item.active { + background: var(--accent-soft); + color: var(--accent); + font-weight: 500; +} + +.sidebar-nav-item-icon { + font-size: 1rem; + width: 20px; + text-align: center; +} + +.sidebar-filters { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border); +} + +.sidebar-filter-group { + margin-bottom: 0.75rem; +} + +.sidebar-filter-label { + font-size: 0.75rem; + font-weight: 500; + color: var(--text-muted); + padding: 0 0.75rem; + margin-bottom: 0.25rem; +} + +.sidebar-filter-option { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.75rem; + font-size: 0.8rem; + color: var(--text-secondary); + cursor: pointer; + border-radius: var(--radius-sm); + transition: background 0.15s; +} + +.sidebar-filter-option:hover { + background: var(--bg-tertiary); +} + +.sidebar-filter-option input[type="radio"] { + accent-color: var(--accent); +} + +/* Mobile filter chips */ +.mobile-filter-chips { + display: none; + overflow-x: auto; + gap: 0.5rem; + padding: 0.75rem 0; + -webkit-overflow-scrolling: touch; +} + +.mobile-filter-chips::-webkit-scrollbar { + display: none; +} + +.mobile-filter-chip { + display: inline-flex; + align-items: center; + padding: 0.4rem 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-full); + font-size: 0.8rem; + color: var(--text-secondary); + white-space: nowrap; + text-decoration: none; + transition: background 0.15s, border-color 0.15s; +} + +.mobile-filter-chip:hover, +.mobile-filter-chip.active { + background: var(--accent-soft); + border-color: var(--accent); + color: var(--accent); +} + +@media (max-width: 768px) { + .mobile-filter-chips { + display: flex; + } +} + /* ============================================================ Print ============================================================ */ From 2e7075adf1ad01be8ad29c1d5831fe3b132a78e2 Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Sun, 22 Mar 2026 13:33:24 +0100 Subject: [PATCH 02/10] fix(frontend): merge duplicate sidebar sticky rules --- internal/views/static/css/kafka.css | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/views/static/css/kafka.css b/internal/views/static/css/kafka.css index d23ea7a..9f014e0 100644 --- a/internal/views/static/css/kafka.css +++ b/internal/views/static/css/kafka.css @@ -986,13 +986,7 @@ a:focus-visible { align-items: start; } -.results-layout .left-sidebar { - position: sticky; - top: calc(var(--header-height) + 1.5rem); - max-height: calc(100vh - var(--header-height) - 3rem); - overflow-y: auto; -} - +.results-layout .left-sidebar, .results-layout .right-sidebar { position: sticky; top: calc(var(--header-height) + 1.5rem); From 0e79b729fee5df61d185acfb69c1ef5ee45bc6e9 Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Sun, 22 Mar 2026 13:36:09 +0100 Subject: [PATCH 03/10] feat(frontend): add three-column results layout with left sidebar navigation Co-Authored-By: Claude Opus 4.6 --- internal/httpapi/handlers.go | 3 +- internal/views/templates/results.html | 85 ++++++++++++++++++++------- internal/views/views.go | 57 +++++++++++++++++- 3 files changed, 122 insertions(+), 23 deletions(-) diff --git a/internal/httpapi/handlers.go b/internal/httpapi/handlers.go index f8db054..e27db01 100644 --- a/internal/httpapi/handlers.go +++ b/internal/httpapi/handlers.go @@ -112,7 +112,8 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) { } if req.Format == contracts.FormatHTML { - pd := views.FromResponse(resp, req.Query, req.Pageno) + pd := views.FromResponse(resp, req.Query, req.Pageno, + r.FormValue("category"), r.FormValue("time"), r.FormValue("type")) if err := views.RenderSearchAuto(w, r, pd); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } diff --git a/internal/views/templates/results.html b/internal/views/templates/results.html index 7010a3a..39e7c64 100644 --- a/internal/views/templates/results.html +++ b/internal/views/templates/results.html @@ -1,32 +1,75 @@ {{define "title"}}{{if .Query}}{{.Query}} โ€” {{end}}{{end}} {{define "content"}}
- -
- -
+ + + + +
+ +
+ +
+ + +
+ All + {{range .Categories}} + {{.}} + {{end}} +
+ + {{template "results_inner" .}}
- -
-{{end}} +{{end}} \ No newline at end of file diff --git a/internal/views/views.go b/internal/views/views.go index 4d7289c..0161d2b 100644 --- a/internal/views/views.go +++ b/internal/views/views.go @@ -50,6 +50,15 @@ type PageData struct { UnresponsiveEngines [][2]string PageNumbers []PageNumber ShowHeader bool + // New fields for three-column layout + Categories []string + CategoryIcons map[string]string + DisabledCategories []string + ActiveCategory string + TimeFilters []FilterOption + TypeFilters []FilterOption + ActiveTime string + ActiveType string } // ResultView is a template-friendly wrapper around a MainResult. @@ -73,6 +82,12 @@ type InfoboxView struct { ImgSrc string } +// FilterOption represents a filter radio option for the sidebar. +type FilterOption struct { + Label string + Value string +} + var ( tmplFull *template.Template tmplIndex *template.Template @@ -116,12 +131,52 @@ func OpenSearchXML(baseURL string) ([]byte, error) { } // FromResponse builds PageData from a search response and request params. -func FromResponse(resp contracts.SearchResponse, query string, pageno int) PageData { +func FromResponse(resp contracts.SearchResponse, query string, pageno int, activeCategory, activeTime, activeType string) PageData { + // Set defaults + if activeCategory == "" { + activeCategory = "all" + } + pd := PageData{ Query: query, Pageno: pageno, NumberOfResults: resp.NumberOfResults, UnresponsiveEngines: resp.UnresponsiveEngines, + + // New: categories with icons + Categories: []string{"all", "news", "images", "videos", "maps"}, + DisabledCategories: []string{"shopping", "music", "weather"}, + CategoryIcons: map[string]string{ + "all": "๐ŸŒ", + "news": "๐Ÿ“ฐ", + "images": "๐Ÿ–ผ๏ธ", + "videos": "๐ŸŽฌ", + "maps": "๐Ÿ—บ๏ธ", + "shopping": "๐Ÿ›’", + "music": "๐ŸŽต", + "weather": "๐ŸŒค๏ธ", + }, + ActiveCategory: activeCategory, + + // Time filters + TimeFilters: []FilterOption{ + {Label: "Any time", Value: ""}, + {Label: "Past hour", Value: "h"}, + {Label: "Past 24 hours", Value: "d"}, + {Label: "Past week", Value: "w"}, + {Label: "Past month", Value: "m"}, + {Label: "Past year", Value: "y"}, + }, + ActiveTime: activeTime, + + // Type filters + TypeFilters: []FilterOption{ + {Label: "All results", Value: ""}, + {Label: "News", Value: "news"}, + {Label: "Videos", Value: "video"}, + {Label: "Images", Value: "image"}, + }, + ActiveType: activeType, } // Convert results. From bfcbd45c572a8db13ffedc2c95e0a0e9f42cc935 Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Sun, 22 Mar 2026 13:40:16 +0100 Subject: [PATCH 04/10] fix(frontend): update FromResponse tests and fix disabled categories rendering Co-Authored-By: Claude Opus 4.6 --- internal/views/templates/results.html | 7 +++++++ internal/views/views.go | 1 - internal/views/views_test.go | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/views/templates/results.html b/internal/views/templates/results.html index 39e7c64..59bc525 100644 --- a/internal/views/templates/results.html +++ b/internal/views/templates/results.html @@ -11,6 +11,13 @@ {{.}} {{end}} + + {{range .DisabledCategories}} + + {{index $.CategoryIcons .}} + {{.}} + + {{end}} + + +
-{{end}} +{{end}} \ No newline at end of file From b4053b7f9894aba0ee363d6bbfc489d8a1734795 Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Sun, 22 Mar 2026 13:47:30 +0100 Subject: [PATCH 06/10] feat(frontend): add preferences page template and styles Co-Authored-By: Claude Opus 4.6 --- internal/views/static/css/kafka.css | 83 ++++++++++ internal/views/templates/preferences.html | 191 ++++++++++++++++++++++ internal/views/views.go | 10 ++ 3 files changed, 284 insertions(+) create mode 100644 internal/views/templates/preferences.html diff --git a/internal/views/static/css/kafka.css b/internal/views/static/css/kafka.css index 9f014e0..ad094e9 100644 --- a/internal/views/static/css/kafka.css +++ b/internal/views/static/css/kafka.css @@ -1269,6 +1269,89 @@ a:focus-visible { } } +/* ============================================================ + Preferences Page Styles + ============================================================ */ + +.pref-section { + margin-bottom: 2rem; +} + +.pref-section:last-child { + margin-bottom: 0; +} + +.pref-section-title { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} + +.pref-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.75rem 0; + border-bottom: 1px solid var(--border); +} + +.pref-row:last-child { + border-bottom: none; +} + +.pref-row label { + font-size: 0.9rem; + color: var(--text-primary); +} + +.pref-row-info { + flex: 1; +} + +.pref-row-info label { + font-weight: 500; +} + +.pref-desc { + font-size: 0.8rem; + color: var(--text-muted); + margin-top: 0.25rem; +} + +.pref-row select { + padding: 0.5rem 0.75rem; + font-size: 0.85rem; + font-family: inherit; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg); + color: var(--text-primary); + cursor: pointer; + min-width: 150px; +} + +.pref-row select:focus { + outline: none; + border-color: var(--accent); +} + +.pref-row input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--accent); + cursor: pointer; + flex-shrink: 0; +} + +.pref-row input[type="checkbox"]:disabled { + opacity: 0.6; + cursor: not-allowed; +} + /* ============================================================ Print ============================================================ */ diff --git a/internal/views/templates/preferences.html b/internal/views/templates/preferences.html new file mode 100644 index 0000000..394c27f --- /dev/null +++ b/internal/views/templates/preferences.html @@ -0,0 +1,191 @@ +{{define "title"}}Preferences{{end}} +{{define "content"}} +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+{{end}} \ No newline at end of file diff --git a/internal/views/views.go b/internal/views/views.go index ac6d4b0..2dec5a7 100644 --- a/internal/views/views.go +++ b/internal/views/views.go @@ -92,6 +92,7 @@ var ( tmplFull *template.Template tmplIndex *template.Template tmplFragment *template.Template + tmplPreferences *template.Template ) func init() { @@ -111,6 +112,9 @@ func init() { tmplFragment = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS, "results_inner.html", "result_item.html", "video_item.html", )) + tmplPreferences = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS, + "base.html", "preferences.html", + )) } // StaticFS returns the embedded static file system for serving CSS/JS/images. @@ -288,3 +292,9 @@ func RenderSearchAuto(w http.ResponseWriter, r *http.Request, data PageData) err return RenderSearch(w, data) } +// RenderPreferences renders the full preferences page. +func RenderPreferences(w http.ResponseWriter, sourceURL string) error { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + return tmplPreferences.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL}) +} + From 70818558cd0bb25458e97c6f4d2b84a5a052ce23 Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Sun, 22 Mar 2026 13:53:23 +0100 Subject: [PATCH 07/10] feat: add GET and POST /preferences route Co-Authored-By: Claude Opus 4.6 --- cmd/kafka/main.go | 2 ++ internal/httpapi/handlers.go | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/cmd/kafka/main.go b/cmd/kafka/main.go index cdc81b5..6785ba7 100644 --- a/cmd/kafka/main.go +++ b/cmd/kafka/main.go @@ -84,6 +84,8 @@ func main() { mux.HandleFunc("/healthz", h.Healthz) mux.HandleFunc("/search", h.Search) mux.HandleFunc("/autocompleter", h.Autocompleter) + mux.HandleFunc("/preferences", h.Preferences) + mux.HandleFunc("POST /preferences", h.PreferencesPOST) mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL)) // Serve embedded static files (CSS, JS, images). diff --git a/internal/httpapi/handlers.go b/internal/httpapi/handlers.go index e27db01..46df1d9 100644 --- a/internal/httpapi/handlers.go +++ b/internal/httpapi/handlers.go @@ -142,3 +142,26 @@ func (h *Handler) Autocompleter(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(suggestions) } + +// Preferences renders the preferences page. +func (h *Handler) Preferences(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/preferences" { + http.NotFound(w, r) + return + } + if err := views.RenderPreferences(w, h.sourceURL); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// PreferencesPOST handles form submission from the preferences page. +// NOTE: This is a no-op. All preferences are stored in localStorage on the client +// via JavaScript. This handler exists only for form submission completeness (e.g., +// if a form POSTs without JS). The JavaScript in settings.js handles all saves. +func (h *Handler) PreferencesPOST(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/preferences" { + http.NotFound(w, r) + return + } + http.Redirect(w, r, "/preferences", http.StatusFound) +} From 0afcf509c31ebdb95fdff122d3866344b6613adc Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Sun, 22 Mar 2026 13:57:32 +0100 Subject: [PATCH 08/10] fix: use single Preferences handler with method check instead of dead POST route --- cmd/kafka/main.go | 1 - internal/httpapi/handlers.go | 20 +++++++------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/cmd/kafka/main.go b/cmd/kafka/main.go index 6785ba7..3a0a80e 100644 --- a/cmd/kafka/main.go +++ b/cmd/kafka/main.go @@ -85,7 +85,6 @@ func main() { mux.HandleFunc("/search", h.Search) mux.HandleFunc("/autocompleter", h.Autocompleter) mux.HandleFunc("/preferences", h.Preferences) - mux.HandleFunc("POST /preferences", h.PreferencesPOST) mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL)) // Serve embedded static files (CSS, JS, images). diff --git a/internal/httpapi/handlers.go b/internal/httpapi/handlers.go index 46df1d9..ce4165b 100644 --- a/internal/httpapi/handlers.go +++ b/internal/httpapi/handlers.go @@ -143,25 +143,19 @@ func (h *Handler) Autocompleter(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(suggestions) } -// Preferences renders the preferences page. +// Preferences handles GET and POST for the preferences page. func (h *Handler) Preferences(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/preferences" { http.NotFound(w, r) return } + if r.Method == "POST" { + // Preferences are stored in localStorage on the client via JavaScript. + // This handler exists only for form submission completeness. + http.Redirect(w, r, "/preferences", http.StatusFound) + return + } if err := views.RenderPreferences(w, h.sourceURL); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } - -// PreferencesPOST handles form submission from the preferences page. -// NOTE: This is a no-op. All preferences are stored in localStorage on the client -// via JavaScript. This handler exists only for form submission completeness (e.g., -// if a form POSTs without JS). The JavaScript in settings.js handles all saves. -func (h *Handler) PreferencesPOST(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/preferences" { - http.NotFound(w, r) - return - } - http.Redirect(w, r, "/preferences", http.StatusFound) -} From 6d7e68ada17a655ef417097c71c32baec08b095a Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Sun, 22 Mar 2026 14:00:53 +0100 Subject: [PATCH 09/10] feat(frontend): reduce popover to theme+engines, add preferences page JS --- internal/views/static/js/settings.js | 104 ++++++++++++++++----------- 1 file changed, 64 insertions(+), 40 deletions(-) diff --git a/internal/views/static/js/settings.js b/internal/views/static/js/settings.js index 762fbcb..9682e6a 100644 --- a/internal/views/static/js/settings.js +++ b/internal/views/static/js/settings.js @@ -103,28 +103,7 @@ function renderPanel(prefs) { engineToggles += ''; }); - var ssOptions = [ - { val: 'moderate', label: 'Moderate' }, - { val: 'strict', label: 'Strict' }, - { val: 'off', label: 'Off' } - ]; - var fmtOptions = [ - { val: 'html', label: 'HTML' }, - { val: 'json', label: 'JSON' }, - { val: 'csv', label: 'CSV' }, - { val: 'rss', label: 'RSS' } - ]; - var ssOptionsHtml = ''; - var fmtOptionsHtml = ''; - ssOptions.forEach(function(o) { - var sel = prefs.safeSearch === o.val ? ' selected' : ''; - ssOptionsHtml += ''; - }); - fmtOptions.forEach(function(o) { - var sel = prefs.format === o.val ? ' selected' : ''; - fmtOptionsHtml += ''; - }); - + body.innerHTML = '
' + '
Appearance
' + @@ -175,24 +154,6 @@ function renderPanel(prefs) { })(checkboxes[j])); } - // Safe search - var ssEl = panel.querySelector('#pref-safesearch'); - if (ssEl) { - ssEl.addEventListener('change', function() { - prefs.safeSearch = ssEl.value; - savePrefs(prefs); - }); - } - - // Format - var fmtEl = panel.querySelector('#pref-format'); - if (fmtEl) { - fmtEl.addEventListener('change', function() { - prefs.format = fmtEl.value; - savePrefs(prefs); - }); - } - // Close button var closeBtn = panel.querySelector('.settings-popover-close'); if (closeBtn) closeBtn.addEventListener('click', closePanel); @@ -269,3 +230,66 @@ if (document.readyState === 'loading') { } else { initSettings(); } + +// Preferences page navigation +function initPreferences() { + var nav = document.getElementById('preferences-nav'); + if (!nav) return; + + var sections = document.querySelectorAll('.pref-section'); + var navItems = nav.querySelectorAll('.preferences-nav-item'); + + function showSection(id) { + sections.forEach(function(sec) { + sec.style.display = sec.id === 'section-' + id ? 'block' : 'none'; + }); + navItems.forEach(function(item) { + item.classList.toggle('active', item.getAttribute('data-section') === id); + }); + } + + navItems.forEach(function(item) { + item.addEventListener('click', function() { + showSection(item.getAttribute('data-section')); + }); + }); + + // Load saved preferences + var prefs = loadPrefs(); + + // Theme + var themeEl = document.getElementById('pref-theme'); + if (themeEl) { + themeEl.value = prefs.theme || 'system'; + themeEl.addEventListener('change', function() { + prefs.theme = themeEl.value; + savePrefs(prefs); + applyTheme(prefs.theme); + }); + } + + // Safe search + var ssEl = document.getElementById('pref-safesearch'); + if (ssEl) { + ssEl.value = prefs.safeSearch || 'moderate'; + ssEl.addEventListener('change', function() { + prefs.safeSearch = ssEl.value; + savePrefs(prefs); + }); + } + + // Format (if exists on page) + var fmtEl = document.getElementById('pref-format'); + if (fmtEl) { + fmtEl.value = prefs.format || 'html'; + fmtEl.addEventListener('change', function() { + prefs.format = fmtEl.value; + savePrefs(prefs); + }); + } + + // Show first section by default + showSection('search'); +} + +document.addEventListener('DOMContentLoaded', initPreferences); From e18a54a41a9baead10b949c961d45fe3baeaa20f Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Sun, 22 Mar 2026 14:05:26 +0100 Subject: [PATCH 10/10] fix(frontend): add HTMX filter submission for sidebar radio buttons Wrap sidebar time/type filters in a form with HTMX attributes so filter changes trigger partial page updates instead of full reload. Co-Authored-By: Claude Opus 4.6 --- internal/views/templates/results.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/views/templates/results.html b/internal/views/templates/results.html index 59bc525..1e02fb9 100644 --- a/internal/views/templates/results.html +++ b/internal/views/templates/results.html @@ -20,7 +20,10 @@ {{end}} - +