Stage 10: admin console & dictionary ops (complaint review, hot-reload, broadcasts) (#11)
This commit was merged in pull request #11.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user