diff --git a/internal/httpapi/handlers.go b/internal/httpapi/handlers.go index f83731d..4516981 100644 --- a/internal/httpapi/handlers.go +++ b/internal/httpapi/handlers.go @@ -55,13 +55,24 @@ func (h *Handler) Healthz(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("OK")) } +// getTheme returns the user's theme preference from cookie, defaulting to "light". +func (h *Handler) getTheme(r *http.Request) string { + if cookie, err := r.Cookie("theme"); err == nil { + if cookie.Value == "dark" || cookie.Value == "light" { + return cookie.Value + } + } + return "light" +} + // Index renders the homepage with the search box. func (h *Handler) Index(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } - if err := views.RenderIndex(w, h.sourceURL); err != nil { + theme := h.getTheme(r) + if err := views.RenderIndex(w, h.sourceURL, theme); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } @@ -92,7 +103,7 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) { req, err := search.ParseSearchRequest(r) if err != nil { if format == "html" || format == "" { - pd := views.PageData{SourceURL: h.sourceURL, Query: q} + pd := views.PageData{SourceURL: h.sourceURL, Query: q, Theme: h.getTheme(r)} if views.IsHTMXRequest(r) { views.RenderSearchFragment(w, pd) } else { @@ -107,7 +118,7 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) { resp, err := h.searchSvc.Search(r.Context(), req) if err != nil { if req.Format == contracts.FormatHTML { - pd := views.PageData{SourceURL: h.sourceURL, Query: req.Query} + pd := views.PageData{SourceURL: h.sourceURL, Query: req.Query, Theme: h.getTheme(r)} if views.IsHTMXRequest(r) { views.RenderSearchFragment(w, pd) } else { @@ -122,6 +133,7 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) { if req.Format == contracts.FormatHTML { pd := views.FromResponse(resp, req.Query, req.Pageno, r.FormValue("category"), r.FormValue("time"), r.FormValue("type")) + pd.Theme = h.getTheme(r) if err := views.RenderSearchAuto(w, r, pd); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } @@ -158,12 +170,29 @@ func (h *Handler) Preferences(w http.ResponseWriter, r *http.Request) { return } if r.Method == "POST" { - // Preferences are stored in localStorage on the client via JavaScript. - // This handler exists only for form submission completeness. + // Handle theme preference via server-side cookie + theme := r.FormValue("theme") + if theme == "dark" || theme == "light" { + http.SetCookie(w, &http.Cookie{ + Name: "theme", + Value: theme, + Path: "/", + MaxAge: 86400 * 365, + HttpOnly: false, // Allow CSS to read via :has() + SameSite: http.SameSiteLaxMode, + }) + } http.Redirect(w, r, "/preferences", http.StatusFound) return } - if err := views.RenderPreferences(w, h.sourceURL); err != nil { + // Read theme cookie for template + theme := "light" + if cookie, err := r.Cookie("theme"); err == nil { + if cookie.Value == "dark" || cookie.Value == "light" { + theme = cookie.Value + } + } + if err := views.RenderPreferences(w, h.sourceURL, theme); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } diff --git a/internal/views/templates/base.html b/internal/views/templates/base.html index 7983f14..898bd6c 100644 --- a/internal/views/templates/base.html +++ b/internal/views/templates/base.html @@ -1,6 +1,6 @@ {{define "base"}} - + diff --git a/internal/views/templates/preferences.html b/internal/views/templates/preferences.html index de335a0..4217370 100644 --- a/internal/views/templates/preferences.html +++ b/internal/views/templates/preferences.html @@ -8,8 +8,11 @@

Appearance

- - Follows system preference + +
diff --git a/internal/views/views.go b/internal/views/views.go index e7ce360..54a1a84 100644 --- a/internal/views/views.go +++ b/internal/views/views.go @@ -55,6 +55,8 @@ type PageData struct { PageNumbers []PageNumber ShowHeader bool IsImageSearch bool + // Theme is the user's selected theme (light/dark) from cookie + Theme string // New fields for three-column layout Categories []string CategoryIcons map[string]string @@ -280,9 +282,9 @@ func FromResponse(resp contracts.SearchResponse, query string, pageno int, activ } // RenderIndex renders the homepage (search box only). -func RenderIndex(w http.ResponseWriter, sourceURL string) error { +func RenderIndex(w http.ResponseWriter, sourceURL, theme string) error { w.Header().Set("Content-Type", "text/html; charset=utf-8") - return tmplIndex.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL}) + return tmplIndex.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL, Theme: theme}) } // RenderSearch renders the full search results page (with base layout). @@ -326,8 +328,8 @@ func RenderSearchAuto(w http.ResponseWriter, r *http.Request, data PageData) err } // RenderPreferences renders the full preferences page. -func RenderPreferences(w http.ResponseWriter, sourceURL string) error { +func RenderPreferences(w http.ResponseWriter, sourceURL, theme string) error { w.Header().Set("Content-Type", "text/html; charset=utf-8") - return tmplPreferences.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL}) + return tmplPreferences.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL, Theme: theme}) }