frontend-foundation: GOAT front-end Phase 1 — auth, tag management, public tag page (22/22 spec scenarios pass)
This commit is contained in:
103
frontend/internal/handlers/handlers.go
Normal file
103
frontend/internal/handlers/handlers.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Package handlers wires templates, auth, and database queries for the
|
||||
// WhereWoof GOAT front-end.
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"wherewoof/frontend/internal/auth"
|
||||
"wherewoof/frontend/internal/db"
|
||||
)
|
||||
|
||||
// Templates maps a page key to its parsed template set (base + page + partials).
|
||||
// Each page is parsed as its own set so the shared "content" block name
|
||||
// doesn't collide across pages.
|
||||
type Templates map[string]*template.Template
|
||||
|
||||
// App holds dependencies shared by all handlers.
|
||||
type App struct {
|
||||
Queries *db.Queries
|
||||
Tpl Templates
|
||||
}
|
||||
|
||||
// New returns an App with the given query layer and template sets.
|
||||
func New(queries *db.Queries, tpl Templates) *App {
|
||||
return &App{Queries: queries, Tpl: tpl}
|
||||
}
|
||||
|
||||
// PageData is the root data passed to the base layout.
|
||||
type PageData struct {
|
||||
CurrentUser *db.User
|
||||
Title string
|
||||
Error string
|
||||
Data any
|
||||
}
|
||||
|
||||
func titleCase(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
return strings.ToUpper(s[:1]) + s[1:]
|
||||
}
|
||||
|
||||
// LoadTemplates parses every page's template set from templates/.
|
||||
func LoadTemplates() (Templates, error) {
|
||||
const dir = "templates"
|
||||
base := dir + "/base.html"
|
||||
pages := map[string][]string{
|
||||
"index": {dir + "/index.html"},
|
||||
"register": {dir + "/register.html"},
|
||||
"login": {dir + "/login.html"},
|
||||
"account": {dir + "/account.html", dir + "/account-panel.html", dir + "/tag-list.html"},
|
||||
"edit": {dir + "/tag-edit.html"},
|
||||
"public": {dir + "/tag-public.html"},
|
||||
"notfound": {dir + "/not-found.html"},
|
||||
}
|
||||
funcs := template.FuncMap{"title": titleCase}
|
||||
tpl := make(Templates, len(pages))
|
||||
for name, files := range pages {
|
||||
paths := append([]string{base}, files...)
|
||||
t, err := template.New(name).Funcs(funcs).ParseFiles(paths...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tpl[name] = t
|
||||
}
|
||||
return tpl, nil
|
||||
}
|
||||
|
||||
// render executes the page's base layout with a PageData populated from the
|
||||
// authenticated user (if any).
|
||||
func (a *App) render(w http.ResponseWriter, r *http.Request, page, title string, data any, errMsg string) {
|
||||
pd := PageData{Title: title, Data: data, Error: errMsg}
|
||||
if uid, ok := auth.GetUserID(r); ok {
|
||||
if u, err := a.Queries.GetUserByID(r.Context(), uid); err == nil {
|
||||
pd.CurrentUser = &u
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := a.Tpl[page].ExecuteTemplate(w, "base", pd); err != nil {
|
||||
http.Error(w, "template error: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// renderPartial executes a named partial (e.g. "account-panel") for HTMX swaps.
|
||||
func (a *App) renderPartial(w http.ResponseWriter, page, partial string, data any) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := a.Tpl[page].ExecuteTemplate(w, partial, data); err != nil {
|
||||
http.Error(w, "template error: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAuth redirects unauthenticated requests to /login.
|
||||
func (a *App) RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := auth.GetUserID(r); !ok {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user