Track five things a day for seventy-five days
The workout, the protein, the calories, the water, and three lines of gratitude. One page per day, a card of seventy-five tiles that fill by how many of the five landed, and a missed day left as a gap rather than a reset. Single Go binary with no dependencies. The log is one JSON file on a mounted volume, written atomically and never silently replaced when it fails to parse, since it is the one thing here that cannot be recreated. Auth is Traefik basic auth at the ingress; the app has no login of its own, so every POST checks Sec-Fetch-Site to stop another origin posting with those credentials. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
65
internal/web/funcs.go
Normal file
65
internal/web/funcs.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Server) templateFuncs() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"asset": s.asset,
|
||||
"num": num,
|
||||
"dateLong": dateLong,
|
||||
"dateShort": dateShort,
|
||||
"weekday": weekday,
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"pluralDays": pluralDays,
|
||||
"tileTitle": tileTitle,
|
||||
}
|
||||
}
|
||||
|
||||
// num renders an optional number for a form field: blank stays blank, so an
|
||||
// unlogged day does not come back pre-filled with a zero.
|
||||
func num(p *int) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(*p)
|
||||
}
|
||||
|
||||
// dateLong renders "Thursday 31 July" for the day being logged.
|
||||
func dateLong(t time.Time) string {
|
||||
return t.Format("Monday 2 January")
|
||||
}
|
||||
|
||||
// dateShort renders "31 Jul" for the tiles.
|
||||
func dateShort(t time.Time) string {
|
||||
return t.Format("2 Jan")
|
||||
}
|
||||
|
||||
func weekday(t time.Time) string {
|
||||
return t.Format("Mon")
|
||||
}
|
||||
|
||||
func pluralDays(n int) string {
|
||||
if n == 1 {
|
||||
return "1 day"
|
||||
}
|
||||
return strconv.Itoa(n) + " days"
|
||||
}
|
||||
|
||||
// tileTitle is the hover and screen-reader text for a square on the card.
|
||||
func tileTitle(v dayView) string {
|
||||
label := "Day " + strconv.Itoa(v.Number) + ", " + dateShort(v.Time) + " — "
|
||||
switch {
|
||||
case v.IsFuture:
|
||||
return label + "still ahead"
|
||||
case v.Marks.Complete():
|
||||
return label + "all five"
|
||||
case v.Score == 0:
|
||||
return label + "nothing logged"
|
||||
default:
|
||||
return label + strconv.Itoa(v.Score) + " of 5"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user