Changed the templating engine to a custom one. First bugged working example

This commit is contained in:
Jonas Hahn
2025-08-23 10:43:06 +02:00
parent 19e4834a9e
commit c9a3196ccb
15 changed files with 215 additions and 125 deletions

17
.air.conf Normal file
View File

@@ -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"

88
assets/styles.css Normal file
View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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 =====================

94
src/templates.go Normal file
View File

@@ -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") // Gos 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
}

View File

@@ -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
}

View File

@@ -11,96 +11,7 @@
<link rel="apple-touch-icon" href="/assets/favicon.ico">
<link rel="shortcut icon" href="/assets/favicon.ico">
<style>
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
}
</style>
<link rel="stylesheet" href="/assets/styles.css">
</head>
<body>
@@ -122,6 +33,7 @@
</div>
</div>
<div class="container">
<!-- This is shorthand for a define along with a template -->
{{block "content" .}}{{end}}
</div>
</body>

View File

@@ -1,4 +1,4 @@
{{define "enter"}}{{template "base" .}}{{end}}
{{define "title"}}Enter scores - QRank{{end}}
{{define "content"}}
<div class="card">
<h2>Enter a game</h2>

View File

@@ -1,4 +1,3 @@
{{define "history"}}{{template "base" .}}{{end}}
{{define "content"}}
<div class="card">
<h2>Global history</h2>

View File

@@ -1,4 +1,3 @@
{{define "leaderboard"}}{{template "base" .}}{{end}}
{{define "content"}}
<div class="card">
<h2>Leaderboard (by wins)</h2>

View File

@@ -1,4 +1,3 @@
{{define "login"}}{{template "base" .}}{{end}}
{{define "content"}}
<div class="card">
<h2>Sign in with your email</h2>

View File

@@ -1,4 +1,3 @@
{{define "sent"}}{{template "base" .}}{{end}}
{{define "content"}}
<div class="card">
<h2>Check the server logs</h2>

View File

@@ -1,4 +1,3 @@
{{define "user"}}{{template "base" .}}{{end}}
{{define "content"}}
<div class="card">
<h2>@{{.Viewed.Username}}</h2>

1
tmp/build-errors.log Normal file
View File

@@ -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

BIN
tmp/main Executable file
View File

Binary file not shown.