-
Welcome
-
-
We're sending you this email because it's the first time we've gotten this email address ({{.C.Destination}}).
-
-
If you'd rather not receive emails from us you can reply with "Unsubscribe" in the subject or body of the email. You can also use the "Unsubscribe" feature of your mail client, if it supports list unsubscribes.
-
-
If instead you'd like to confirm that you're willing to receive emails at this address, you can do so by clicking below:
+
+
+
+
Welcome
+
+
+
+
+ {{ if not .IsBrowser }}
+
+ {{ end }}
-
-
I want emails from Report Mosquitoes Online
+
+
+
+
+
Welcome
+
+
+ We're sending you this email because it's the first time we've gotten
+ this email address ({{ .C.Destination }}).
+
+
+
+ If you'd rather not receive emails from us you can reply with
+ "Unsubscribe" in the subject or body of the email. You can also use
+ the "Unsubscribe" feature of your mail client, if it supports list
+ unsubscribes.
+
+
+
+ If instead you'd like to confirm that you're willing to receive emails
+ at this address, you can do so by clicking below:
+
+
+
+
+
+
-
-
-
-
+
diff --git a/html/embed.go b/html/embed.go
new file mode 100644
index 00000000..4faac68b
--- /dev/null
+++ b/html/embed.go
@@ -0,0 +1,106 @@
+package html
+
+import (
+ "bytes"
+ "embed"
+ //"errors"
+ "fmt"
+ "html/template"
+ //"io"
+ "io/fs"
+ //"math"
+ "net/http"
+ //"path"
+ //"strconv"
+ "strings"
+ //"time"
+
+ //"github.com/Gleipnir-Technology/nidus-sync/config"
+ //"github.com/aarondl/opt/null"
+ //"github.com/google/uuid"
+ "github.com/rs/zerolog/log"
+)
+
+//go:embed template/*
+var embeddedFiles embed.FS
+
+type templateSystemEmbed struct {
+ allTemplates *template.Template
+ nameToTemplate map[string]*template.Template
+ sourceFS fs.FS
+}
+
+func (ts templateSystemEmbed) loadAll() error {
+ ts.nameToTemplate = make(map[string]*template.Template, 0)
+ err := fs.WalkDir(ts.sourceFS, "template", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if d.IsDir() {
+ return nil
+ }
+ short_path := removeLeadingDir(path)
+
+ new_t, err := loadTemplateEmbedded(ts.sourceFS, short_path)
+ if err != nil {
+ return fmt.Errorf("Failed to add load template '%s': %w", short_path, err)
+ }
+ _, err = ts.allTemplates.AddParseTree(new_t.Name(), new_t.Tree)
+ if err != nil {
+ return fmt.Errorf("Failed to add parsed template '%s': %w", path, err)
+ }
+ ts.nameToTemplate[short_path] = new_t
+ log.Debug().Str("path", path).Str("short_path", short_path).Msg("Loaded template")
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("failed to load embeded templates: %w", err)
+ }
+ return nil
+}
+func (ts templateSystemEmbed) renderOrError(w http.ResponseWriter, template_name string, context interface{}) {
+ buf := &bytes.Buffer{}
+ t, err := loadTemplateEmbedded(ts.sourceFS, template_name)
+ if err != nil {
+ log.Error().Err(err).Str("template_name", template_name).Msg("Failed to load embedded template")
+ RespondError(w, "Failed to load template", err, http.StatusInternalServerError)
+ return
+ }
+ for name, templ := range ts.nameToTemplate {
+ _, err := t.AddParseTree(name, templ.Tree)
+ if err != nil {
+ log.Error().Err(err).Str("name", name).Msg("Failed to add parse tree")
+ RespondError(w, "Failed to add template", err, http.StatusInternalServerError)
+ return
+ }
+ }
+ err = t.ExecuteTemplate(buf, template_name, context)
+ if err != nil {
+ log.Error().Err(err).Str("template_name", template_name).Msg("Failed to render embedded template")
+ RespondError(w, "Failed to render template", err, http.StatusInternalServerError)
+ return
+ }
+ buf.WriteTo(w)
+}
+func loadTemplateEmbedded(sourceFS fs.FS, path string) (*template.Template, error) {
+ content, err := fs.ReadFile(sourceFS, "template/"+path)
+ if err != nil {
+ return nil, fmt.Errorf("error reading template template/%s: %w", path, err)
+ }
+
+ new_t := template.New(path)
+ addFuncMap(new_t)
+ _, err = new_t.Parse(string(content))
+ if err != nil {
+ return nil, fmt.Errorf("error parsing '%s': %w", path, err)
+ }
+ return new_t, nil
+}
+func removeLeadingDir(path string) string {
+ parts := strings.SplitN(path, "/", 2)
+ if len(parts) == 2 {
+ return parts[1]
+ }
+ return path
+}
diff --git a/html/filesystem.go b/html/filesystem.go
new file mode 100644
index 00000000..0862c0df
--- /dev/null
+++ b/html/filesystem.go
@@ -0,0 +1,244 @@
+package html
+
+import (
+ "bytes"
+ //"embed"
+ //"errors"
+ "fmt"
+ "html/template"
+ //"io"
+ "io/fs"
+ //"math"
+ "net/http"
+ "os"
+ //"path"
+ //"strconv"
+ //"strings"
+ //"time"
+
+ //"github.com/Gleipnir-Technology/nidus-sync/config"
+ //"github.com/aarondl/opt/null"
+ //"github.com/google/uuid"
+ "github.com/rs/zerolog/log"
+)
+
+// The filesystem being used
+var templates templateSystem
+
+type templateSystem interface {
+ loadAll() error
+ renderOrError(http.ResponseWriter, string, interface{})
+}
+
+type templateSystemDisk struct {
+ sourceFS fs.FS
+}
+
+func LoadTemplates() error {
+ _, err := os.Stat("html/template")
+ if err == nil {
+ templates = templateSystemDisk{
+ sourceFS: os.DirFS("./html/template"),
+ }
+ } else {
+ templates = templateSystemEmbed{
+ allTemplates: template.New("all"),
+ sourceFS: embeddedFiles,
+ }
+ templates.loadAll()
+ }
+ return nil
+}
+
+func (ts templateSystemDisk) loadAll() error {
+ return nil
+}
+func (ts templateSystemDisk) renderOrError(w http.ResponseWriter, template_name string, context interface{}) {
+ t, err := ts.parseTemplate(template_name)
+ if err != nil {
+ log.Error().Err(err).Str("template_name", template_name).Msg("Failed to parse template")
+ RespondError(w, "Failed to parse template", err, http.StatusInternalServerError)
+ return
+ }
+ err = ts.addSupportingTemplates(t)
+ if err != nil {
+ log.Error().Err(err).Str("template_name", template_name).Msg("Failed to add supporting templates")
+ RespondError(w, "Failed to add supporting templates", err, http.StatusInternalServerError)
+ return
+ }
+ err = ts.addSVGTemplates(t)
+ if err != nil {
+ log.Error().Err(err).Str("template_name", template_name).Msg("Failed to add supporting templates")
+ RespondError(w, "Failed to add supporting templates", err, http.StatusInternalServerError)
+ return
+ }
+
+ buf := &bytes.Buffer{}
+ err = t.Execute(buf, context)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to render template")
+ RespondError(w, "Failed to render template", err, http.StatusInternalServerError)
+ return
+ }
+ buf.WriteTo(w)
+}
+func (ts templateSystemDisk) addSupportingTemplates(t *template.Template) error {
+ err := fs.WalkDir(ts.sourceFS, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if d.IsDir() {
+ return nil
+ }
+
+ content, err := fs.ReadFile(ts.sourceFS, path)
+ if err != nil {
+ return fmt.Errorf("error reading template %s: %w", path, err)
+ }
+
+ new_t := template.New(path)
+ addFuncMap(new_t)
+ _, err = new_t.Parse(string(content))
+ if err != nil {
+ return fmt.Errorf("error parsing '%s': %w", path, err)
+ }
+ _, err = t.AddParseTree(new_t.Name(), new_t.Tree)
+ if err != nil {
+ return fmt.Errorf("error adding parse tree '%s': %w", path, err)
+ }
+ log.Debug().Str("path", path).Msg("Read template")
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("error walking template directory: %w", err)
+ }
+ return nil
+}
+func (ts templateSystemDisk) addSVGTemplates(t *template.Template) error {
+ svg_fs, err := fs.Sub(ts.sourceFS, "svg")
+ if err != nil {
+ return fmt.Errorf("Failed to get svg subdir: %w", err)
+ }
+ svgs, err := fs.ReadDir(svg_fs, ".")
+ if err != nil {
+ log.Warn().Msg("Failed to read svg directory")
+ return nil
+ }
+ for _, svg := range svgs {
+ content, err := fs.ReadFile(svg_fs, svg.Name())
+ if err != nil {
+ return fmt.Errorf("Failed to read svg '%s' from embedded filesystem: %w", svg, err)
+ }
+ svg_name := svg.Name()
+ svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content))
+ svg_t, err := template.New(svg_name).Parse(svg_template)
+ if err != nil {
+ return fmt.Errorf("Failed to parse svg '%s' from embedded filesystem: %v", svg, err)
+ }
+ _, err = t.AddParseTree(svg_t.Name(), svg_t.Tree)
+ if err != nil {
+ return fmt.Errorf("Failed to add svg '%s' to embedded template: %v", svg, err)
+ }
+ log.Debug().Str("name", svg_name).Msg("add svg template")
+ }
+ return nil
+}
+func (ts templateSystemDisk) parseTemplate(filename string) (*template.Template, error) {
+ t := template.New(filename)
+ log.Debug().Str("filename", filename).Msg("parsing template")
+ addFuncMap(t)
+ content, err := fs.ReadFile(ts.sourceFS, filename)
+ if err != nil {
+ return nil, fmt.Errorf("error reading template %s: %w", filename, err)
+ }
+ _, err = t.Parse(string(content))
+ if err != nil {
+ return nil, fmt.Errorf("error parsing '%s': %w", filename, err)
+ }
+ return t, nil
+}
+func addSVGTemplates(fsys fs.FS, templ *template.Template) error {
+ svgs, err := fs.ReadDir(fsys, ".")
+ if err != nil {
+ log.Warn().Msg("Failed to read svg directory")
+ return nil
+ }
+ for _, svg := range svgs {
+ content, err := fs.ReadFile(fsys, svg.Name())
+ if err != nil {
+ return fmt.Errorf("Failed to read svg '%s' from embedded filesystem: %w", svg, err)
+ }
+ svg_name := svg.Name()
+ svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content))
+ svg_t, err := template.New(svg_name).Parse(svg_template)
+ if err != nil {
+ return fmt.Errorf("Failed to parse svg '%s' from embedded filesystem: %v", svg, err)
+ }
+ _, err = templ.AddParseTree(svg_t.Name(), svg_t.Tree)
+ if err != nil {
+ return fmt.Errorf("Failed to add svg '%s' to embedded template: %v", svg, err)
+ }
+ //log.Debug().Str("name", svg_name).Msg("add svg template")
+ }
+ return nil
+}
+
+/*
+func executeTemplate(w io.Writer, data any) error {
+ if bt.template == nil {
+ name := path.Base(bt.files[0])
+ templ, err := parseFromDisk(bt.subdir, bt.files)
+ if err != nil {
+ return fmt.Errorf("Failed to parse template file: %w", err)
+ }
+ if templ == nil {
+ w.Write([]byte("Failed to read from disk: "))
+ return errors.New("Template parsing failed")
+ }
+ //log.Debug().Str("name", templ.Name()).Msg("Parsed template")
+ return templ.ExecuteTemplate(w, name, data)
+ } else {
+ name := path.Base(bt.files[0])
+ return bt.template.ExecuteTemplate(w, name, data)
+ }
+}
+func parseEmbedded(embeddedFiles embed.FS, subdir string, files []string) *template.Template {
+ funcMap := makeFuncMap()
+ // Remap the file names to embedded paths
+ embeddedFilePaths := make([]string, 0)
+ for _, f := range files {
+ embeddedFilePaths = append(embeddedFilePaths, strings.TrimPrefix(f, subdir))
+ }
+ name := path.Base(embeddedFilePaths[0])
+ log.Debug().Str("name", name).Strs("paths", embeddedFilePaths).Msg("Parsing embedded template")
+ t, err := template.New(name).Funcs(funcMap).ParseFS(embeddedFiles, embeddedFilePaths...)
+ if err != nil {
+ panic(fmt.Sprintf("Failed to parse embedded template %s: %v", name, err))
+ }
+ svg_fs, err := fs.Sub(embeddedFiles, "template/svg")
+ if err != nil {
+ panic(fmt.Sprintf("Failed to read static/svg: %v", err))
+ }
+ err = addSVGTemplates(svg_fs, t)
+ if err != nil {
+ panic(fmt.Sprintf("Failed to add SVG templates: %v", err))
+ }
+ return t
+}
+func parseFromDisk(subdir string, files []string) (*template.Template, error) {
+ funcMap := makeFuncMap()
+ name := path.Base(files[0])
+ //log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk")
+ templ, err := template.New(name).Funcs(funcMap).ParseFiles(files...)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to parse %s: %w", files, err)
+ }
+ fsys := os.DirFS(subdir + "/template/svg")
+ err = addSVGTemplates(fsys, templ)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to add SVGs from disk: %w", err)
+ }
+ return templ, nil
+}
+*/
diff --git a/html/func.go b/html/func.go
new file mode 100644
index 00000000..c857c96a
--- /dev/null
+++ b/html/func.go
@@ -0,0 +1,211 @@
+package html
+
+import (
+ //"bytes"
+ "fmt"
+ "html/template"
+ //"io/fs"
+ "math"
+ //"net/http"
+ //"os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/aarondl/opt/null"
+ "github.com/google/uuid"
+ "github.com/rs/zerolog/log"
+)
+
+func addFuncMap(t *template.Template) {
+ funcMap := template.FuncMap{
+ "bigNumber": bigNumber,
+ "html": unescapeHTML,
+ "json": unescapeJS,
+ "GISStatement": gisStatement,
+ "latLngDisplay": latLngDisplay,
+ "publicReportID": publicReportID,
+ "timeAsRelativeDate": timeAsRelativeDate,
+ "timeDelta": timeDelta,
+ "timeElapsed": timeElapsed,
+ "timeInterval": timeInterval,
+ "timeSince": timeSince,
+ "timeSincePtr": timeSincePtr,
+ "uuidShort": uuidShort,
+ }
+ t.Funcs(funcMap)
+}
+
+func bigNumber(n int) string {
+ // Convert the number to a string
+ numStr := strconv.FormatInt(int64(n), 10)
+
+ // Add commas every three digits from the right
+ var result strings.Builder
+ for i, char := range numStr {
+ if i > 0 && (len(numStr)-i)%3 == 0 {
+ result.WriteByte(',')
+ }
+ result.WriteRune(char)
+ }
+
+ return result.String()
+}
+
+func publicReportID(s string) string {
+ if len(s) != 12 {
+ return s
+ }
+ return s[0:4] + "-" + s[4:8] + "-" + s[8:12]
+}
+func timeAsRelativeDate(d time.Time) string {
+ return d.Format("01-02")
+}
+
+// FormatTimeDuration returns a human-readable string representing a time.Duration
+// as "X units early" or "X units late"
+func timeDelta(d time.Duration) string {
+ suffix := "late"
+ if d < 0 {
+ suffix = "early"
+ d = -d // Make duration positive for calculations
+ }
+
+ const (
+ day = 24 * time.Hour
+ week = 7 * day
+ )
+
+ log.Info().Int64("delta", int64(d)).Str("suffix", suffix).Msg("Time delta")
+ switch {
+ case d >= week:
+ weeks := d / week
+ if weeks == 1 {
+ return "1 week " + suffix
+ }
+ return fmt.Sprintf("%d weeks %s", weeks, suffix)
+
+ case d >= day:
+ days := d / day
+ if days == 1 {
+ return "1 day " + suffix
+ }
+ return fmt.Sprintf("%d days %s", days, suffix)
+
+ case d >= time.Hour:
+ hours := d / time.Hour
+ if hours == 1 {
+ return "1 hour " + suffix
+ }
+ return fmt.Sprintf("%d hours %s", hours, suffix)
+
+ case d >= time.Minute:
+ minutes := d / time.Minute
+ if minutes == 1 {
+ return "1 minute " + suffix
+ }
+ return fmt.Sprintf("%d minutes %s", minutes, suffix)
+
+ default:
+ seconds := d / time.Second
+ if seconds == 1 {
+ return "1 second " + suffix
+ }
+ return fmt.Sprintf("%d seconds %s", seconds, suffix)
+ }
+}
+
+func timeElapsed(seconds null.Val[float32]) string {
+ if !seconds.IsValue() {
+ return "none"
+ }
+ s := int(seconds.MustGet())
+ hours := s / 3600
+ remainder := s - (hours * 3600)
+ minutes := remainder / 60
+ remainder = remainder - (minutes * 60)
+ if hours > 0 {
+ return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, remainder)
+ } else if minutes > 0 {
+ return fmt.Sprintf("%02d:%02d", minutes, remainder)
+ } else {
+ return fmt.Sprintf("%d seconds", remainder)
+ }
+}
+
+func timeInterval(d time.Duration) string {
+ seconds := d.Seconds()
+
+ // Less than 120 seconds -> show in seconds
+ if seconds < 120 {
+ return fmt.Sprintf("every %d seconds", int(math.Round(seconds)))
+ }
+
+ minutes := d.Minutes()
+ // Less than 120 minutes -> show in minutes
+ if minutes < 120 {
+ return fmt.Sprintf("every %d minutes", int(math.Round(minutes)))
+ }
+
+ hours := d.Hours()
+ // Less than 48 hours -> show in hours
+ if hours < 48 {
+ return fmt.Sprintf("every %d hours", int(math.Round(hours)))
+ }
+
+ days := hours / 24
+ // Less than 14 days -> show in days
+ if days < 14 {
+ return fmt.Sprintf("every %d days", int(math.Round(days)))
+ }
+
+ weeks := days / 7
+ // Less than 8 weeks -> show in weeks
+ if weeks < 8 {
+ return fmt.Sprintf("every %d weeks", int(math.Round(weeks)))
+ }
+
+ months := days / 30
+ // Less than 24 months -> show in months
+ if months < 24 {
+ return fmt.Sprintf("every %d months", int(math.Round(months)))
+ }
+
+ years := days / 365
+ return fmt.Sprintf("every %d years", int(math.Round(years)))
+}
+func timeSincePtr(t *time.Time) string {
+ if t == nil {
+ return "never"
+ }
+ return timeSince(*t)
+}
+func timeSince(t time.Time) string {
+ now := time.Now()
+ diff := now.Sub(t)
+
+ hours := diff.Hours()
+ if hours < 1 {
+ minutes := diff.Minutes()
+ return fmt.Sprintf("%d minutes ago", int(minutes))
+ } else if hours < 24 {
+ return fmt.Sprintf("%d hours ago", int(hours))
+ } else {
+ days := hours / 24
+ return fmt.Sprintf("%d days ago", int(days))
+ }
+}
+func unescapeHTML(s string) template.HTML {
+ return template.HTML(s)
+}
+func unescapeJS(s string) template.JS {
+ return template.JS(s)
+}
+func uuidShort(uuid uuid.UUID) string {
+ s := uuid.String()
+ if len(s) < 7 {
+ return s // Return as is if too short
+ }
+
+ return s[:3] + "..." + s[len(s)-4:]
+}
diff --git a/html/html.go b/html/html.go
index 884aadb3..209fc7ae 100644
--- a/html/html.go
+++ b/html/html.go
@@ -1,351 +1,9 @@
package html
import (
- "bytes"
- "embed"
- "errors"
- "fmt"
- "html/template"
- "io"
- "io/fs"
- "math"
"net/http"
- "os"
- "path"
- "strconv"
- "strings"
- "time"
-
- "github.com/Gleipnir-Technology/nidus-sync/config"
- "github.com/aarondl/opt/null"
- "github.com/google/uuid"
- "github.com/rs/zerolog/log"
)
-var TemplatesByFilename = make(map[string]BuiltTemplate, 0)
-
-type BuiltTemplate struct {
- files []string
- subdir string
- // Nil if we are going to read templates off disk every time we render
- // because we are in development mode.
- template *template.Template
-}
-
-func (bt *BuiltTemplate) executeTemplate(w io.Writer, data any) error {
- if bt.template == nil {
- name := path.Base(bt.files[0])
- templ, err := parseFromDisk(bt.subdir, bt.files)
- if err != nil {
- return fmt.Errorf("Failed to parse template file: %w", err)
- }
- if templ == nil {
- w.Write([]byte("Failed to read from disk: "))
- return errors.New("Template parsing failed")
- }
- //log.Debug().Str("name", templ.Name()).Msg("Parsed template")
- return templ.ExecuteTemplate(w, name, data)
- } else {
- name := path.Base(bt.files[0])
- return bt.template.ExecuteTemplate(w, name, data)
- }
-}
-
-func NewBuiltTemplate(embeddedFiles embed.FS, subdir string, files ...string) *BuiltTemplate {
- files_on_disk := true
- for _, f := range files {
- _, err := os.Stat(f)
- if err != nil {
- files_on_disk = false
- if !config.IsProductionEnvironment() {
- log.Warn().Str("file", f).Msg("template file is not on disk")
- }
- break
- }
- }
- var result BuiltTemplate
- if files_on_disk {
- result = BuiltTemplate{
- files: files,
- subdir: subdir,
- template: nil,
- }
- } else {
- result = BuiltTemplate{
- files: files,
- subdir: subdir,
- template: parseEmbedded(embeddedFiles, subdir, files),
- }
- }
- TemplatesByFilename[path.Base(files[0])] = result
- return &result
-}
-
-func RenderOrError(w http.ResponseWriter, template *BuiltTemplate, context interface{}) {
- buf := &bytes.Buffer{}
- err := template.executeTemplate(buf, context)
- if err != nil {
- log.Error().Err(err).Strs("files", template.files).Msg("Failed to render template")
- RespondError(w, "Failed to render template", err, http.StatusInternalServerError)
- return
- }
- buf.WriteTo(w)
-}
-
-func bigNumber(n int) string {
- // Convert the number to a string
- numStr := strconv.FormatInt(int64(n), 10)
-
- // Add commas every three digits from the right
- var result strings.Builder
- for i, char := range numStr {
- if i > 0 && (len(numStr)-i)%3 == 0 {
- result.WriteByte(',')
- }
- result.WriteRune(char)
- }
-
- return result.String()
-}
-
-func makeFuncMap() template.FuncMap {
- funcMap := template.FuncMap{
- "bigNumber": bigNumber,
- "html": unescapeHTML,
- "json": unescapeJS,
- "GISStatement": gisStatement,
- "latLngDisplay": latLngDisplay,
- "publicReportID": publicReportID,
- "timeAsRelativeDate": timeAsRelativeDate,
- "timeDelta": timeDelta,
- "timeElapsed": timeElapsed,
- "timeInterval": timeInterval,
- "timeSince": timeSince,
- "timeSincePtr": timeSincePtr,
- "uuidShort": uuidShort,
- }
- return funcMap
-}
-func addSVGTemplates(fsys fs.FS, templ *template.Template) error {
- svgs, err := fs.ReadDir(fsys, ".")
- if err != nil {
- log.Warn().Msg("Failed to read svg directory")
- return nil
- }
- for _, svg := range svgs {
- content, err := fs.ReadFile(fsys, svg.Name())
- if err != nil {
- return fmt.Errorf("Failed to read svg '%s' from embedded filesystem: %w", svg, err)
- }
- svg_name := svg.Name()
- svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content))
- svg_t, err := template.New(svg_name).Parse(svg_template)
- if err != nil {
- return fmt.Errorf("Failed to parse svg '%s' from embedded filesystem: %v", svg, err)
- }
- _, err = templ.AddParseTree(svg_t.Name(), svg_t.Tree)
- if err != nil {
- return fmt.Errorf("Failed to add svg '%s' to embedded template: %v", svg, err)
- }
- //log.Debug().Str("name", svg_name).Msg("add svg template")
- }
- return nil
-}
-func parseEmbedded(embeddedFiles embed.FS, subdir string, files []string) *template.Template {
- funcMap := makeFuncMap()
- // Remap the file names to embedded paths
- embeddedFilePaths := make([]string, 0)
- for _, f := range files {
- embeddedFilePaths = append(embeddedFilePaths, strings.TrimPrefix(f, subdir))
- }
- name := path.Base(embeddedFilePaths[0])
- log.Debug().Str("name", name).Strs("paths", embeddedFilePaths).Msg("Parsing embedded template")
- t, err := template.New(name).Funcs(funcMap).ParseFS(embeddedFiles, embeddedFilePaths...)
- if err != nil {
- panic(fmt.Sprintf("Failed to parse embedded template %s: %v", name, err))
- }
- svg_fs, err := fs.Sub(embeddedFiles, "template/svg")
- if err != nil {
- panic(fmt.Sprintf("Failed to read static/svg: %v", err))
- }
- err = addSVGTemplates(svg_fs, t)
- if err != nil {
- panic(fmt.Sprintf("Failed to add SVG templates: %v", err))
- }
- return t
-}
-
-func parseFromDisk(subdir string, files []string) (*template.Template, error) {
- funcMap := makeFuncMap()
- name := path.Base(files[0])
- //log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk")
- templ, err := template.New(name).Funcs(funcMap).ParseFiles(files...)
- if err != nil {
- return nil, fmt.Errorf("Failed to parse %s: %w", files, err)
- }
- fsys := os.DirFS(subdir + "/template/svg")
- err = addSVGTemplates(fsys, templ)
- if err != nil {
- return nil, fmt.Errorf("Failed to add SVGs from disk: %w", err)
- }
- return templ, nil
-}
-
-func publicReportID(s string) string {
- if len(s) != 12 {
- return s
- }
- return s[0:4] + "-" + s[4:8] + "-" + s[8:12]
-}
-
-func timeAsRelativeDate(d time.Time) string {
- return d.Format("01-02")
-}
-
-// FormatTimeDuration returns a human-readable string representing a time.Duration
-// as "X units early" or "X units late"
-func timeDelta(d time.Duration) string {
- suffix := "late"
- if d < 0 {
- suffix = "early"
- d = -d // Make duration positive for calculations
- }
-
- const (
- day = 24 * time.Hour
- week = 7 * day
- )
-
- log.Info().Int64("delta", int64(d)).Str("suffix", suffix).Msg("Time delta")
- switch {
- case d >= week:
- weeks := d / week
- if weeks == 1 {
- return "1 week " + suffix
- }
- return fmt.Sprintf("%d weeks %s", weeks, suffix)
-
- case d >= day:
- days := d / day
- if days == 1 {
- return "1 day " + suffix
- }
- return fmt.Sprintf("%d days %s", days, suffix)
-
- case d >= time.Hour:
- hours := d / time.Hour
- if hours == 1 {
- return "1 hour " + suffix
- }
- return fmt.Sprintf("%d hours %s", hours, suffix)
-
- case d >= time.Minute:
- minutes := d / time.Minute
- if minutes == 1 {
- return "1 minute " + suffix
- }
- return fmt.Sprintf("%d minutes %s", minutes, suffix)
-
- default:
- seconds := d / time.Second
- if seconds == 1 {
- return "1 second " + suffix
- }
- return fmt.Sprintf("%d seconds %s", seconds, suffix)
- }
-}
-
-func timeElapsed(seconds null.Val[float32]) string {
- if !seconds.IsValue() {
- return "none"
- }
- s := int(seconds.MustGet())
- hours := s / 3600
- remainder := s - (hours * 3600)
- minutes := remainder / 60
- remainder = remainder - (minutes * 60)
- if hours > 0 {
- return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, remainder)
- } else if minutes > 0 {
- return fmt.Sprintf("%02d:%02d", minutes, remainder)
- } else {
- return fmt.Sprintf("%d seconds", remainder)
- }
-}
-
-func timeInterval(d time.Duration) string {
- seconds := d.Seconds()
-
- // Less than 120 seconds -> show in seconds
- if seconds < 120 {
- return fmt.Sprintf("every %d seconds", int(math.Round(seconds)))
- }
-
- minutes := d.Minutes()
- // Less than 120 minutes -> show in minutes
- if minutes < 120 {
- return fmt.Sprintf("every %d minutes", int(math.Round(minutes)))
- }
-
- hours := d.Hours()
- // Less than 48 hours -> show in hours
- if hours < 48 {
- return fmt.Sprintf("every %d hours", int(math.Round(hours)))
- }
-
- days := hours / 24
- // Less than 14 days -> show in days
- if days < 14 {
- return fmt.Sprintf("every %d days", int(math.Round(days)))
- }
-
- weeks := days / 7
- // Less than 8 weeks -> show in weeks
- if weeks < 8 {
- return fmt.Sprintf("every %d weeks", int(math.Round(weeks)))
- }
-
- months := days / 30
- // Less than 24 months -> show in months
- if months < 24 {
- return fmt.Sprintf("every %d months", int(math.Round(months)))
- }
-
- years := days / 365
- return fmt.Sprintf("every %d years", int(math.Round(years)))
-}
-func timeSincePtr(t *time.Time) string {
- if t == nil {
- return "never"
- }
- return timeSince(*t)
-}
-func timeSince(t time.Time) string {
- now := time.Now()
- diff := now.Sub(t)
-
- hours := diff.Hours()
- if hours < 1 {
- minutes := diff.Minutes()
- return fmt.Sprintf("%d minutes ago", int(minutes))
- } else if hours < 24 {
- return fmt.Sprintf("%d hours ago", int(hours))
- } else {
- days := hours / 24
- return fmt.Sprintf("%d days ago", int(days))
- }
-}
-func unescapeHTML(s string) template.HTML {
- return template.HTML(s)
-}
-func unescapeJS(s string) template.JS {
- return template.JS(s)
-}
-func uuidShort(uuid uuid.UUID) string {
- s := uuid.String()
- if len(s) < 7 {
- return s // Return as is if too short
- }
-
- return s[:3] + "..." + s[len(s)-4:]
+func RenderOrError(w http.ResponseWriter, template_name string, content interface{}) {
+ templates.renderOrError(w, template_name, content)
}
diff --git a/rmo/template/base.html b/html/template/rmo/base.html
similarity index 97%
rename from rmo/template/base.html
rename to html/template/rmo/base.html
index d96bdd81..e6420289 100644
--- a/rmo/template/base.html
+++ b/html/template/rmo/base.html
@@ -42,7 +42,7 @@
{{ template "content" . }}
- {{ template "footer" . }}
+ {{ template "rmo/component/footer.html" . }}