Add user theme support and migrate frontend to HTMX templates

- Add theme column to users table with default 'tokyo-night'
- Update all user queries to include theme field
- Add ListUsers and UpdateUserTheme database queries
- Update auth handlers to support HTMX form submissions and JSON API
- Add ListUsers API endpoint
- Replace embedded static files with Go templates
- Update Dockerfile to copy templates directory
- Redesign index.html with inline styles and HTMX forms
- Update Bruno API testing requests for auth endpoints
This commit is contained in:
2026-01-22 18:38:06 -05:00
parent 2399e892a6
commit e32fda6b65
12 changed files with 644 additions and 222 deletions
+25 -38
View File
@@ -4,12 +4,10 @@ import (
"bookmann/internal/config"
"bookmann/internal/database"
"bookmann/internal/handlers"
"embed"
"html/template"
"io"
"log"
"mime"
"net/http"
"path/filepath"
"strings"
"github.com/go-playground/validator/v10"
jwtgo "github.com/golang-jwt/jwt"
@@ -17,8 +15,13 @@ import (
"github.com/labstack/echo/v4/middleware"
)
//go:embed static/*
var staticFS embed.FS
type TemplateRenderer struct {
templates *template.Template
}
func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
// CustomValidator wraps the go-playground validator
type CustomValidator struct {
@@ -45,6 +48,12 @@ func main() {
// Set up validator
e.Validator = &CustomValidator{validator: validator.New()}
// Set up templates
renderer := &TemplateRenderer{
templates: template.Must(template.ParseGlob("templates/*.html")),
}
e.Renderer = renderer
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
@@ -69,42 +78,20 @@ func main() {
// Protected routes
protected := e.Group("/api", jwtMiddleware)
protected.GET("/auth/profile", auth.GetProfile)
protected.GET("/users", auth.ListUsers)
// Routes
handlers.SetupRoutes(protected, queries)
// Serve static files with SPA fallback from embedded FS
e.GET("/*", func(c echo.Context) error {
path := c.Request().URL.Path
if strings.HasPrefix(path, "/api") {
return c.String(http.StatusNotFound, "Not found")
}
// Try to serve the file from embedded FS
filePath := "static" + path
if file, err := staticFS.Open(filePath); err == nil {
defer file.Close()
contentType := mime.TypeByExtension(filepath.Ext(filePath))
if contentType == "" {
contentType = "application/octet-stream"
}
return c.Stream(http.StatusOK, contentType, file)
}
// Try .html extension
htmlPath := filePath + ".html"
if file, err := staticFS.Open(htmlPath); err == nil {
defer file.Close()
return c.Stream(http.StatusOK, "text/html", file)
}
// Try /index.html
indexPath := filePath + "/index.html"
if file, err := staticFS.Open(indexPath); err == nil {
defer file.Close()
return c.Stream(http.StatusOK, "text/html", file)
}
// Fallback to index.html
file, _ := staticFS.Open("static/index.html")
defer file.Close()
return c.Stream(http.StatusOK, "text/html", file)
// Page routes
e.GET("/", func(c echo.Context) error {
return c.Render(http.StatusOK, "index.html", nil)
})
e.GET("/login", func(c echo.Context) error {
return c.Render(http.StatusOK, "login.html", nil)
})
e.GET("/register", func(c echo.Context) error {
return c.Render(http.StatusOK, "register.html", nil)
})
// Start server