refactor: Streamline asset serving and localization handling
- Refactored static file serving to utilize Echo's built-in middleware for improved clarity and maintainability. - Removed deprecated template loading logic and replaced it with a direct approach using embedded localization files. - Simplified the localization retrieval process by leveraging a pre-parsed strings map, enhancing performance and reducing complexity. - Updated various template components to reference assets directly from the new serving path. - Enhanced logging for localization errors to provide better feedback during runtime.
- commit
- 283cc352918380b9155a0eb00b8045eefe945b28
- author
- bamen · · 2025-03-30 15:47 UTC
- parent
- e39fdc8
- signature
- unsigned
16 files · +180 -174
M internal/httpserve/handlers/container_management.go +2 -12
| @@ -9,7 +9,6 @@ import ( | |||
| 9 | 9 | "time" | |
| 10 | 10 | ||
| 11 | 11 | "github.com/bnema/gordon/internal/server" | |
| 12 | - | "github.com/bnema/gordon/internal/templating/load" | |
| 13 | 12 | components "github.com/bnema/gordon/internal/templating/models/templ/components" | |
| 14 | 13 | "github.com/bnema/gordon/internal/templating/render" | |
| 15 | 14 | "github.com/bnema/gordon/pkg/docker" | |
| @@ -100,18 +99,9 @@ type ContainerDisplay struct { | |||
| 100 | 99 | ||
| 101 | 100 | // ActionSuccess returns the success HTML fragment | |
| 102 | 101 | func ActionSuccess(a *server.App) string { | |
| 103 | - | // Create a data structure to pass to the template | |
| 104 | - | data := map[string]interface{}{ | |
| 105 | - | "Message": "Container deleted successfully!", | |
| 106 | - | } | |
| 107 | - | ||
| 108 | - | successFragment, err := load.Fragment(a, "success", data) | |
| 109 | - | if err != nil { | |
| 110 | - | fmt.Println(err) | |
| 111 | - | return "" | |
| 112 | - | } | |
| 113 | 102 | ||
| 114 | - | return successFragment | |
| 103 | + | // return just a string | |
| 104 | + | return "success" | |
| 115 | 105 | } | |
| 116 | 106 | ||
| 117 | 107 | // ImageManagerComponent handles the /image-manager route (HTMX route) | |
| @@ -9,7 +9,6 @@ import ( | |||||
| 9 | "time" | 9 | "time" | ||
| 10 | 10 | ||||
| 11 | "github.com/bnema/gordon/internal/server" | 11 | "github.com/bnema/gordon/internal/server" | ||
| 12 | - | "github.com/bnema/gordon/internal/templating/load" | |||
| 13 | components "github.com/bnema/gordon/internal/templating/models/templ/components" | 12 | components "github.com/bnema/gordon/internal/templating/models/templ/components" | ||
| 14 | "github.com/bnema/gordon/internal/templating/render" | 13 | "github.com/bnema/gordon/internal/templating/render" | ||
| 15 | "github.com/bnema/gordon/pkg/docker" | 14 | "github.com/bnema/gordon/pkg/docker" | ||
| @@ -100,18 +99,9 @@ type ContainerDisplay struct { | |||||
| 100 | 99 | ||||
| 101 | // ActionSuccess returns the success HTML fragment | 100 | // ActionSuccess returns the success HTML fragment | ||
| 102 | func ActionSuccess(a *server.App) string { | 101 | func ActionSuccess(a *server.App) string { | ||
| 103 | - | // Create a data structure to pass to the template | |||
| 104 | - | data := map[string]interface{}{ | |||
| 105 | - | "Message": "Container deleted successfully!", | |||
| 106 | - | } | |||
| 107 | - | ||||
| 108 | - | successFragment, err := load.Fragment(a, "success", data) | |||
| 109 | - | if err != nil { | |||
| 110 | - | fmt.Println(err) | |||
| 111 | - | return "" | |||
| 112 | - | } | |||
| 113 | 102 | ||||
| 114 | - | return successFragment | 103 | + | // return just a string |
| 104 | + | return "success" | |||
| 115 | } | 105 | } | ||
| 116 | 106 | ||||
| 117 | // ImageManagerComponent handles the /image-manager route (HTMX route) | 107 | // ImageManagerComponent handles the /image-manager route (HTMX route) | ||
M internal/httpserve/middleware/secure.go +24 -1
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package middleware | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "log" | |
| 5 | 6 | "net/http" | |
| 6 | 7 | "os" | |
| @@ -16,13 +17,35 @@ import ( | |||
| 16 | 17 | func SecureRoutes(a *server.App) echo.MiddlewareFunc { | |
| 17 | 18 | proxyURL := a.Config.Build.ProxyURL | |
| 18 | 19 | urlCheckVersion := a.Config.Build.ProxyURL + "/version" | |
| 20 | + | ||
| 21 | + | // Base directives | |
| 22 | + | defaultSrc := "'self'" | |
| 23 | + | styleSrc := "'self' 'unsafe-inline'" | |
| 24 | + | fontSrc := "'self' data:" | |
| 25 | + | imgSrc := "'self' data:" | |
| 26 | + | connectSrc := fmt.Sprintf("'self' %s %s", proxyURL, urlCheckVersion) | |
| 27 | + | ||
| 28 | + | // Start building script-src | |
| 29 | + | scriptSrc := "'self' 'unsafe-inline' 'unsafe-eval'" | |
| 30 | + | ||
| 31 | + | // Conditionally add Cloudflare Insights if SkipCertificates is true | |
| 32 | + | if a.Config.ReverseProxy.SkipCertificates { | |
| 33 | + | scriptSrc += " https://static.cloudflareinsights.com" | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // Construct the final CSP string | |
| 37 | + | csp := fmt.Sprintf( | |
| 38 | + | "default-src %s; style-src %s; font-src %s; img-src %s; script-src %s; connect-src %s", | |
| 39 | + | defaultSrc, styleSrc, fontSrc, imgSrc, scriptSrc, connectSrc, | |
| 40 | + | ) | |
| 41 | + | ||
| 19 | 42 | return middleware.SecureWithConfig(middleware.SecureConfig{ | |
| 20 | 43 | XSSProtection: "1; mode=block", | |
| 21 | 44 | ContentTypeNosniff: "nosniff", | |
| 22 | 45 | XFrameOptions: "SAMEORIGIN", | |
| 23 | 46 | HSTSMaxAge: 3600, | |
| 24 | 47 | HSTSExcludeSubdomains: false, | |
| 25 | - | ContentSecurityPolicy: "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' " + proxyURL + " " + urlCheckVersion, | |
| 48 | + | ContentSecurityPolicy: csp, // Use the dynamically constructed CSP | |
| 26 | 49 | }) | |
| 27 | 50 | } | |
| 28 | 51 | ||
| @@ -1,6 +1,7 @@ | |||||
| 1 | package middleware | 1 | package middleware | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "fmt" | |||
| 4 | "log" | 5 | "log" | ||
| 5 | "net/http" | 6 | "net/http" | ||
| 6 | "os" | 7 | "os" | ||
| @@ -16,13 +17,35 @@ import ( | |||||
| 16 | func SecureRoutes(a *server.App) echo.MiddlewareFunc { | 17 | func SecureRoutes(a *server.App) echo.MiddlewareFunc { | ||
| 17 | proxyURL := a.Config.Build.ProxyURL | 18 | proxyURL := a.Config.Build.ProxyURL | ||
| 18 | urlCheckVersion := a.Config.Build.ProxyURL + "/version" | 19 | urlCheckVersion := a.Config.Build.ProxyURL + "/version" | ||
| 20 | + | ||||
| 21 | + | // Base directives | |||
| 22 | + | defaultSrc := "'self'" | |||
| 23 | + | styleSrc := "'self' 'unsafe-inline'" | |||
| 24 | + | fontSrc := "'self' data:" | |||
| 25 | + | imgSrc := "'self' data:" | |||
| 26 | + | connectSrc := fmt.Sprintf("'self' %s %s", proxyURL, urlCheckVersion) | |||
| 27 | + | ||||
| 28 | + | // Start building script-src | |||
| 29 | + | scriptSrc := "'self' 'unsafe-inline' 'unsafe-eval'" | |||
| 30 | + | ||||
| 31 | + | // Conditionally add Cloudflare Insights if SkipCertificates is true | |||
| 32 | + | if a.Config.ReverseProxy.SkipCertificates { | |||
| 33 | + | scriptSrc += " https://static.cloudflareinsights.com" | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // Construct the final CSP string | |||
| 37 | + | csp := fmt.Sprintf( | |||
| 38 | + | "default-src %s; style-src %s; font-src %s; img-src %s; script-src %s; connect-src %s", | |||
| 39 | + | defaultSrc, styleSrc, fontSrc, imgSrc, scriptSrc, connectSrc, | |||
| 40 | + | ) | |||
| 41 | + | ||||
| 19 | return middleware.SecureWithConfig(middleware.SecureConfig{ | 42 | return middleware.SecureWithConfig(middleware.SecureConfig{ | ||
| 20 | XSSProtection: "1; mode=block", | 43 | XSSProtection: "1; mode=block", | ||
| 21 | ContentTypeNosniff: "nosniff", | 44 | ContentTypeNosniff: "nosniff", | ||
| 22 | XFrameOptions: "SAMEORIGIN", | 45 | XFrameOptions: "SAMEORIGIN", | ||
| 23 | HSTSMaxAge: 3600, | 46 | HSTSMaxAge: 3600, | ||
| 24 | HSTSExcludeSubdomains: false, | 47 | HSTSExcludeSubdomains: false, | ||
| 25 | - | ContentSecurityPolicy: "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' " + proxyURL + " " + urlCheckVersion, | 48 | + | ContentSecurityPolicy: csp, // Use the dynamically constructed CSP |
| 26 | }) | 49 | }) | ||
| 27 | } | 50 | } | ||
| 28 | 51 | ||||
M internal/httpserve/router.go +8 -9
| @@ -56,25 +56,24 @@ func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | |||
| 56 | 56 | ||
| 57 | 57 | log.Debug("Binding static routes under /assets/*") | |
| 58 | 58 | ||
| 59 | - | // Setup static file server | |
| 60 | - | httpFS := http.FS(a.PublicFS) | |
| 61 | - | noDirFS := handlers.NewNoDirFileSys(httpFS) | |
| 62 | - | fileServer := http.FileServer(noDirFS) | |
| 63 | - | ||
| 64 | 59 | // Middleware to set cache headers for static files | |
| 65 | 60 | staticCacheMiddleware := func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 66 | 61 | return func(c echo.Context) error { | |
| 62 | + | // Determine caching based on environment | |
| 67 | 63 | if os.Getenv("RUN_ENV") != "dev" { | |
| 68 | - | c.Response().Header().Set("Cache-Control", "public, max-age=86400") // 1 day | |
| 64 | + | c.Response().Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day in production | |
| 69 | 65 | } else { | |
| 70 | - | c.Response().Header().Set("Cache-Control", "no-cache") | |
| 66 | + | c.Response().Header().Set("Cache-Control", "no-cache") // No caching in development | |
| 71 | 67 | } | |
| 72 | 68 | return next(c) | |
| 73 | 69 | } | |
| 74 | 70 | } | |
| 75 | 71 | ||
| 76 | - | // Serve static files from /assets path | |
| 77 | - | e.GET("/assets/*", echo.WrapHandler(http.StripPrefix("/assets/", fileServer)), staticCacheMiddleware, echomid.Gzip()) | |
| 72 | + | // Use Echo's static file middleware to serve assets directly from the filesystem. | |
| 73 | + | // The path is relative to the project root. | |
| 74 | + | staticGroup := e.Group("/assets") | |
| 75 | + | staticGroup.Use(staticCacheMiddleware) | |
| 76 | + | staticGroup.Static("/", "internal/webui/public/assets") | |
| 78 | 77 | ||
| 79 | 78 | // Only bind HTMX endpoints if Admin WebUI is enabled | |
| 80 | 79 | if a.Config.Admin.IsAdminWebUIEnabled() { | |
| @@ -56,25 +56,24 @@ func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | |||||
| 56 | 56 | ||||
| 57 | log.Debug("Binding static routes under /assets/*") | 57 | log.Debug("Binding static routes under /assets/*") | ||
| 58 | 58 | ||||
| 59 | - | // Setup static file server | |||
| 60 | - | httpFS := http.FS(a.PublicFS) | |||
| 61 | - | noDirFS := handlers.NewNoDirFileSys(httpFS) | |||
| 62 | - | fileServer := http.FileServer(noDirFS) | |||
| 63 | - | ||||
| 64 | // Middleware to set cache headers for static files | 59 | // Middleware to set cache headers for static files | ||
| 65 | staticCacheMiddleware := func(next echo.HandlerFunc) echo.HandlerFunc { | 60 | staticCacheMiddleware := func(next echo.HandlerFunc) echo.HandlerFunc { | ||
| 66 | return func(c echo.Context) error { | 61 | return func(c echo.Context) error { | ||
| 62 | + | // Determine caching based on environment | |||
| 67 | if os.Getenv("RUN_ENV") != "dev" { | 63 | if os.Getenv("RUN_ENV") != "dev" { | ||
| 68 | - | c.Response().Header().Set("Cache-Control", "public, max-age=86400") // 1 day | 64 | + | c.Response().Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day in production |
| 69 | } else { | 65 | } else { | ||
| 70 | - | c.Response().Header().Set("Cache-Control", "no-cache") | 66 | + | c.Response().Header().Set("Cache-Control", "no-cache") // No caching in development |
| 71 | } | 67 | } | ||
| 72 | return next(c) | 68 | return next(c) | ||
| 73 | } | 69 | } | ||
| 74 | } | 70 | } | ||
| 75 | 71 | ||||
| 76 | - | // Serve static files from /assets path | 72 | + | // Use Echo's static file middleware to serve assets directly from the filesystem. |
| 77 | - | e.GET("/assets/*", echo.WrapHandler(http.StripPrefix("/assets/", fileServer)), staticCacheMiddleware, echomid.Gzip()) | 73 | + | // The path is relative to the project root. |
| 74 | + | staticGroup := e.Group("/assets") | |||
| 75 | + | staticGroup.Use(staticCacheMiddleware) | |||
| 76 | + | staticGroup.Static("/", "internal/webui/public/assets") | |||
| 78 | 77 | ||||
| 79 | // Only bind HTMX endpoints if Admin WebUI is enabled | 78 | // Only bind HTMX endpoints if Admin WebUI is enabled | ||
| 80 | if a.Config.Admin.IsAdminWebUIEnabled() { | 79 | if a.Config.Admin.IsAdminWebUIEnabled() { | ||
M internal/server/app_server.go +4 -4
| @@ -4,7 +4,6 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "database/sql" | |
| 6 | 6 | "fmt" | |
| 7 | - | "io/fs" | |
| 8 | 7 | "time" | |
| 9 | 8 | ||
| 10 | 9 | "github.com/bnema/gordon/internal/common" | |
| @@ -14,9 +13,10 @@ import ( | |||
| 14 | 13 | ) | |
| 15 | 14 | ||
| 16 | 15 | type App struct { | |
| 17 | - | TemplateFS fs.FS | |
| 18 | - | PublicFS fs.FS | |
| 19 | - | LocYML []byte // strings.yml contains the strings for the current language | |
| 16 | + | // TemplateFS fs.FS // No longer needed | |
| 17 | + | // PublicFS fs.FS // Removed as we now serve assets directly | |
| 18 | + | // LocYML []byte // Replaced by parsed Strings map | |
| 19 | + | Strings map[string]interface{} // Parsed localization strings | |
| 20 | 20 | DBDir string | |
| 21 | 21 | DBFilename string | |
| 22 | 22 | DBPath string | |
| @@ -4,7 +4,6 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "database/sql" | 5 | "database/sql" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | - | "io/fs" | |||
| 8 | "time" | 7 | "time" | ||
| 9 | 8 | ||||
| 10 | "github.com/bnema/gordon/internal/common" | 9 | "github.com/bnema/gordon/internal/common" | ||
| @@ -14,9 +13,10 @@ import ( | |||||
| 14 | ) | 13 | ) | ||
| 15 | 14 | ||||
| 16 | type App struct { | 15 | type App struct { | ||
| 17 | - | TemplateFS fs.FS | 16 | + | // TemplateFS fs.FS // No longer needed |
| 18 | - | PublicFS fs.FS | 17 | + | // PublicFS fs.FS // Removed as we now serve assets directly |
| 19 | - | LocYML []byte // strings.yml contains the strings for the current language | 18 | + | // LocYML []byte // Replaced by parsed Strings map |
| 19 | + | Strings map[string]interface{} // Parsed localization strings | |||
| 20 | DBDir string | 20 | DBDir string | ||
| 21 | DBFilename string | 21 | DBFilename string | ||
| 22 | DBPath string | 22 | DBPath string | ||
M internal/server/new_server.go +9 -14
| @@ -1,7 +1,6 @@ | |||
| 1 | 1 | package server | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "io" | |
| 5 | 4 | "os" | |
| 6 | 5 | "path/filepath" | |
| 7 | 6 | "time" | |
| @@ -9,8 +8,8 @@ import ( | |||
| 9 | 8 | "github.com/bnema/gordon/internal/common" | |
| 10 | 9 | "github.com/bnema/gordon/internal/proxy" | |
| 11 | 10 | "github.com/bnema/gordon/internal/templating" | |
| 12 | - | "github.com/bnema/gordon/internal/webui" | |
| 13 | 11 | "github.com/bnema/gordon/pkg/logger" | |
| 12 | + | "gopkg.in/yaml.v3" | |
| 14 | 13 | ) | |
| 15 | 14 | ||
| 16 | 15 | func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | |
| @@ -40,28 +39,24 @@ func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | |||
| 40 | 39 | } | |
| 41 | 40 | } | |
| 42 | 41 | ||
| 43 | - | // Open the strings.yml file containing the strings for the current language | |
| 44 | - | file, err := templating.TemplateFS.Open("txt/locstrings.yml") | |
| 42 | + | // Read the embedded localization file | |
| 43 | + | locBytes, err := templating.LocFS.ReadFile("models/txt/locstrings.yml") | |
| 45 | 44 | if err != nil { | |
| 46 | - | logger.Fatalf("Failed to open strings.yml: %v", err) | |
| 45 | + | logger.Fatalf("Failed to read embedded locstrings.yml: %v", err) | |
| 47 | 46 | } | |
| 48 | 47 | ||
| 49 | - | // Read the file content into a byte slice | |
| 50 | - | bytes, err := io.ReadAll(file) | |
| 51 | - | if err != nil { | |
| 52 | - | logger.Fatalf("Failed to read strings.yml: %v", err) | |
| 48 | + | // Parse the YAML content | |
| 49 | + | var stringsMap map[string]interface{} | |
| 50 | + | if err := yaml.Unmarshal(locBytes, &stringsMap); err != nil { | |
| 51 | + | logger.Fatalf("Failed to parse locstrings.yml: %v", err) | |
| 53 | 52 | } | |
| 54 | 53 | ||
| 55 | - | file.Close() | |
| 56 | - | ||
| 57 | 54 | // Initialize DB | |
| 58 | 55 | DBDir := config.General.StorageDir + "/db" | |
| 59 | 56 | ||
| 60 | 57 | // Initialize App | |
| 61 | 58 | a := &App{ | |
| 62 | - | TemplateFS: templating.TemplateFS, | |
| 63 | - | PublicFS: webui.PublicFS, | |
| 64 | - | LocYML: bytes, | |
| 59 | + | Strings: stringsMap, | |
| 65 | 60 | DBDir: DBDir, | |
| 66 | 61 | DBFilename: DBFilename, | |
| 67 | 62 | Config: *config, | |
| @@ -1,7 +1,6 @@ | |||||
| 1 | package server | 1 | package server | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "io" | |||
| 5 | "os" | 4 | "os" | ||
| 6 | "path/filepath" | 5 | "path/filepath" | ||
| 7 | "time" | 6 | "time" | ||
| @@ -9,8 +8,8 @@ import ( | |||||
| 9 | "github.com/bnema/gordon/internal/common" | 8 | "github.com/bnema/gordon/internal/common" | ||
| 10 | "github.com/bnema/gordon/internal/proxy" | 9 | "github.com/bnema/gordon/internal/proxy" | ||
| 11 | "github.com/bnema/gordon/internal/templating" | 10 | "github.com/bnema/gordon/internal/templating" | ||
| 12 | - | "github.com/bnema/gordon/internal/webui" | |||
| 13 | "github.com/bnema/gordon/pkg/logger" | 11 | "github.com/bnema/gordon/pkg/logger" | ||
| 12 | + | "gopkg.in/yaml.v3" | |||
| 14 | ) | 13 | ) | ||
| 15 | 14 | ||||
| 16 | func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | 15 | func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | ||
| @@ -40,28 +39,24 @@ func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | |||||
| 40 | } | 39 | } | ||
| 41 | } | 40 | } | ||
| 42 | 41 | ||||
| 43 | - | // Open the strings.yml file containing the strings for the current language | 42 | + | // Read the embedded localization file |
| 44 | - | file, err := templating.TemplateFS.Open("txt/locstrings.yml") | 43 | + | locBytes, err := templating.LocFS.ReadFile("models/txt/locstrings.yml") |
| 45 | if err != nil { | 44 | if err != nil { | ||
| 46 | - | logger.Fatalf("Failed to open strings.yml: %v", err) | 45 | + | logger.Fatalf("Failed to read embedded locstrings.yml: %v", err) |
| 47 | } | 46 | } | ||
| 48 | 47 | ||||
| 49 | - | // Read the file content into a byte slice | 48 | + | // Parse the YAML content |
| 50 | - | bytes, err := io.ReadAll(file) | 49 | + | var stringsMap map[string]interface{} |
| 51 | - | if err != nil { | 50 | + | if err := yaml.Unmarshal(locBytes, &stringsMap); err != nil { |
| 52 | - | logger.Fatalf("Failed to read strings.yml: %v", err) | 51 | + | logger.Fatalf("Failed to parse locstrings.yml: %v", err) |
| 53 | } | 52 | } | ||
| 54 | 53 | ||||
| 55 | - | file.Close() | |||
| 56 | - | ||||
| 57 | // Initialize DB | 54 | // Initialize DB | ||
| 58 | DBDir := config.General.StorageDir + "/db" | 55 | DBDir := config.General.StorageDir + "/db" | ||
| 59 | 56 | ||||
| 60 | // Initialize App | 57 | // Initialize App | ||
| 61 | a := &App{ | 58 | a := &App{ | ||
| 62 | - | TemplateFS: templating.TemplateFS, | 59 | + | Strings: stringsMap, |
| 63 | - | PublicFS: webui.PublicFS, | |||
| 64 | - | LocYML: bytes, | |||
| 65 | DBDir: DBDir, | 60 | DBDir: DBDir, | ||
| 66 | DBFilename: DBFilename, | 61 | DBFilename: DBFilename, | ||
| 67 | Config: *config, | 62 | Config: *config, | ||
M internal/templating/embed.go +6 -6
| @@ -2,12 +2,12 @@ package templating | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "embed" | |
| 5 | - | ||
| 6 | - | "github.com/labstack/echo/v4" | |
| 7 | 5 | ) | |
| 8 | 6 | ||
| 9 | - | // Embedding templates directories | |
| 7 | + | // Embedding localization file | |
| 8 | + | ||
| 9 | + | //go:embed models/txt/locstrings.yml | |
| 10 | + | var locFS embed.FS | |
| 10 | 11 | ||
| 11 | - | //go:embed models/* | |
| 12 | - | var template embed.FS | |
| 13 | - | var TemplateFS = echo.MustSubFS(template, "models") | |
| 12 | + | // LocFS provides access to the embedded localization file. | |
| 13 | + | var LocFS = locFS | |
| @@ -2,12 +2,12 @@ package templating | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "embed" | 4 | "embed" | ||
| 5 | - | ||||
| 6 | - | "github.com/labstack/echo/v4" | |||
| 7 | ) | 5 | ) | ||
| 8 | 6 | ||||
| 9 | - | // Embedding templates directories | 7 | + | // Embedding localization file |
| 8 | + | ||||
| 9 | + | //go:embed models/txt/locstrings.yml | |||
| 10 | + | var locFS embed.FS | |||
| 10 | 11 | ||||
| 11 | - | //go:embed models/* | 12 | + | // LocFS provides access to the embedded localization file. |
| 12 | - | var template embed.FS | 13 | + | var LocFS = locFS |
| 13 | - | var TemplateFS = echo.MustSubFS(template, "models") | |||
D internal/templating/load/fragment.go +0 -48
| @@ -1,48 +0,0 @@ | |||
| 1 | - | package load | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bytes" | |
| 5 | - | "fmt" | |
| 6 | - | "html/template" | |
| 7 | - | "io" | |
| 8 | - | ||
| 9 | - | "github.com/bnema/gordon/internal/server" | |
| 10 | - | ) | |
| 11 | - | ||
| 12 | - | // Fragment loads and renders a specific HTML fragment from the app's TemplateFS | |
| 13 | - | func Fragment(a *server.App, fragmentName string, data ...interface{}) (string, error) { | |
| 14 | - | filePath := fmt.Sprintf("html/fragments/%s.gohtml", fragmentName) | |
| 15 | - | ||
| 16 | - | // Open the file | |
| 17 | - | file, err := a.TemplateFS.Open(filePath) | |
| 18 | - | if err != nil { | |
| 19 | - | return "", err | |
| 20 | - | } | |
| 21 | - | defer file.Close() | |
| 22 | - | ||
| 23 | - | // Read the file's content | |
| 24 | - | content, err := io.ReadAll(file) | |
| 25 | - | if err != nil { | |
| 26 | - | return "", err | |
| 27 | - | } | |
| 28 | - | ||
| 29 | - | // If no data is provided, just return the raw content | |
| 30 | - | if len(data) == 0 { | |
| 31 | - | return string(content), nil | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | // Parse the template | |
| 35 | - | tmpl, err := template.New(fragmentName).Parse(string(content)) | |
| 36 | - | if err != nil { | |
| 37 | - | return "", err | |
| 38 | - | } | |
| 39 | - | ||
| 40 | - | // Execute the template with the provided data | |
| 41 | - | var buf bytes.Buffer | |
| 42 | - | err = tmpl.Execute(&buf, data[0]) | |
| 43 | - | if err != nil { | |
| 44 | - | return "", err | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | return buf.String(), nil | |
| 48 | - | } | |
| @@ -1,48 +0,0 @@ | |||||
| 1 | - | package load | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "bytes" | |||
| 5 | - | "fmt" | |||
| 6 | - | "html/template" | |||
| 7 | - | "io" | |||
| 8 | - | ||||
| 9 | - | "github.com/bnema/gordon/internal/server" | |||
| 10 | - | ) | |||
| 11 | - | ||||
| 12 | - | // Fragment loads and renders a specific HTML fragment from the app's TemplateFS | |||
| 13 | - | func Fragment(a *server.App, fragmentName string, data ...interface{}) (string, error) { | |||
| 14 | - | filePath := fmt.Sprintf("html/fragments/%s.gohtml", fragmentName) | |||
| 15 | - | ||||
| 16 | - | // Open the file | |||
| 17 | - | file, err := a.TemplateFS.Open(filePath) | |||
| 18 | - | if err != nil { | |||
| 19 | - | return "", err | |||
| 20 | - | } | |||
| 21 | - | defer file.Close() | |||
| 22 | - | ||||
| 23 | - | // Read the file's content | |||
| 24 | - | content, err := io.ReadAll(file) | |||
| 25 | - | if err != nil { | |||
| 26 | - | return "", err | |||
| 27 | - | } | |||
| 28 | - | ||||
| 29 | - | // If no data is provided, just return the raw content | |||
| 30 | - | if len(data) == 0 { | |||
| 31 | - | return string(content), nil | |||
| 32 | - | } | |||
| 33 | - | ||||
| 34 | - | // Parse the template | |||
| 35 | - | tmpl, err := template.New(fragmentName).Parse(string(content)) | |||
| 36 | - | if err != nil { | |||
| 37 | - | return "", err | |||
| 38 | - | } | |||
| 39 | - | ||||
| 40 | - | // Execute the template with the provided data | |||
| 41 | - | var buf bytes.Buffer | |||
| 42 | - | err = tmpl.Execute(&buf, data[0]) | |||
| 43 | - | if err != nil { | |||
| 44 | - | return "", err | |||
| 45 | - | } | |||
| 46 | - | ||||
| 47 | - | return buf.String(), nil | |||
| 48 | - | } | |||
M internal/templating/models/templ/components/header.templ +7 -7
| @@ -9,13 +9,13 @@ templ Header(title string) { | |||
| 9 | 9 | <title>{ title }</title> | |
| 10 | 10 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |
| 11 | 11 | ||
| 12 | - | <script src="/libs/jquery.min.js"></script> | |
| 13 | - | <script src="/libs/htmx.min.js"></script> | |
| 14 | - | <script src="/libs/htmx-response-targets.js"></script> | |
| 15 | - | <script src="/libs/htmx-debug.js"></script> | |
| 16 | - | <script src="/libs/htmx-sse.js"></script> | |
| 17 | - | <script src="/libs/daisyui.js" type="module"></script> | |
| 18 | - | <script defer src="/libs/hyperscript.min.js"></script> | |
| 12 | + | <script src="/assets/js/jquery.min.js"></script> | |
| 13 | + | <script src="/assets/js/htmx.min.js"></script> | |
| 14 | + | <script src="/assets/js/htmx-response-targets.js"></script> | |
| 15 | + | <script src="/assets/js/htmx-debug.js"></script> | |
| 16 | + | <script src="/assets/js/htmx-sse.js"></script> | |
| 17 | + | <script src="/assets/js/daisyui.js" type="module"></script> | |
| 18 | + | <script defer src="/assets/js/hyperscript.min.js"></script> | |
| 19 | 19 | <script defer src="/assets/js/custom.js"></script> | |
| 20 | 20 | ||
| 21 | 21 | <!-- daisyUI theme configuration --> | |
| @@ -9,13 +9,13 @@ templ Header(title string) { | |||||
| 9 | <title>{ title }</title> | 9 | <title>{ title }</title> | ||
| 10 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | 10 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | ||
| 11 | 11 | ||||
| 12 | - | <script src="/libs/jquery.min.js"></script> | 12 | + | <script src="/assets/js/jquery.min.js"></script> |
| 13 | - | <script src="/libs/htmx.min.js"></script> | 13 | + | <script src="/assets/js/htmx.min.js"></script> |
| 14 | - | <script src="/libs/htmx-response-targets.js"></script> | 14 | + | <script src="/assets/js/htmx-response-targets.js"></script> |
| 15 | - | <script src="/libs/htmx-debug.js"></script> | 15 | + | <script src="/assets/js/htmx-debug.js"></script> |
| 16 | - | <script src="/libs/htmx-sse.js"></script> | 16 | + | <script src="/assets/js/htmx-sse.js"></script> |
| 17 | - | <script src="/libs/daisyui.js" type="module"></script> | 17 | + | <script src="/assets/js/daisyui.js" type="module"></script> |
| 18 | - | <script defer src="/libs/hyperscript.min.js"></script> | 18 | + | <script defer src="/assets/js/hyperscript.min.js"></script> |
| 19 | <script defer src="/assets/js/custom.js"></script> | 19 | <script defer src="/assets/js/custom.js"></script> | ||
| 20 | 20 | ||||
| 21 | <!-- daisyUI theme configuration --> | 21 | <!-- daisyUI theme configuration --> | ||
M internal/templating/models/templ/components/header_templ.go +1 -1 generated file: collapsed
M internal/templating/models/templ/components/imagelist_templ.go +11 -11
| @@ -61,7 +61,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 61 | 61 | if templ_7745c5c3_Err != nil { | |
| 62 | 62 | return templ_7745c5c3_Err | |
| 63 | 63 | } | |
| 64 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h3><div class=\"flex gap-2\"><button class=\"btn btn-sm btn-secondary\" hx-get=\"/htmx/upload-image\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0992</span> Upload</button> <button class=\"btn btn-sm btn-primary\" hx-get=\"/htmx/image-manager\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0453</span> Refresh</button> <button class=\"btn btn-sm btn-warning\" hx-post=\"/htmx/image-manager/prune\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\" hx-confirm=\"Are you sure you want to delete all unused (dangling and unreferenced) images? This cannot be undone.\"><span class=\"iconf\">\U000f0a79</span> Purge Unused</button></div></div><!-- Image list --><table class=\"table\"><!-- head --><thead><tr><th>Repository</th><th>Tag</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead> <tbody>") | |
| 64 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h3><div class=\"flex gap-2\"><button class=\"btn btn-sm btn-secondary\" hx-get=\"/htmx/upload-image\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0992</span> Upload</button> <button class=\"btn btn-sm btn-primary\" hx-get=\"/htmx/image-manager\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0453</span> Refresh</button> <button id=\"purge-button\" class=\"btn btn-sm btn-warning\" hx-post=\"/htmx/image-manager/prune\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\" hx-indicator=\"#purge-spinner\" hx-confirm=\"Are you sure you want to delete all unused (dangling and unreferenced) images? This cannot be undone.\"><span class=\"btn-content flex items-center\"><span class=\"iconf mr-1\">\U000f0a79</span> Purge Unused</span> <span class=\"loading loading-spinner loading-sm hidden\" id=\"purge-spinner\"></span></button></div></div><!-- Image list --><table class=\"table\"><!-- head --><thead><tr><th>Repository</th><th>Tag</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead> <tbody>") | |
| 65 | 65 | if templ_7745c5c3_Err != nil { | |
| 66 | 66 | return templ_7745c5c3_Err | |
| 67 | 67 | } | |
| @@ -79,7 +79,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 79 | 79 | var templ_7745c5c3_Var3 string | |
| 80 | 80 | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(img.Repository) | |
| 81 | 81 | if templ_7745c5c3_Err != nil { | |
| 82 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 86, Col: 50} | |
| 82 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 91, Col: 50} | |
| 83 | 83 | } | |
| 84 | 84 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) | |
| 85 | 85 | if templ_7745c5c3_Err != nil { | |
| @@ -92,7 +92,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 92 | 92 | var templ_7745c5c3_Var4 string | |
| 93 | 93 | templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(img.ID) | |
| 94 | 94 | if templ_7745c5c3_Err != nil { | |
| 95 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 87, Col: 51} | |
| 95 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 92, Col: 51} | |
| 96 | 96 | } | |
| 97 | 97 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) | |
| 98 | 98 | if templ_7745c5c3_Err != nil { | |
| @@ -105,7 +105,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 105 | 105 | var templ_7745c5c3_Var5 string | |
| 106 | 106 | templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(img.Tag) | |
| 107 | 107 | if templ_7745c5c3_Err != nil { | |
| 108 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 91, Col: 21} | |
| 108 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 96, Col: 21} | |
| 109 | 109 | } | |
| 110 | 110 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) | |
| 111 | 111 | if templ_7745c5c3_Err != nil { | |
| @@ -118,7 +118,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 118 | 118 | var templ_7745c5c3_Var6 string | |
| 119 | 119 | templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(img.Size) | |
| 120 | 120 | if templ_7745c5c3_Err != nil { | |
| 121 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 92, Col: 22} | |
| 121 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 97, Col: 22} | |
| 122 | 122 | } | |
| 123 | 123 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) | |
| 124 | 124 | if templ_7745c5c3_Err != nil { | |
| @@ -131,7 +131,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 131 | 131 | var templ_7745c5c3_Var7 string | |
| 132 | 132 | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(img.Created) | |
| 133 | 133 | if templ_7745c5c3_Err != nil { | |
| 134 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 93, Col: 25} | |
| 134 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 98, Col: 25} | |
| 135 | 135 | } | |
| 136 | 136 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) | |
| 137 | 137 | if templ_7745c5c3_Err != nil { | |
| @@ -144,7 +144,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 144 | 144 | var templ_7745c5c3_Var8 string | |
| 145 | 145 | templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", img.ShortID)))) | |
| 146 | 146 | if templ_7745c5c3_Err != nil { | |
| 147 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 103, Col: 94} | |
| 147 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 108, Col: 94} | |
| 148 | 148 | } | |
| 149 | 149 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) | |
| 150 | 150 | if templ_7745c5c3_Err != nil { | |
| @@ -157,7 +157,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 157 | 157 | var templ_7745c5c3_Var9 string | |
| 158 | 158 | templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("#create-form-target-" + img.ShortID) | |
| 159 | 159 | if templ_7745c5c3_Err != nil { | |
| 160 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 104, Col: 61} | |
| 160 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 109, Col: 61} | |
| 161 | 161 | } | |
| 162 | 162 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) | |
| 163 | 163 | if templ_7745c5c3_Err != nil { | |
| @@ -170,7 +170,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 170 | 170 | var templ_7745c5c3_Var10 string | |
| 171 | 171 | templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/image-manager/delete/%s", img.ShortID)))) | |
| 172 | 172 | if templ_7745c5c3_Err != nil { | |
| 173 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 113, Col: 101} | |
| 173 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 118, Col: 101} | |
| 174 | 174 | } | |
| 175 | 175 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) | |
| 176 | 176 | if templ_7745c5c3_Err != nil { | |
| @@ -183,7 +183,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 183 | 183 | var templ_7745c5c3_Var11 string | |
| 184 | 184 | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs("create-form-target-" + img.ShortID) | |
| 185 | 185 | if templ_7745c5c3_Err != nil { | |
| 186 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 125, Col: 64} | |
| 186 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 130, Col: 64} | |
| 187 | 187 | } | |
| 188 | 188 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | |
| 189 | 189 | if templ_7745c5c3_Err != nil { | |
| @@ -195,7 +195,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||
| 195 | 195 | } | |
| 196 | 196 | } | |
| 197 | 197 | } | |
| 198 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</tbody></table></div></div><script>\n\tconsole.debug('Image list rendered');\n\t</script>") | |
| 198 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</tbody></table></div></div><!-- Notification Area --><div id=\"image-prune-notifications\" class=\"fixed bottom-4 right-4 z-50 space-y-2\"></div><style>\n\t #purge-button.htmx-request .btn-content {\n\t display: none;\n\t }\n\t #purge-button.htmx-request #purge-spinner {\n\t display: inline-block;\n\t }\n\t</style><script>\n\tconsole.debug('Image list rendered');\n\n\tdocument.body.addEventListener('showMessage', function(evt) {\n\t\tconsole.debug('showMessage event received:', evt.detail);\n\t\tconst detail = evt.detail;\n\t\tconst notificationArea = document.getElementById('image-prune-notifications');\n\t\tif (!notificationArea || !detail || !detail.message) return;\n\n\t\tconst alertDiv = document.createElement('div');\n\t\tlet alertClass = 'alert-info'; // Default\n\t\tlet iconClass = 'ℹ️'; // Default icon\n\n\t\tif (detail.level === 'success') {\n\t\t\talertClass = 'alert-success';\n\t\t\ticonClass = '\U000f056c'; // Checkmark icon\n\t\t} else if (detail.level === 'error') {\n\t\t\talertClass = 'alert-error';\n\t\t\ticonClass = '\U000f015a'; // Error icon\n\t\t}\n\n\t\talertDiv.className = `alert ${alertClass} shadow-lg flex`;\n\t\talertDiv.innerHTML = `\n\t\t\t<span class=\"iconf text-xl\">${iconClass}</span>\n\t\t\t<span>${detail.message}</span>\n\t\t\t<button class=\"btn btn-sm btn-ghost ml-auto\" onclick=\"this.parentElement.remove()\">✕</button>\n\t\t`;\n\n\t\tnotificationArea.appendChild(alertDiv);\n\n\t\t// Automatically remove the alert after 5 seconds\n\t\tsetTimeout(() => {\n\t\t\talertDiv.remove();\n\t\t}, 5000);\n\t});\n\t</script>") | |
| 199 | 199 | if templ_7745c5c3_Err != nil { | |
| 200 | 200 | return templ_7745c5c3_Err | |
| 201 | 201 | } | |
| @@ -61,7 +61,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 61 | if templ_7745c5c3_Err != nil { | 61 | if templ_7745c5c3_Err != nil { | ||
| 62 | return templ_7745c5c3_Err | 62 | return templ_7745c5c3_Err | ||
| 63 | } | 63 | } | ||
| 64 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h3><div class=\"flex gap-2\"><button class=\"btn btn-sm btn-secondary\" hx-get=\"/htmx/upload-image\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0992</span> Upload</button> <button class=\"btn btn-sm btn-primary\" hx-get=\"/htmx/image-manager\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0453</span> Refresh</button> <button class=\"btn btn-sm btn-warning\" hx-post=\"/htmx/image-manager/prune\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\" hx-confirm=\"Are you sure you want to delete all unused (dangling and unreferenced) images? This cannot be undone.\"><span class=\"iconf\">\U000f0a79</span> Purge Unused</button></div></div><!-- Image list --><table class=\"table\"><!-- head --><thead><tr><th>Repository</th><th>Tag</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead> <tbody>") | 64 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h3><div class=\"flex gap-2\"><button class=\"btn btn-sm btn-secondary\" hx-get=\"/htmx/upload-image\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0992</span> Upload</button> <button class=\"btn btn-sm btn-primary\" hx-get=\"/htmx/image-manager\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0453</span> Refresh</button> <button id=\"purge-button\" class=\"btn btn-sm btn-warning\" hx-post=\"/htmx/image-manager/prune\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\" hx-indicator=\"#purge-spinner\" hx-confirm=\"Are you sure you want to delete all unused (dangling and unreferenced) images? This cannot be undone.\"><span class=\"btn-content flex items-center\"><span class=\"iconf mr-1\">\U000f0a79</span> Purge Unused</span> <span class=\"loading loading-spinner loading-sm hidden\" id=\"purge-spinner\"></span></button></div></div><!-- Image list --><table class=\"table\"><!-- head --><thead><tr><th>Repository</th><th>Tag</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead> <tbody>") |
| 65 | if templ_7745c5c3_Err != nil { | 65 | if templ_7745c5c3_Err != nil { | ||
| 66 | return templ_7745c5c3_Err | 66 | return templ_7745c5c3_Err | ||
| 67 | } | 67 | } | ||
| @@ -79,7 +79,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 79 | var templ_7745c5c3_Var3 string | 79 | var templ_7745c5c3_Var3 string | ||
| 80 | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(img.Repository) | 80 | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(img.Repository) | ||
| 81 | if templ_7745c5c3_Err != nil { | 81 | if templ_7745c5c3_Err != nil { | ||
| 82 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 86, Col: 50} | 82 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 91, Col: 50} |
| 83 | } | 83 | } | ||
| 84 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) | 84 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) | ||
| 85 | if templ_7745c5c3_Err != nil { | 85 | if templ_7745c5c3_Err != nil { | ||
| @@ -92,7 +92,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 92 | var templ_7745c5c3_Var4 string | 92 | var templ_7745c5c3_Var4 string | ||
| 93 | templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(img.ID) | 93 | templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(img.ID) | ||
| 94 | if templ_7745c5c3_Err != nil { | 94 | if templ_7745c5c3_Err != nil { | ||
| 95 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 87, Col: 51} | 95 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 92, Col: 51} |
| 96 | } | 96 | } | ||
| 97 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) | 97 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) | ||
| 98 | if templ_7745c5c3_Err != nil { | 98 | if templ_7745c5c3_Err != nil { | ||
| @@ -105,7 +105,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 105 | var templ_7745c5c3_Var5 string | 105 | var templ_7745c5c3_Var5 string | ||
| 106 | templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(img.Tag) | 106 | templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(img.Tag) | ||
| 107 | if templ_7745c5c3_Err != nil { | 107 | if templ_7745c5c3_Err != nil { | ||
| 108 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 91, Col: 21} | 108 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 96, Col: 21} |
| 109 | } | 109 | } | ||
| 110 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) | 110 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) | ||
| 111 | if templ_7745c5c3_Err != nil { | 111 | if templ_7745c5c3_Err != nil { | ||
| @@ -118,7 +118,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 118 | var templ_7745c5c3_Var6 string | 118 | var templ_7745c5c3_Var6 string | ||
| 119 | templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(img.Size) | 119 | templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(img.Size) | ||
| 120 | if templ_7745c5c3_Err != nil { | 120 | if templ_7745c5c3_Err != nil { | ||
| 121 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 92, Col: 22} | 121 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 97, Col: 22} |
| 122 | } | 122 | } | ||
| 123 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) | 123 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) | ||
| 124 | if templ_7745c5c3_Err != nil { | 124 | if templ_7745c5c3_Err != nil { | ||
| @@ -131,7 +131,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 131 | var templ_7745c5c3_Var7 string | 131 | var templ_7745c5c3_Var7 string | ||
| 132 | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(img.Created) | 132 | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(img.Created) | ||
| 133 | if templ_7745c5c3_Err != nil { | 133 | if templ_7745c5c3_Err != nil { | ||
| 134 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 93, Col: 25} | 134 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 98, Col: 25} |
| 135 | } | 135 | } | ||
| 136 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) | 136 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) | ||
| 137 | if templ_7745c5c3_Err != nil { | 137 | if templ_7745c5c3_Err != nil { | ||
| @@ -144,7 +144,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 144 | var templ_7745c5c3_Var8 string | 144 | var templ_7745c5c3_Var8 string | ||
| 145 | templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", img.ShortID)))) | 145 | templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", img.ShortID)))) | ||
| 146 | if templ_7745c5c3_Err != nil { | 146 | if templ_7745c5c3_Err != nil { | ||
| 147 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 103, Col: 94} | 147 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 108, Col: 94} |
| 148 | } | 148 | } | ||
| 149 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) | 149 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) | ||
| 150 | if templ_7745c5c3_Err != nil { | 150 | if templ_7745c5c3_Err != nil { | ||
| @@ -157,7 +157,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 157 | var templ_7745c5c3_Var9 string | 157 | var templ_7745c5c3_Var9 string | ||
| 158 | templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("#create-form-target-" + img.ShortID) | 158 | templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("#create-form-target-" + img.ShortID) | ||
| 159 | if templ_7745c5c3_Err != nil { | 159 | if templ_7745c5c3_Err != nil { | ||
| 160 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 104, Col: 61} | 160 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 109, Col: 61} |
| 161 | } | 161 | } | ||
| 162 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) | 162 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) | ||
| 163 | if templ_7745c5c3_Err != nil { | 163 | if templ_7745c5c3_Err != nil { | ||
| @@ -170,7 +170,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 170 | var templ_7745c5c3_Var10 string | 170 | var templ_7745c5c3_Var10 string | ||
| 171 | templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/image-manager/delete/%s", img.ShortID)))) | 171 | templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/image-manager/delete/%s", img.ShortID)))) | ||
| 172 | if templ_7745c5c3_Err != nil { | 172 | if templ_7745c5c3_Err != nil { | ||
| 173 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 113, Col: 101} | 173 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 118, Col: 101} |
| 174 | } | 174 | } | ||
| 175 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) | 175 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) | ||
| 176 | if templ_7745c5c3_Err != nil { | 176 | if templ_7745c5c3_Err != nil { | ||
| @@ -183,7 +183,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 183 | var templ_7745c5c3_Var11 string | 183 | var templ_7745c5c3_Var11 string | ||
| 184 | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs("create-form-target-" + img.ShortID) | 184 | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs("create-form-target-" + img.ShortID) | ||
| 185 | if templ_7745c5c3_Err != nil { | 185 | if templ_7745c5c3_Err != nil { | ||
| 186 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 125, Col: 64} | 186 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 130, Col: 64} |
| 187 | } | 187 | } | ||
| 188 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | 188 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | ||
| 189 | if templ_7745c5c3_Err != nil { | 189 | if templ_7745c5c3_Err != nil { | ||
| @@ -195,7 +195,7 @@ func ImageList(images []HumanReadableImage) templ.Component { | |||||
| 195 | } | 195 | } | ||
| 196 | } | 196 | } | ||
| 197 | } | 197 | } | ||
| 198 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</tbody></table></div></div><script>\n\tconsole.debug('Image list rendered');\n\t</script>") | 198 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</tbody></table></div></div><!-- Notification Area --><div id=\"image-prune-notifications\" class=\"fixed bottom-4 right-4 z-50 space-y-2\"></div><style>\n\t #purge-button.htmx-request .btn-content {\n\t display: none;\n\t }\n\t #purge-button.htmx-request #purge-spinner {\n\t display: inline-block;\n\t }\n\t</style><script>\n\tconsole.debug('Image list rendered');\n\n\tdocument.body.addEventListener('showMessage', function(evt) {\n\t\tconsole.debug('showMessage event received:', evt.detail);\n\t\tconst detail = evt.detail;\n\t\tconst notificationArea = document.getElementById('image-prune-notifications');\n\t\tif (!notificationArea || !detail || !detail.message) return;\n\n\t\tconst alertDiv = document.createElement('div');\n\t\tlet alertClass = 'alert-info'; // Default\n\t\tlet iconClass = 'ℹ️'; // Default icon\n\n\t\tif (detail.level === 'success') {\n\t\t\talertClass = 'alert-success';\n\t\t\ticonClass = '\U000f056c'; // Checkmark icon\n\t\t} else if (detail.level === 'error') {\n\t\t\talertClass = 'alert-error';\n\t\t\ticonClass = '\U000f015a'; // Error icon\n\t\t}\n\n\t\talertDiv.className = `alert ${alertClass} shadow-lg flex`;\n\t\talertDiv.innerHTML = `\n\t\t\t<span class=\"iconf text-xl\">${iconClass}</span>\n\t\t\t<span>${detail.message}</span>\n\t\t\t<button class=\"btn btn-sm btn-ghost ml-auto\" onclick=\"this.parentElement.remove()\">✕</button>\n\t\t`;\n\n\t\tnotificationArea.appendChild(alertDiv);\n\n\t\t// Automatically remove the alert after 5 seconds\n\t\tsetTimeout(() => {\n\t\t\talertDiv.remove();\n\t\t}, 5000);\n\t});\n\t</script>") |
| 199 | if templ_7745c5c3_Err != nil { | 199 | if templ_7745c5c3_Err != nil { | ||
| 200 | return templ_7745c5c3_Err | 200 | return templ_7745c5c3_Err | ||
| 201 | } | 201 | } | ||
M internal/templating/models/templ/pages/login/index.templ +4 -4
| @@ -13,13 +13,13 @@ templ LoginPage(title string, buildVersion string, errorMsg string) { | |||
| 13 | 13 | <!-- Custom CSS + Tailwind --> | |
| 14 | 14 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |
| 15 | 15 | <!-- HTMX min js --> | |
| 16 | - | <script src="/libs/htmx.min.js"></script> | |
| 16 | + | <script src="/assets/js/htmx.min.js"></script> | |
| 17 | 17 | <!-- HTMX SSE --> | |
| 18 | - | <script src="/libs/htmx-sse.js"></script> | |
| 18 | + | <script src="/assets/js/htmx-sse.js"></script> | |
| 19 | 19 | <!-- DaisyUI --> | |
| 20 | - | <script src="/libs/daisyui.js" type="module"></script> | |
| 20 | + | <script src="/assets/js/daisyui.js" type="module"></script> | |
| 21 | 21 | <!-- jQuery --> | |
| 22 | - | <script src="/libs/jquery.min.js"></script> | |
| 22 | + | <script src="/assets/js/jquery.min.js"></script> | |
| 23 | 23 | <!-- Custom JS from assets/js --> | |
| 24 | 24 | <script src="/assets/js/custom.js" defer></script> | |
| 25 | 25 | ||
| @@ -13,13 +13,13 @@ templ LoginPage(title string, buildVersion string, errorMsg string) { | |||||
| 13 | <!-- Custom CSS + Tailwind --> | 13 | <!-- Custom CSS + Tailwind --> | ||
| 14 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | 14 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | ||
| 15 | <!-- HTMX min js --> | 15 | <!-- HTMX min js --> | ||
| 16 | - | <script src="/libs/htmx.min.js"></script> | 16 | + | <script src="/assets/js/htmx.min.js"></script> |
| 17 | <!-- HTMX SSE --> | 17 | <!-- HTMX SSE --> | ||
| 18 | - | <script src="/libs/htmx-sse.js"></script> | 18 | + | <script src="/assets/js/htmx-sse.js"></script> |
| 19 | <!-- DaisyUI --> | 19 | <!-- DaisyUI --> | ||
| 20 | - | <script src="/libs/daisyui.js" type="module"></script> | 20 | + | <script src="/assets/js/daisyui.js" type="module"></script> |
| 21 | <!-- jQuery --> | 21 | <!-- jQuery --> | ||
| 22 | - | <script src="/libs/jquery.min.js"></script> | 22 | + | <script src="/assets/js/jquery.min.js"></script> |
| 23 | <!-- Custom JS from assets/js --> | 23 | <!-- Custom JS from assets/js --> | ||
| 24 | <script src="/assets/js/custom.js" defer></script> | 24 | <script src="/assets/js/custom.js" defer></script> | ||
| 25 | 25 | ||||
M internal/templating/models/templ/pages/login/index_templ.go +1 -1 generated file: collapsed
A internal/templating/render/html.go +77 -0
| @@ -0,0 +1,77 @@ | |||
| 1 | + | package render | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "fmt" | |
| 6 | + | "html/template" | |
| 7 | + | "io/fs" | |
| 8 | + | "path" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/gordon/internal/server" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | type Renderer struct { | |
| 14 | + | Template *template.Template | |
| 15 | + | ParseError error | |
| 16 | + | BuildVersion string | |
| 17 | + | LatestVersion string | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // Render function renders the template with the given data | |
| 21 | + | func (r *Renderer) Render(data interface{}, a *server.App) (string, error) { | |
| 22 | + | if r.ParseError != nil { | |
| 23 | + | return "", fmt.Errorf("failed to parse template: %w", r.ParseError) | |
| 24 | + | ||
| 25 | + | } | |
| 26 | + | if r.Template == nil { | |
| 27 | + | return "", fmt.Errorf("template is nil") | |
| 28 | + | } | |
| 29 | + | // Type assert data to map[string]interface{} to add BuildVersion | |
| 30 | + | dataMap, ok := data.(map[string]interface{}) | |
| 31 | + | if !ok { | |
| 32 | + | return "", fmt.Errorf("data is not a map[string]interface{}") | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // Automatically add BuildVersion to the data map | |
| 36 | + | dataMap["BuildVersion"] = r.BuildVersion | |
| 37 | + | buf := new(bytes.Buffer) | |
| 38 | + | err := r.Template.Execute(buf, dataMap) | |
| 39 | + | if err != nil { | |
| 40 | + | return "", fmt.Errorf("failed to execute template: %w", err) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | return buf.String(), nil | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // GetHTMLRenderer function returns a new Renderer instance | |
| 47 | + | func GetHTMLRenderer(mainPath string, filename string, fs fs.FS, a *server.App, fragmentsPath ...string) (*Renderer, error) { | |
| 48 | + | // Full path to the main template | |
| 49 | + | fullPath := path.Join(mainPath, filename) | |
| 50 | + | // Check if the file exists in the provided fs.FS using fs.Open | |
| 51 | + | file, err := fs.Open(fullPath) | |
| 52 | + | if err != nil { | |
| 53 | + | return nil, fmt.Errorf("failed to open %s: %w", filename, err) | |
| 54 | + | } | |
| 55 | + | file.Close() | |
| 56 | + | ||
| 57 | + | // Parse the main template | |
| 58 | + | tmpl, err := template.New(filename).ParseFS(fs, fullPath) | |
| 59 | + | if err != nil { | |
| 60 | + | return nil, fmt.Errorf("failed to parse %s: %w", filename, err) | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // If a fragments path is provided, parse the fragment templates | |
| 64 | + | if len(fragmentsPath) > 0 && fragmentsPath[0] != "" { | |
| 65 | + | fragmentsGlob := path.Join(fragmentsPath[0], "*.gohtml") | |
| 66 | + | _, err = tmpl.ParseFS(fs, fragmentsGlob) | |
| 67 | + | if err != nil { | |
| 68 | + | return nil, fmt.Errorf("failed to parse fragments: %w", err) | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // Return the Renderer instance | |
| 73 | + | return &Renderer{ | |
| 74 | + | Template: tmpl, | |
| 75 | + | BuildVersion: a.Config.Build.BuildVersion, | |
| 76 | + | }, nil | |
| 77 | + | } | |
| @@ -0,0 +1,77 @@ | |||||
| 1 | + | package render | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "fmt" | |||
| 6 | + | "html/template" | |||
| 7 | + | "io/fs" | |||
| 8 | + | "path" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/gordon/internal/server" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | type Renderer struct { | |||
| 14 | + | Template *template.Template | |||
| 15 | + | ParseError error | |||
| 16 | + | BuildVersion string | |||
| 17 | + | LatestVersion string | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // Render function renders the template with the given data | |||
| 21 | + | func (r *Renderer) Render(data interface{}, a *server.App) (string, error) { | |||
| 22 | + | if r.ParseError != nil { | |||
| 23 | + | return "", fmt.Errorf("failed to parse template: %w", r.ParseError) | |||
| 24 | + | ||||
| 25 | + | } | |||
| 26 | + | if r.Template == nil { | |||
| 27 | + | return "", fmt.Errorf("template is nil") | |||
| 28 | + | } | |||
| 29 | + | // Type assert data to map[string]interface{} to add BuildVersion | |||
| 30 | + | dataMap, ok := data.(map[string]interface{}) | |||
| 31 | + | if !ok { | |||
| 32 | + | return "", fmt.Errorf("data is not a map[string]interface{}") | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // Automatically add BuildVersion to the data map | |||
| 36 | + | dataMap["BuildVersion"] = r.BuildVersion | |||
| 37 | + | buf := new(bytes.Buffer) | |||
| 38 | + | err := r.Template.Execute(buf, dataMap) | |||
| 39 | + | if err != nil { | |||
| 40 | + | return "", fmt.Errorf("failed to execute template: %w", err) | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | return buf.String(), nil | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // GetHTMLRenderer function returns a new Renderer instance | |||
| 47 | + | func GetHTMLRenderer(mainPath string, filename string, fs fs.FS, a *server.App, fragmentsPath ...string) (*Renderer, error) { | |||
| 48 | + | // Full path to the main template | |||
| 49 | + | fullPath := path.Join(mainPath, filename) | |||
| 50 | + | // Check if the file exists in the provided fs.FS using fs.Open | |||
| 51 | + | file, err := fs.Open(fullPath) | |||
| 52 | + | if err != nil { | |||
| 53 | + | return nil, fmt.Errorf("failed to open %s: %w", filename, err) | |||
| 54 | + | } | |||
| 55 | + | file.Close() | |||
| 56 | + | ||||
| 57 | + | // Parse the main template | |||
| 58 | + | tmpl, err := template.New(filename).ParseFS(fs, fullPath) | |||
| 59 | + | if err != nil { | |||
| 60 | + | return nil, fmt.Errorf("failed to parse %s: %w", filename, err) | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | // If a fragments path is provided, parse the fragment templates | |||
| 64 | + | if len(fragmentsPath) > 0 && fragmentsPath[0] != "" { | |||
| 65 | + | fragmentsGlob := path.Join(fragmentsPath[0], "*.gohtml") | |||
| 66 | + | _, err = tmpl.ParseFS(fs, fragmentsGlob) | |||
| 67 | + | if err != nil { | |||
| 68 | + | return nil, fmt.Errorf("failed to parse fragments: %w", err) | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // Return the Renderer instance | |||
| 73 | + | return &Renderer{ | |||
| 74 | + | Template: tmpl, | |||
| 75 | + | BuildVersion: a.Config.Build.BuildVersion, | |||
| 76 | + | }, nil | |||
| 77 | + | } | |||
M internal/templating/render/localization.go +20 -42
| @@ -4,55 +4,33 @@ import ( | |||
| 4 | 4 | "fmt" | |
| 5 | 5 | ||
| 6 | 6 | "github.com/bnema/gordon/internal/server" | |
| 7 | - | "gopkg.in/yaml.v3" | |
| 8 | 7 | ) | |
| 9 | 8 | ||
| 10 | - | type Localizations map[string]map[string]map[string]interface{} | |
| 11 | - | ||
| 12 | - | // GetLocalization function returns the localization for the given language | |
| 9 | + | // GetLocalization function returns the localization map for the given language | |
| 13 | 10 | func GetLocalization(lang string, a *server.App) (map[string]interface{}, error) { | |
| 14 | - | // 1. Check if the language is supported | |
| 15 | - | supportedLanguages := []string{"en", "fr"} // Add more languages here | |
| 16 | - | if !contains(supportedLanguages, lang) { | |
| 17 | - | return nil, fmt.Errorf("language %s is not supported", lang) | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | // 2. Get the localizations from the strings.yml file | |
| 21 | - | localizations, err := getLocalizations(a) | |
| 22 | - | if err != nil { | |
| 23 | - | return nil, fmt.Errorf("failed to get localizations: %w", err) | |
| 24 | - | } | |
| 25 | - | ||
| 26 | - | // 3. Return the localizations for the given language | |
| 27 | - | if localization, ok := localizations[lang]; ok { | |
| 28 | - | result := make(map[string]interface{}) | |
| 29 | - | for k, v := range localization { | |
| 30 | - | result[k] = v | |
| 11 | + | // 1. Check if the language exists in the pre-parsed map | |
| 12 | + | langData, langExists := a.Strings[lang] | |
| 13 | + | if !langExists { | |
| 14 | + | // Fallback to default language (e.g., "en") if primary is not found | |
| 15 | + | defaultLang := "en" | |
| 16 | + | langData, langExists = a.Strings[defaultLang] | |
| 17 | + | if !langExists { | |
| 18 | + | return nil, fmt.Errorf("default language '%s' not found in localization data", defaultLang) | |
| 31 | 19 | } | |
| 32 | - | return result, nil | |
| 20 | + | // Use default language if primary was not found | |
| 21 | + | lang = defaultLang | |
| 22 | + | fmt.Printf("Warning: Language '%s' not found, falling back to '%s'\n", lang, defaultLang) | |
| 33 | 23 | } | |
| 34 | 24 | ||
| 35 | - | return nil, fmt.Errorf("language %s is not available", lang) | |
| 36 | - | } | |
| 37 | - | ||
| 38 | - | // getLocalizations function returns the localizations from the strings.yml file | |
| 39 | - | func getLocalizations(a *server.App) (Localizations, error) { | |
| 40 | - | var loc Localizations | |
| 41 | - | ||
| 42 | - | err := yaml.Unmarshal(a.LocYML, &loc) | |
| 43 | - | if err != nil { | |
| 44 | - | return loc, fmt.Errorf("failed to unmarshal strings.yml: %w", err) | |
| 25 | + | // 2. Assert that the language data is a map | |
| 26 | + | localizationMap, ok := langData.(map[string]interface{}) | |
| 27 | + | if !ok { | |
| 28 | + | return nil, fmt.Errorf("localization data for language '%s' is not in the expected map format", lang) | |
| 45 | 29 | } | |
| 46 | 30 | ||
| 47 | - | return loc, nil | |
| 31 | + | // 3. Return the localization map for the requested (or fallback) language | |
| 32 | + | return localizationMap, nil | |
| 48 | 33 | } | |
| 49 | 34 | ||
| 50 | - | // Helper function to check if a slice contains a string | |
| 51 | - | func contains(slice []string, str string) bool { | |
| 52 | - | for _, v := range slice { | |
| 53 | - | if v == str { | |
| 54 | - | return true | |
| 55 | - | } | |
| 56 | - | } | |
| 57 | - | return false | |
| 58 | - | } | |
| 35 | + | // Helper function to check if a slice contains a string - No longer needed as we check map keys | |
| 36 | + | // func contains(slice []string, str string) bool { ... } | |
| @@ -4,55 +4,33 @@ import ( | |||||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | 5 | ||||
| 6 | "github.com/bnema/gordon/internal/server" | 6 | "github.com/bnema/gordon/internal/server" | ||
| 7 | - | "gopkg.in/yaml.v3" | |||
| 8 | ) | 7 | ) | ||
| 9 | 8 | ||||
| 10 | - | type Localizations map[string]map[string]map[string]interface{} | 9 | + | // GetLocalization function returns the localization map for the given language |
| 11 | - | ||||
| 12 | - | // GetLocalization function returns the localization for the given language | |||
| 13 | func GetLocalization(lang string, a *server.App) (map[string]interface{}, error) { | 10 | func GetLocalization(lang string, a *server.App) (map[string]interface{}, error) { | ||
| 14 | - | // 1. Check if the language is supported | 11 | + | // 1. Check if the language exists in the pre-parsed map |
| 15 | - | supportedLanguages := []string{"en", "fr"} // Add more languages here | 12 | + | langData, langExists := a.Strings[lang] |
| 16 | - | if !contains(supportedLanguages, lang) { | 13 | + | if !langExists { |
| 17 | - | return nil, fmt.Errorf("language %s is not supported", lang) | 14 | + | // Fallback to default language (e.g., "en") if primary is not found |
| 18 | - | } | 15 | + | defaultLang := "en" |
| 19 | - | 16 | + | langData, langExists = a.Strings[defaultLang] | |
| 20 | - | // 2. Get the localizations from the strings.yml file | 17 | + | if !langExists { |
| 21 | - | localizations, err := getLocalizations(a) | 18 | + | return nil, fmt.Errorf("default language '%s' not found in localization data", defaultLang) |
| 22 | - | if err != nil { | |||
| 23 | - | return nil, fmt.Errorf("failed to get localizations: %w", err) | |||
| 24 | - | } | |||
| 25 | - | ||||
| 26 | - | // 3. Return the localizations for the given language | |||
| 27 | - | if localization, ok := localizations[lang]; ok { | |||
| 28 | - | result := make(map[string]interface{}) | |||
| 29 | - | for k, v := range localization { | |||
| 30 | - | result[k] = v | |||
| 31 | } | 19 | } | ||
| 32 | - | return result, nil | 20 | + | // Use default language if primary was not found |
| 21 | + | lang = defaultLang | |||
| 22 | + | fmt.Printf("Warning: Language '%s' not found, falling back to '%s'\n", lang, defaultLang) | |||
| 33 | } | 23 | } | ||
| 34 | 24 | ||||
| 35 | - | return nil, fmt.Errorf("language %s is not available", lang) | 25 | + | // 2. Assert that the language data is a map |
| 36 | - | } | 26 | + | localizationMap, ok := langData.(map[string]interface{}) |
| 37 | - | 27 | + | if !ok { | |
| 38 | - | // getLocalizations function returns the localizations from the strings.yml file | 28 | + | return nil, fmt.Errorf("localization data for language '%s' is not in the expected map format", lang) |
| 39 | - | func getLocalizations(a *server.App) (Localizations, error) { | |||
| 40 | - | var loc Localizations | |||
| 41 | - | ||||
| 42 | - | err := yaml.Unmarshal(a.LocYML, &loc) | |||
| 43 | - | if err != nil { | |||
| 44 | - | return loc, fmt.Errorf("failed to unmarshal strings.yml: %w", err) | |||
| 45 | } | 29 | } | ||
| 46 | 30 | ||||
| 47 | - | return loc, nil | 31 | + | // 3. Return the localization map for the requested (or fallback) language |
| 32 | + | return localizationMap, nil | |||
| 48 | } | 33 | } | ||
| 49 | 34 | ||||
| 50 | - | // Helper function to check if a slice contains a string | 35 | + | // Helper function to check if a slice contains a string - No longer needed as we check map keys |
| 51 | - | func contains(slice []string, str string) bool { | 36 | + | // func contains(slice []string, str string) bool { ... } |
| 52 | - | for _, v := range slice { | |||
| 53 | - | if v == str { | |||
| 54 | - | return true | |||
| 55 | - | } | |||
| 56 | - | } | |||
| 57 | - | return false | |||
| 58 | - | } | |||
D internal/webui/embed.go +0 -14
| @@ -1,14 +0,0 @@ | |||
| 1 | - | package webui | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "embed" | |
| 5 | - | ||
| 6 | - | "github.com/labstack/echo/v4" | |
| 7 | - | ) | |
| 8 | - | ||
| 9 | - | // Embedding the public directory | |
| 10 | - | // | |
| 11 | - | //go:embed public/* | |
| 12 | - | var public embed.FS | |
| 13 | - | ||
| 14 | - | var PublicFS = echo.MustSubFS(public, "public") | |
| @@ -1,14 +0,0 @@ | |||||
| 1 | - | package webui | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "embed" | |||
| 5 | - | ||||
| 6 | - | "github.com/labstack/echo/v4" | |||
| 7 | - | ) | |||
| 8 | - | ||||
| 9 | - | // Embedding the public directory | |||
| 10 | - | // | |||
| 11 | - | //go:embed public/* | |||
| 12 | - | var public embed.FS | |||
| 13 | - | ||||
| 14 | - | var PublicFS = echo.MustSubFS(public, "public") | |||
M internal/webui/public/assets/css/tailwind.css +6 -0
| @@ -3484,6 +3484,9 @@ | |||
| 3484 | 3484 | font-size: 0.625rem; | |
| 3485 | 3485 | } | |
| 3486 | 3486 | } | |
| 3487 | + | .bottom-4 { | |
| 3488 | + | bottom: calc(var(--spacing) * 4); | |
| 3489 | + | } | |
| 3487 | 3490 | .file-input { | |
| 3488 | 3491 | cursor: pointer; | |
| 3489 | 3492 | cursor: pointer; | |
| @@ -4626,6 +4629,9 @@ | |||
| 4626 | 4629 | .ml-10 { | |
| 4627 | 4630 | margin-left: calc(var(--spacing) * 10); | |
| 4628 | 4631 | } | |
| 4632 | + | .ml-auto { | |
| 4633 | + | margin-left: auto; | |
| 4634 | + | } | |
| 4629 | 4635 | .carousel-item { | |
| 4630 | 4636 | box-sizing: content-box; | |
| 4631 | 4637 | display: flex; | |
| @@ -3484,6 +3484,9 @@ | |||||
| 3484 | font-size: 0.625rem; | 3484 | font-size: 0.625rem; | ||
| 3485 | } | 3485 | } | ||
| 3486 | } | 3486 | } | ||
| 3487 | + | .bottom-4 { | |||
| 3488 | + | bottom: calc(var(--spacing) * 4); | |||
| 3489 | + | } | |||
| 3487 | .file-input { | 3490 | .file-input { | ||
| 3488 | cursor: pointer; | 3491 | cursor: pointer; | ||
| 3489 | cursor: pointer; | 3492 | cursor: pointer; | ||
| @@ -4626,6 +4629,9 @@ | |||||
| 4626 | .ml-10 { | 4629 | .ml-10 { | ||
| 4627 | margin-left: calc(var(--spacing) * 10); | 4630 | margin-left: calc(var(--spacing) * 10); | ||
| 4628 | } | 4631 | } | ||
| 4632 | + | .ml-auto { | |||
| 4633 | + | margin-left: auto; | |||
| 4634 | + | } | |||
| 4629 | .carousel-item { | 4635 | .carousel-item { | ||
| 4630 | box-sizing: content-box; | 4636 | box-sizing: content-box; | ||
| 4631 | display: flex; | 4637 | display: flex; | ||