Apply the theme: tokens, Oswald, song cards, toasts

The theme handoff encoded as CSS custom properties rather than a Tailwind
config, since there is no Tailwind here. Palette, spacing, radii, shadows and
motion follow it as written; docs/theme.md lists what differs and why.

- Oswald vendored as a 21KB variable woff2, latin subset, no CDN. Its phantom
  weight 900 resolved to 700 — loading a weight you don't have is what made
  the brand render differently per platform
- color-scheme: dark makes the native audio element fit the palette, which
  was the handoff's complaint about it
- Songs are text cards rather than artwork tiles, because there is no
  artwork. The unreviewed state keeps its red-brown border and gains a badge,
  so it is never carried by colour alone
- Nav is the three-column grid; the mobile menu is <details>, no JS
- Flash messages became bottom-right toasts

Favicon carried over from the Nuxt project.
This commit is contained in:
Esa Kataja
2026-07-31 22:16:29 +03:00
parent 91e136055c
commit f33f4fa4d6
16 changed files with 724 additions and 232 deletions
+8 -8
View File
@@ -160,7 +160,7 @@ type authForm struct {
}
func (a *app) loginPage(w http.ResponseWriter, r *http.Request) {
a.render(w, r, http.StatusOK, "login.html", page{Title: "Kirjaudu", Data: authForm{}})
a.render(w, r, http.StatusOK, "login.html", page{Title: "Kirjaudu", Narrow: true, Data: authForm{}})
}
func (a *app) login(w http.ResponseWriter, r *http.Request) {
@@ -169,7 +169,7 @@ func (a *app) login(w http.ResponseWriter, r *http.Request) {
if a.logins.locked(email) {
form.Errors["form"] = "Liian monta yritystä. Yritä hetken kuluttua uudelleen."
a.render(w, r, http.StatusTooManyRequests, "login.html", page{Title: "Kirjaudu", Data: form})
a.render(w, r, http.StatusTooManyRequests, "login.html", page{Title: "Kirjaudu", Narrow: true, Data: form})
return
}
@@ -185,12 +185,12 @@ func (a *app) login(w http.ResponseWriter, r *http.Request) {
// One message for both cases: a distinct "no such account" tells anyone who asks which
// addresses are members.
form.Errors["form"] = "Sähköposti tai salasana ei täsmää."
a.render(w, r, http.StatusUnauthorized, "login.html", page{Title: "Kirjaudu", Data: form})
a.render(w, r, http.StatusUnauthorized, "login.html", page{Title: "Kirjaudu", Narrow: true, Data: form})
return
}
if banned {
form.Errors["form"] = "Tunnus on estetty."
a.render(w, r, http.StatusForbidden, "login.html", page{Title: "Kirjaudu", Data: form})
a.render(w, r, http.StatusForbidden, "login.html", page{Title: "Kirjaudu", Narrow: true, Data: form})
return
}
@@ -219,7 +219,7 @@ func (a *app) logout(w http.ResponseWriter, r *http.Request) {
func (a *app) registerPage(w http.ResponseWriter, r *http.Request) {
a.render(w, r, http.StatusOK, "register.html",
page{Title: "Liity", Data: authForm{Code: r.URL.Query().Get("code")}})
page{Title: "Liity", Narrow: true, Data: authForm{Code: r.URL.Query().Get("code")}})
}
// register spends the invite only when the account is actually created: both statements are in one
@@ -248,7 +248,7 @@ func (a *app) register(w http.ResponseWriter, r *http.Request) {
form.Errors["code"] = "Kutsukoodi on pakollinen."
}
if len(form.Errors) > 0 {
a.render(w, r, http.StatusUnprocessableEntity, "register.html", page{Title: "Liity", Data: form})
a.render(w, r, http.StatusUnprocessableEntity, "register.html", page{Title: "Liity", Narrow: true, Data: form})
return
}
@@ -273,7 +273,7 @@ func (a *app) register(w http.ResponseWriter, r *http.Request) {
form.Code).Scan(&inviteID)
if errors.Is(err, pgx.ErrNoRows) {
form.Errors["code"] = "Kutsukoodi ei kelpaa."
a.render(w, r, http.StatusUnprocessableEntity, "register.html", page{Title: "Liity", Data: form})
a.render(w, r, http.StatusUnprocessableEntity, "register.html", page{Title: "Liity", Narrow: true, Data: form})
return
} else if err != nil {
slog.Error("burn invite", "ctx", "invites", "error", err)
@@ -288,7 +288,7 @@ func (a *app) register(w http.ResponseWriter, r *http.Request) {
if isUnique(err) {
// Rolls back, so the invite is still valid.
form.Errors["email"] = "Sähköpostiosoite on jo käytössä."
a.render(w, r, http.StatusUnprocessableEntity, "register.html", page{Title: "Liity", Data: form})
a.render(w, r, http.StatusUnprocessableEntity, "register.html", page{Title: "Liity", Narrow: true, Data: form})
return
} else if err != nil {
slog.Error("create user", "ctx", "auth", "error", err)