package templates
import (
"bytes"
"fmt"
"html/template"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gorilla/csrf"
"github.com/mattnite/forgejo-tickets/internal/auth"
"github.com/mattnite/forgejo-tickets/internal/models"
)
type Renderer struct {
templates map[string]*template.Template
}
type PageData struct {
User *models.User
CSRFToken string
Flash *Flash
Data interface{}
}
type Flash struct {
Type string // "success", "error", "info"
Message string
}
func NewRenderer() (*Renderer, error) {
r := &Renderer{
templates: make(map[string]*template.Template),
}
if err := r.parseTemplates(); err != nil {
return nil, err
}
return r, nil
}
func (r *Renderer) parseTemplates() error {
templateDir := "web/templates"
partials, err := filepath.Glob(filepath.Join(templateDir, "partials", "*.html"))
if err != nil {
return fmt.Errorf("glob partials: %w", err)
}
err = filepath.WalkDir(filepath.Join(templateDir, "pages"), func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || !strings.HasSuffix(path, ".html") {
return nil
}
layout := filepath.Join(templateDir, "layouts", "base.html")
if strings.Contains(path, "admin") {
layout = filepath.Join(templateDir, "layouts", "admin.html")
}
if _, err := os.Stat(layout); os.IsNotExist(err) {
return nil
}
files := []string{layout}
files = append(files, partials...)
files = append(files, path)
name := strings.TrimPrefix(path, filepath.Join(templateDir, "pages")+"/")
name = strings.TrimSuffix(name, ".html")
tmpl, err := template.New(filepath.Base(layout)).Funcs(templateFuncs()).ParseFiles(files...)
if err != nil {
return fmt.Errorf("parse template %s: %w", name, err)
}
r.templates[name] = tmpl
return nil
})
if err != nil {
return err
}
return nil
}
func (r *Renderer) Render(w http.ResponseWriter, req *http.Request, name string, data interface{}) {
tmpl, ok := r.templates[name]
if !ok {
http.Error(w, fmt.Sprintf("template %q not found", name), http.StatusInternalServerError)
return
}
pd := PageData{
User: auth.CurrentUserFromRequest(req),
CSRFToken: csrf.Token(req),
Data: data,
}
if msg := req.URL.Query().Get("flash"); msg != "" {
flashType := req.URL.Query().Get("flash_type")
if flashType == "" {
flashType = "info"
}
pd.Flash = &Flash{Type: flashType, Message: msg}
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, pd); err != nil {
http.Error(w, fmt.Sprintf("render template: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
buf.WriteTo(w)
}
func (r *Renderer) RenderError(w http.ResponseWriter, req *http.Request, status int, message string) {
w.WriteHeader(status)
if tmpl, ok := r.templates["error"]; ok {
pd := PageData{
User: auth.CurrentUserFromRequest(req),
CSRFToken: csrf.Token(req),
Data: map[string]interface{}{"Status": status, "Message": message},
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, pd); err == nil {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
buf.WriteTo(w)
return
}
}
http.Error(w, message, status)
}