diff --git a/cmd/kafka/main.go b/cmd/kafka/main.go index cdc81b5..3a0a80e 100644 --- a/cmd/kafka/main.go +++ b/cmd/kafka/main.go @@ -84,6 +84,7 @@ func main() { mux.HandleFunc("/healthz", h.Healthz) mux.HandleFunc("/search", h.Search) mux.HandleFunc("/autocompleter", h.Autocompleter) + mux.HandleFunc("/preferences", h.Preferences) 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 f8db054..ce4165b 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) } @@ -141,3 +142,20 @@ 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 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) + } +} diff --git a/internal/views/static/css/kafka.css b/internal/views/static/css/kafka.css index 8ae97ea..ad094e9 100644 --- a/internal/views/static/css/kafka.css +++ b/internal/views/static/css/kafka.css @@ -975,6 +975,383 @@ 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, +.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; + } +} + +/* ============================================================ + 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/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 = '