Files
database_render/internal/template/renderer.go
2025-08-06 11:12:16 +08:00

262 lines
7.1 KiB
Go

package template
import (
"encoding/json"
"fmt"
"html/template"
"log/slog"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/static"
"github.com/rogeecn/database_render/internal/config"
"github.com/rogeecn/database_render/internal/service"
)
// Renderer handles template rendering
type Renderer struct {
templates *template.Template
service *service.DataService
config *config.Config
logger *slog.Logger
}
// NewRenderer creates a new template renderer
func NewRenderer(service *service.DataService, cfg *config.Config) (*Renderer, error) {
r := &Renderer{
service: service,
config: cfg,
logger: slog.With("component", "renderer"),
}
if err := r.loadTemplates(); err != nil {
return nil, fmt.Errorf("failed to load templates: %w", err)
}
return r, nil
}
// loadTemplates loads all templates from the templates directory
func (r *Renderer) loadTemplates() error {
// Define template functions
funcMap := template.FuncMap{
"dict": func(values ...interface{}) map[string]interface{} {
dict := make(map[string]interface{})
for i := 0; i < len(values); i += 2 {
if i+1 < len(values) {
key := values[i].(string)
dict[key] = values[i+1]
}
}
return dict
},
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"mul": func(a, b int) int { return a * b },
"min": func(a, b int) int {
if a < b {
return a
}
return b
},
"max": func(a, b int) int {
if a > b {
return a
}
return b
},
"json": func(v interface{}) string {
b, _ := json.Marshal(v)
return string(b)
},
"split": func(s string, sep string) []string {
return strings.Split(s, sep)
},
"formatTime": func(t interface{}) string {
switch v := t.(type) {
case time.Time:
return v.Format("2006-01-02 15:04:05")
case string:
return v
default:
return fmt.Sprintf("%v", v)
}
},
"renderField": func(value interface{}, renderType string, column interface{}) template.HTML {
switch renderType {
case "time":
return template.HTML(r.formatTime(value))
case "tag":
if columnMap, ok := column.(map[string]interface{}); ok {
if values, ok := columnMap["values"].(map[string]interface{}); ok {
if tag, ok := values[fmt.Sprintf("%v", value)].(map[string]interface{}); ok {
color := tag["color"].(string)
label := tag["label"].(string)
return template.HTML(fmt.Sprintf(
` <span class="tag" style="background-color: %s; color: white;">%s</span> `,
color, label,
))
}
}
}
return template.HTML(fmt.Sprintf("%v", value))
case "markdown":
// Return raw content for client-side markdown rendering
return template.HTML(fmt.Sprintf("%v", value))
default:
return template.HTML(fmt.Sprintf("%v", value))
}
},
"truncate": func(s string, length int) string {
if len(s) <= length {
return s
}
if length > 3 {
return s[:length-3] + "..."
}
return s[:length]
},
"eq": func(a, b interface{}) bool {
return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
},
}
// Load templates
templateDir := "web/templates"
pattern := filepath.Join(templateDir, "*.html")
tmpl, err := template.New("").Funcs(funcMap).ParseGlob(pattern)
if err != nil {
return fmt.Errorf("failed to parse templates: %w", err)
}
r.templates = tmpl
r.logger.Info("templates loaded successfully")
return nil
}
// formatTime formats time values for display
func (r *Renderer) formatTime(value interface{}) string {
switch v := value.(type) {
case time.Time:
return v.Format("2006-01-02 15:04:05")
case string:
// Try to parse as time
if t, err := time.Parse("2006-01-02T15:04:05Z", v); err == nil {
return t.Format("2006-01-02 15:04:05")
}
return v
default:
return fmt.Sprintf("%v", v)
}
}
// RenderList renders the list view for a table
func (r *Renderer) RenderList(c fiber.Ctx, tableName string) error {
// Parse query parameters
page, _ := strconv.Atoi(c.Query("page", "1"))
pageSize, _ := strconv.Atoi(c.Query("per_page", "10"))
search := c.Query("search", "")
sortField := c.Query("sort", "")
sortOrder := c.Query("order", "asc")
// Get table data
data, err := r.service.GetTableData(tableName, page, pageSize, search, sortField, sortOrder)
if err != nil {
r.logger.Error("failed to get table data", "error", err)
return c.Status(http.StatusInternalServerError).SendString("Failed to load table data")
}
// Get all tables for navigation
tables, err := r.service.GetTables()
if err != nil {
r.logger.Error("failed to get tables", "error", err)
return c.Status(http.StatusInternalServerError).SendString("Failed to load tables")
}
// Get table alias from config
tableConfig, err := r.service.GetTableConfig(tableName)
if err != nil {
r.logger.Error("failed to get table config", "error", err)
return c.Status(http.StatusInternalServerError).SendString("Failed to load table configuration")
}
// Calculate record range
startRecord := int64((data.Page-1)*data.PerPage + 1)
endRecord := startRecord + int64(len(data.Data)) - 1
if data.Total == 0 {
startRecord = 0
endRecord = 0
}
// Prepare template data
templateData := map[string]interface{}{
"Table": tableName,
"TableAlias": tableConfig.Alias,
"Columns": data.Columns,
"Data": data.Data,
"Total": data.Total,
"Page": data.Page,
"PerPage": data.PerPage,
"Pages": data.Pages,
"Search": search,
"SortField": sortField,
"SortOrder": sortOrder,
"Tables": tables,
"CurrentPath": c.Path(),
"StartRecord": startRecord,
"EndRecord": endRecord,
"LastUpdate": time.Now().Format("2006-01-02 15:04:05"),
}
// set content-type html
c.Response().Header.Set("Content-Type", "text/html; charset=utf-8")
// Render template
return r.templates.ExecuteTemplate(c.Response().BodyWriter(), "list.html", templateData)
}
// RenderIndex renders the index page
func (r *Renderer) RenderIndex(c fiber.Ctx) error {
// Get default table
defaultTable, err := r.service.GetDefaultTable()
if err != nil {
r.logger.Error("failed to get default table", "error", err)
return c.Status(http.StatusInternalServerError).SendString("No tables configured")
}
// Redirect to default table
return c.Redirect().To(fmt.Sprintf("/?table=%s", defaultTable))
}
// ServeStatic serves static files
func (r *Renderer) ServeStatic(app *fiber.App) {
// Serve static files
app.Use("/static/*", static.New("web/static"))
app.Use("/css/*", static.New("web/static/css"))
app.Use("/js/*", static.New("web/static/js"))
app.Use("/images/*", static.New("web/static/images"))
}
// NotFoundHandler handles 404 errors
func (r *Renderer) NotFoundHandler(c fiber.Ctx) error {
return c.Status(http.StatusNotFound).SendString("Page not found")
}
// ErrorHandler handles errors
func (r *Renderer) ErrorHandler(c fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
r.logger.Error("request error", "error", err, "code", code)
return c.Status(code).JSON(fiber.Map{
"error": err.Error(),
})
}