diff --git a/.air.conf b/.air.conf new file mode 100644 index 0000000..6d298ee --- /dev/null +++ b/.air.conf @@ -0,0 +1,17 @@ +root = "." +tmp_dir = "tmp" + +[build] + cmd = "go build -o ./tmp/main ./src/main.go" + bin = "tmp/main" + full_bin = "tmp/main" + include_ext = ["go", "tpl", "tmpl", "html", "css", "js"] + exclude_dir = ["tmp", "vendor"] + +[log] + time = true + +[colors] + main = "yellow" + watcher = "cyan" + build = "green" diff --git a/assets/styles.css b/assets/styles.css new file mode 100644 index 0000000..da5af47 --- /dev/null +++ b/assets/styles.css @@ -0,0 +1,88 @@ +body { + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, sans-serif; + margin: 0; + padding: 0; + background: #f7f7f9; + color: #111 +} + +.container { + max-width: 800px; + margin: 0 auto; + padding: 16px +} + +.card { + background: #fff; + border-radius: 12px; + padding: 16px; + margin: 12px 0; + box-shadow: 0 2px 6px rgba(0, 0, 0, .06) +} + +.row { + display: flex; + gap: 8px; + flex-wrap: wrap +} + +.btn { + display: inline-block; + padding: 10px 14px; + border-radius: 10px; + border: 1px solid #ddd; + background: #fafafa; + text-decoration: none +} + +.btn-primary { + background: #111; + color: #fff; + border-color: #111 +} + +.input { + width: 100%; + padding: 10px; + border-radius: 10px; + border: 1px solid #ddd +} + +.label { + font-size: 12px; + color: #444; + margin-bottom: 6px; + display: block +} + +.header { + padding: 12px 16px; + background: #fff; + position: sticky; + top: 0; + border-bottom: 1px solid #eee +} + +.nav a { + margin-right: 12px; + text-decoration: none; + color: #111 +} + +.small { + font-size: 12px; + color: #666 +} + +.list li { + padding: 8px 0; + border-bottom: 1px solid #f0f0f0 +} + +.badge { + display: inline-block; + padding: 2px 8px; + border-radius: 999px; + background: #eee; + font-size: 12px +} \ No newline at end of file diff --git a/src/handlers.go b/src/handlers.go index 6f2b988..ec303ca 100644 --- a/src/handlers.go +++ b/src/handlers.go @@ -10,8 +10,6 @@ import ( "gorm.io/gorm" ) -type ctxData map[string]any - func getIndex(c *gin.Context) { if getSessionUser(c) != nil { c.Redirect(302, "/enter") @@ -21,7 +19,7 @@ func getIndex(c *gin.Context) { } func getLogin(c *gin.Context) { - render(c, "login", ctxData{}) + tm.Render(c, "login", gin.H{}) } func postLogin(c *gin.Context) { @@ -54,7 +52,7 @@ func postLogin(c *gin.Context) { link := strings.TrimRight(baseURL, "/") + "/magic?token=" + token log.Printf("[MAGIC LINK] %s for %s (valid until %s)\n", link, email, lt.ExpiresAt.Format(time.RFC3339)) - render(c, "sent", ctxData{"Email": email}) + tm.Render(c, "sent", gin.H{"Email": email}) } func getMagic(c *gin.Context) { @@ -98,7 +96,7 @@ func getEnter(c *gin.Context) { if table != nil { post = "/t/" + table.Slug + "/enter" } - render(c, "enter", ctxData{"PostAction": post, "Table": table}) + tm.Render(c, "enter", gin.H{"PostAction": post, "Table": table}) } func postEnter(c *gin.Context) { @@ -199,7 +197,7 @@ func getHistory(c *gin.Context) { } rows = append(rows, GRow{Game: g, PlayersA: a, PlayersB: b}) } - render(c, "history", ctxData{"Games": rows}) + tm.Render(c, "history", gin.H{"Games": rows}) } func getLeaderboard(c *gin.Context) { @@ -265,7 +263,7 @@ func getLeaderboard(c *gin.Context) { rows = rows[:100] } - render(c, "leaderboard", ctxData{"Rows": rows}) + tm.Render(c, "leaderboard", gin.H{"Rows": rows}) } func getUserView(c *gin.Context) { @@ -313,7 +311,7 @@ func getUserView(c *gin.Context) { own = true } - render(c, "user", ctxData{"Viewed": u, "Stats": st, "Own": own}) + tm.Render(c, "user", gin.H{"Viewed": u, "Stats": st, "Own": own}) } func getMe(c *gin.Context) { @@ -322,7 +320,7 @@ func getMe(c *gin.Context) { c.Redirect(302, "/login") return } - render(c, "user", ctxData{"Viewed": cu, "Stats": struct{ Games, Wins, Losses int }{0, 0, 0}, "Own": true}) + tm.Render(c, "user", gin.H{"Viewed": cu, "Stats": struct{ Games, Wins, Losses int }{0, 0, 0}, "Own": true}) } func postMe(c *gin.Context) { diff --git a/src/main.go b/src/main.go index 5e287c5..34dc630 100644 --- a/src/main.go +++ b/src/main.go @@ -1,10 +1,8 @@ package main import ( - "html/template" "log" "os" - "time" "github.com/gin-gonic/gin" "gorm.io/driver/postgres" @@ -24,11 +22,7 @@ const ( PORT = "18765" ) -// ===================== Templates ===================== - -var tpl = template.Must(template.New("").Funcs(template.FuncMap{ - "fmtTime": func(t time.Time) string { return t.Local().Format("1000-01-01 10:10") }, -}).ParseGlob("templates/*.html")) +var logger = log.Default() // ===================== Main ===================== diff --git a/src/templates.go b/src/templates.go new file mode 100644 index 0000000..5d1fc95 --- /dev/null +++ b/src/templates.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "html/template" + "log" + "os" + "path/filepath" + "time" + + "github.com/gin-gonic/gin" +) + +var tm *TemplateManager + +func init() { + tm = NewTemplateManager("templates", "base", "html") +} + +type TemplateManager struct { + templates map[string]*template.Template + funcs template.FuncMap + base string + ext string + dir string +} + +// NewTemplateManager initializes the manager and loads templates +func NewTemplateManager(dir string, base string, ext string) *TemplateManager { + tm := &TemplateManager{ + templates: make(map[string]*template.Template), + funcs: template.FuncMap{ + "fmtTime": func(t time.Time) string { + return t.Local().Format("2006-01-02 15:04") // Go’s reference time + }, + }, + base: base, + dir: dir, + ext: ext, + } + tm.LoadTemplates() + + return tm +} + +// LoadTemplates parses the base template with each view template in the directory +func (tm *TemplateManager) LoadTemplates() { + pattern := filepath.Join(tm.dir, "*."+tm.ext) + files, err := filepath.Glob(pattern) + if err != nil { + panic(err) + } + base := filepath.Join(tm.dir, tm.base+"."+tm.ext) + + for _, file := range files { + if filepath.Base(file) == tm.base { + continue + } + + name := stripAfterDot(filepath.Base(file)) + logger.Println(name) + + // Parse base + view template together + tpl, err := template.New(name). + Funcs(tm.funcs). + ParseFiles(base, file) + if err != nil { + panic(err) + } + + tm.templates[name] = tpl + } +} + +// Render executes a template by name into the given context +func (tm *TemplateManager) Render(c *gin.Context, name string, data gin.H) error { + print("\nRendering template:", name, "\n") + u := getSessionUser(c) + data["CurrentUser"] = u + + tpl, ok := tm.templates[name] + if !ok { + return os.ErrNotExist + } + fmt.Print(tm.templates[name]) + + if err := tpl.ExecuteTemplate(c.Writer, tm.base, data); err != nil { + log.Println("tpl error:", err) + c.Status(500) + return err + } + + return nil +} diff --git a/src/utils.go b/src/utils.go index b5b9e1d..d8ad17c 100644 --- a/src/utils.go +++ b/src/utils.go @@ -4,11 +4,8 @@ import ( "crypto/rand" "encoding/hex" "errors" - "log" "strings" "time" - - "github.com/gin-gonic/gin" ) func mustRandToken(n int) string { @@ -118,15 +115,9 @@ func fmtSscanf(s, _ string, a *int) (int, error) { return 1, nil } -func render(c *gin.Context, name string, data ctxData) { - u := getSessionUser(c) - if data == nil { - data = ctxData{} - } - log.Println("tpl error:", name) - data["CurrentUser"] = u - if err := tpl.ExecuteTemplate(c.Writer, name, data); err != nil { - log.Println("tpl error:", err) - c.Status(500) +func stripAfterDot(s string) string { + if idx := strings.Index(s, "."); idx != -1 { + return s[:idx] } + return s } diff --git a/templates/base.html b/templates/base.html index bc4463d..26e764f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,96 +11,7 @@ - +
@@ -122,6 +33,7 @@