272 lines
7.3 KiB
Go
272 lines
7.3 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)
|
|
},
|
|
"until": func(n int) []int {
|
|
result := make([]int, n)
|
|
for i := 0; i < n; i++ {
|
|
result[i] = i + 1
|
|
}
|
|
return result
|
|
},
|
|
"ge": func(a, b int) bool {
|
|
return a >= 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(),
|
|
})
|
|
}
|