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 @@
+ {{block "content" .}}{{end}}
diff --git a/templates/enter.html b/templates/enter.html index 4185ad8..a5f558f 100644 --- a/templates/enter.html +++ b/templates/enter.html @@ -1,4 +1,4 @@ -{{define "enter"}}{{template "base" .}}{{end}} +{{define "title"}}Enter scores - QRank{{end}} {{define "content"}}

Enter a game

diff --git a/templates/history.html b/templates/history.html index b45680a..664552c 100644 --- a/templates/history.html +++ b/templates/history.html @@ -1,4 +1,3 @@ -{{define "history"}}{{template "base" .}}{{end}} {{define "content"}}

Global history

diff --git a/templates/leaderboard.html b/templates/leaderboard.html index a8e8109..a9c8311 100644 --- a/templates/leaderboard.html +++ b/templates/leaderboard.html @@ -1,4 +1,3 @@ -{{define "leaderboard"}}{{template "base" .}}{{end}} {{define "content"}}

Leaderboard (by wins)

diff --git a/templates/login.html b/templates/login.html index 51693ca..b54e64a 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,4 +1,3 @@ -{{define "login"}}{{template "base" .}}{{end}} {{define "content"}}

Sign in with your email

diff --git a/templates/sent.html b/templates/sent.html index 8e2ed14..e57cf8a 100644 --- a/templates/sent.html +++ b/templates/sent.html @@ -1,4 +1,3 @@ -{{define "sent"}}{{template "base" .}}{{end}} {{define "content"}}

Check the server logs

diff --git a/templates/user.html b/templates/user.html index 8e09188..b1ff19a 100644 --- a/templates/user.html +++ b/templates/user.html @@ -1,4 +1,3 @@ -{{define "user"}}{{template "base" .}}{{end}} {{define "content"}}

@{{.Viewed.Username}}

diff --git a/tmp/build-errors.log b/tmp/build-errors.log new file mode 100644 index 0000000..a2a130a --- /dev/null +++ b/tmp/build-errors.log @@ -0,0 +1 @@ +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/tmp/main b/tmp/main new file mode 100755 index 0000000..cf24bf0 Binary files /dev/null and b/tmp/main differ