102 lines
3.0 KiB
Go
102 lines
3.0 KiB
Go
package adminconsole
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/fs"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
//go:embed templates
|
|
var templatesFS embed.FS
|
|
|
|
//go:embed assets
|
|
var assetsFS embed.FS
|
|
|
|
// Renderer holds the parsed admin console templates. It composes one template set
|
|
// per content page, each combining the shared layout (the page chrome and the
|
|
// "layout" entry template) with that page's "content" block, so rendering a page
|
|
// is a single ExecuteTemplate call against "layout".
|
|
type Renderer struct {
|
|
pages map[string]*template.Template
|
|
}
|
|
|
|
// PageData is the view model passed to every admin console page. Title is the
|
|
// document title; ActiveNav marks the highlighted navigation entry; Data carries
|
|
// the page-specific payload (one of the *View types in views.go).
|
|
type PageData struct {
|
|
Title string
|
|
ActiveNav string
|
|
Data any
|
|
}
|
|
|
|
// NewRenderer parses the embedded layout and every content page under
|
|
// templates/pages. It fails when a template cannot be parsed.
|
|
func NewRenderer() (*Renderer, error) {
|
|
base, err := template.New("layout").ParseFS(templatesFS, "templates/layout.gohtml")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse admin console layout: %w", err)
|
|
}
|
|
|
|
pageFiles, err := fs.Glob(templatesFS, "templates/pages/*.gohtml")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("enumerate admin console pages: %w", err)
|
|
}
|
|
if len(pageFiles) == 0 {
|
|
return nil, fmt.Errorf("admin console: no page templates found under templates/pages")
|
|
}
|
|
|
|
pages := make(map[string]*template.Template, len(pageFiles))
|
|
for _, file := range pageFiles {
|
|
name := strings.TrimSuffix(path.Base(file), ".gohtml")
|
|
clone, err := base.Clone()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("clone admin console layout for %q: %w", name, err)
|
|
}
|
|
if _, err := clone.ParseFS(templatesFS, file); err != nil {
|
|
return nil, fmt.Errorf("parse admin console page %q: %w", name, err)
|
|
}
|
|
pages[name] = clone
|
|
}
|
|
|
|
return &Renderer{pages: pages}, nil
|
|
}
|
|
|
|
// MustNewRenderer is like NewRenderer but panics on error. The templates are
|
|
// embedded at build time, so a parse failure is a programmer error.
|
|
func MustNewRenderer() *Renderer {
|
|
renderer, err := NewRenderer()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return renderer
|
|
}
|
|
|
|
// Render writes the named page, wrapped in the shared layout, to w using data. It
|
|
// renders into an intermediate buffer first, so a mid-render failure never emits
|
|
// a partial document. It returns an error for an unknown page or a failed render.
|
|
func (r *Renderer) Render(w io.Writer, page string, data PageData) error {
|
|
tmpl, ok := r.pages[page]
|
|
if !ok {
|
|
return fmt.Errorf("admin console: unknown page %q", page)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.ExecuteTemplate(&buf, "layout", data); err != nil {
|
|
return fmt.Errorf("render admin console page %q: %w", page, err)
|
|
}
|
|
|
|
_, err := buf.WriteTo(w)
|
|
return err
|
|
}
|
|
|
|
// Assets returns the embedded static asset tree rooted at the assets directory,
|
|
// suitable for serving under /_gm/assets/.
|
|
func Assets() (fs.FS, error) {
|
|
return fs.Sub(assetsFS, "assets")
|
|
}
|