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/model" "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( ` %s `, 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"), "TemplateType": "table", // Default to table view } // set content-type html c.Response().Header.Set("Content-Type", "text/html; charset=utf-8") // Use new template system for rendering absBuiltinPath, _ := filepath.Abs("./web/templates/builtin") absCustomPath, _ := filepath.Abs("./web/templates/custom") loader := NewTemplateLoader(TemplateConfig{ BuiltinPath: absBuiltinPath, CustomPath: absCustomPath, CacheEnabled: true, }) // Debug the data structure safely r.logger.Info("template data structure", "data_type", fmt.Sprintf("%T", templateData["Data"]), "columns_type", fmt.Sprintf("%T", templateData["Columns"]), ) // Check data length safely var dataLength int if data, ok := templateData["Data"].([]map[string]interface{}); ok { dataLength = len(data) } else if data, ok := templateData["Data"].([]interface{}); ok { dataLength = len(data) } var columnsLength int if cols, ok := templateData["Columns"].([]model.ColumnConfig); ok { columnsLength = len(cols) } else if cols, ok := templateData["Columns"].([]interface{}); ok { columnsLength = len(cols) } r.logger.Info("data lengths", "data_length", dataLength, "columns_length", columnsLength) // Try complete template - standalone template without layout inheritance tmpl, err := loader.LoadTemplate("complete", tableName) if err != nil { r.logger.Error("failed to load complete template", "error", err) // Fallback to simple template tmpl, err = loader.LoadTemplate("simple", tableName) if err != nil { r.logger.Error("failed to load simple template", "error", err) return r.templates.ExecuteTemplate(c.Response().BodyWriter(), "list.html", templateData) } } r.logger.Info("executing template", "template", "debug/simple") err = tmpl.Execute(c.Response().BodyWriter(), templateData) if err != nil { r.logger.Error("failed to execute template", "error", err) return r.templates.ExecuteTemplate(c.Response().BodyWriter(), "list.html", templateData) } return nil } // 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(), }) }