Skip to content

Commit 096aab5

Browse files
committed
Theme switcher
1 parent dd7ade4 commit 096aab5

File tree

13 files changed

+378
-27
lines changed

13 files changed

+378
-27
lines changed

app/handlers/routes.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ import (
1414

1515
func RegisterRoutes(r chi.Router, cfg *config.Config, logger *slog.Logger, sqlDB *sql.DB, queries *db.Queries, sessionStore *sessions.CookieStore) {
1616
r.Use(rctx.SessionCtxMiddleware(logger, sessionStore))
17+
r.Use(rctx.ThemeCtxMiddleware())
1718

1819
r.Get("/", handleHome(logger))
1920

2021
r.Get("/new", handleSpacesNew(cfg, logger))
2122
r.Post("/new", handleSpacesCreate(cfg, logger, sqlDB, queries))
2223

2324
r.Get("/language", handleLanguageSelect(logger))
25+
r.Get("/theme", handleThemeSelect(logger))
26+
r.Post("/theme", handleThemeSet(logger))
2427

2528
r.Route("/s/{token}", func(r chi.Router) {
2629
r.Use(rctx.SpaceCtxMiddleware(queries))

app/handlers/theme.handlers.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package handlers
2+
3+
import (
4+
"log/slog"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/nicolashery/simply-shared-notes/app/rctx"
9+
"github.com/nicolashery/simply-shared-notes/app/session"
10+
"github.com/nicolashery/simply-shared-notes/app/views/pages"
11+
)
12+
13+
func handleThemeSelect(logger *slog.Logger) http.HandlerFunc {
14+
return func(w http.ResponseWriter, r *http.Request) {
15+
err := pages.ThemeSelect().Render(r.Context(), w)
16+
if err != nil {
17+
logger.Error(
18+
"failed to render template",
19+
slog.Any("error", err),
20+
slog.String("template", "ThemeSelect"),
21+
)
22+
http.Error(w, "internal server error", http.StatusInternalServerError)
23+
}
24+
}
25+
}
26+
27+
type SelectThemeForm struct {
28+
Theme string
29+
}
30+
31+
func parseSelectThemeForm(r *http.Request, f *SelectThemeForm) error {
32+
err := r.ParseForm()
33+
if err != nil {
34+
return err
35+
}
36+
37+
f.Theme = strings.Trim(r.Form.Get("theme"), " ")
38+
39+
return nil
40+
}
41+
42+
func handleThemeSet(logger *slog.Logger) http.HandlerFunc {
43+
return func(w http.ResponseWriter, r *http.Request) {
44+
var form SelectThemeForm
45+
err := parseSelectThemeForm(r, &form)
46+
if err != nil {
47+
http.Error(w, "failed to parse form", http.StatusBadRequest)
48+
return
49+
}
50+
51+
validThemes := map[string]bool{
52+
"light": true,
53+
"dark": true,
54+
"": true, // system default
55+
}
56+
57+
if !validThemes[form.Theme] {
58+
http.Error(w, "invalid theme", http.StatusBadRequest)
59+
return
60+
}
61+
62+
sess := rctx.GetSession(r.Context())
63+
64+
if form.Theme == "" {
65+
delete(sess.Values, session.ThemeKey)
66+
} else {
67+
sess.Values[session.ThemeKey] = form.Theme
68+
}
69+
70+
err = sess.Save(r, w)
71+
if err != nil {
72+
logger.Error("failed to save session", slog.Any("error", err))
73+
http.Error(w, "internal server error", http.StatusInternalServerError)
74+
return
75+
}
76+
77+
// Redirect back to referring page or home
78+
referer := r.Header.Get("Referer")
79+
if referer != "" {
80+
http.Redirect(w, r, referer, http.StatusSeeOther)
81+
} else {
82+
http.Redirect(w, r, "/", http.StatusSeeOther)
83+
}
84+
}
85+
}

app/rctx/context_keys.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ type contextKey string
55
const (
66
viteContextKey contextKey = "vite"
77
sessionContextKey contextKey = "session"
8+
themeContextKey contextKey = "theme"
89
spaceContextKey contextKey = "space"
910
spaceStatsContextKey contextKey = "space_stats"
1011
accessContextKey contextKey = "access"

app/rctx/theme.context.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package rctx
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/nicolashery/simply-shared-notes/app/session"
8+
)
9+
10+
func ThemeCtxMiddleware() func(http.Handler) http.Handler {
11+
return func(next http.Handler) http.Handler {
12+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+
sess := GetSession(r.Context())
14+
theme, ok := sess.Values[session.ThemeKey].(string)
15+
if !ok {
16+
theme = ""
17+
}
18+
19+
ctx := context.WithValue(r.Context(), themeContextKey, theme)
20+
next.ServeHTTP(w, r.WithContext(ctx))
21+
})
22+
}
23+
}
24+
25+
func GetTheme(ctx context.Context) string {
26+
theme, ok := ctx.Value(themeContextKey).(string)
27+
if !ok {
28+
return ""
29+
}
30+
return theme
31+
}

app/session/session.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import (
77
)
88

99
const (
10-
CookieName = "simplysharednotes_session"
11-
IdentityKey = "identity"
12-
RedirectKey = "redirect_url"
10+
CookieName = "simplysharednotes_session"
11+
ThemeKey = "theme"
12+
IdentityKey = "identity"
13+
RedirectKey = "redirect_url"
1314
)
1415

1516
func InitStore(secret string, isDev bool) *sessions.CookieStore {

app/views/layouts/base.layout.templ

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import "github.com/nicolashery/simply-shared-notes/app/rctx"
44

55
templ Base(title string, assets []string) {
66
{{ vite := rctx.GetVite(ctx) }}
7+
{{ theme := rctx.GetTheme(ctx) }}
78
<!DOCTYPE html>
8-
<html lang="en">
9+
<html lang="en" if theme != "" { data-theme={ theme } }>
910
<head>
1011
<meta charset="utf-8"/>
1112
<meta name="viewport" content="width=device-width, initial-scale=1"/>

app/views/layouts/base.layout_templ.go

Lines changed: 32 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/views/layouts/space.layout.templ

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ templ Space(assets []string) {
3232
}
3333

3434
templ navBar(route string, space *db.Space, stats *db.GetSpaceStatsRow, access *acc.Access, identity *identity.Identity) {
35-
<div class="navbar bg-base-100 px-4 border-b-2 border-base-300 mb-2">
35+
<div class="navbar bg-base-100 px-4 border-b-2 border-base-300 mb-2 dark:mb-6 theme-dark:mb-6">
3636
<div class="navbar-start">
3737
<div>
3838
@navMenu(route, space, stats, access)
@@ -310,6 +310,28 @@ templ userMenu(access *acc.Access, identity *identity.Identity) {
310310
English (EN)
311311
</a>
312312
</li>
313+
<li>
314+
<a
315+
href={ templ.URL("/theme") }
316+
class="flex"
317+
>
318+
<svg
319+
class="size-5"
320+
xmlns="http://www.w3.org/2000/svg"
321+
viewBox="0 0 24 24"
322+
fill="none"
323+
stroke="currentColor"
324+
stroke-width="2"
325+
stroke-linecap="round"
326+
stroke-linejoin="round"
327+
class="lucide lucide-palette-icon lucide-palette"
328+
>
329+
<path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z"></path><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"></circle><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"></circle><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"></circle>
330+
<circle cx="8.5" cy="7.5" r=".5" fill="currentColor"></circle>
331+
</svg>
332+
Change theme
333+
</a>
334+
</li>
313335
</ul>
314336
if !access.IsView() {
315337
<form method="POST" action={ templ.URL(fmt.Sprintf("/s/%s/identity/delete", access.Token)) }>

app/views/layouts/space.layout_templ.go

Lines changed: 22 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package pages
2+
3+
import (
4+
"github.com/nicolashery/simply-shared-notes/app/rctx"
5+
"github.com/nicolashery/simply-shared-notes/app/views/layouts"
6+
)
7+
8+
templ ThemeSelect() {
9+
{{ theme := rctx.GetTheme(ctx) }}
10+
@layouts.Landing("Change theme") {
11+
<div class="max-w-md mx-auto py-4">
12+
<h1 class="text-2xl font-bold mb-1.5">Change theme</h1>
13+
<p class="mb-4 text-sm opacity-60">
14+
Choose your preferred color theme:
15+
</p>
16+
<form method="POST" action="/theme" class="flex flex-col gap-3">
17+
<label class="flex items-center cursor-pointer gap-2">
18+
<input
19+
type="radio"
20+
name="theme"
21+
value=""
22+
class="radio radio-sm"
23+
checked?={ theme == "" }
24+
/>
25+
<span>System</span>
26+
<span class="text-sm opacity-60">(follows your device settings)</span>
27+
</label>
28+
<label class="flex items-center cursor-pointer gap-2">
29+
<input
30+
type="radio"
31+
name="theme"
32+
value="light"
33+
class="radio radio-sm"
34+
checked?={ theme == "light" }
35+
/>
36+
<span>Light</span>
37+
</label>
38+
<label class="flex items-center cursor-pointer gap-2">
39+
<input
40+
type="radio"
41+
name="theme"
42+
value="dark"
43+
class="radio radio-sm"
44+
checked?={ theme == "dark" }
45+
/>
46+
<span>Dark</span>
47+
</label>
48+
<button type="submit" class="btn btn-primary mt-4">Save</button>
49+
<p class="text-sm opacity-60">Your theme preference will be saved in a secure session cookie.</p>
50+
</form>
51+
</div>
52+
}
53+
}

0 commit comments

Comments
 (0)