// 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) } }