feat: init project
This commit is contained in:
250
internal/template/renderer.go
Normal file
250
internal/template/renderer.go
Normal file
@@ -0,0 +1,250 @@
|
||||
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")
|
||||
}
|
||||
|
||||
// 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(),
|
||||
}
|
||||
|
||||
// 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(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user