diff --git a/.env.example b/.env.example index b5393aac..d29ab73a 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,10 @@ PBW_LISTEN_HOST="" # The port on which the pgbackweb will listen for incoming HTTP requests. PBW_LISTEN_PORT="" +# Path prefix to use for all routes. If you set this to e.g. "/pgbackweb", +# the web interface will be available at http://:/pgbackweb +PBW_PATH_PREFIX="" + # Your timezone, this impacts logging, backup filenames and default timezone # in the web interface. TZ="" diff --git a/.github/workflows/docs-deploy.yaml b/.github/workflows/docs-deploy.yaml new file mode 100644 index 00000000..177d4dfa --- /dev/null +++ b/.github/workflows/docs-deploy.yaml @@ -0,0 +1,28 @@ +name: Deploy Documentation + +on: + push: + branches: + - main + paths: + - "docs/**" + +jobs: + deploy: + name: Deploy to Cloudflare Pages + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Deploy + uses: cloudflare/wrangler-action@v3 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + command: pages deploy ./docs --project-name=ufobackup + gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.prettierignore b/.prettierignore index 5d7ceb50..cd0dc420 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ internal/view/static/libs/ +internal/view/static/build/ diff --git a/README.md b/README.md index 805867d9..dff9de87 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ 🐘 Effortless PostgreSQL backups with a user-friendly web interface! 🌐💾

+

CI Status @@ -28,10 +29,16 @@

+> [!NOTE] +> **We're growing! New name, bigger future** +> +> PG Back Web is becoming **UFO Backup**! The new name reflects a future where the project expands beyond PostgreSQL, making powerful backups simple and accessible for everyone +> +> Curious about the roadmap or want to shape the project's future? Join the [community](https://ufobackup.uforg.dev/r/community) to discuss ideas and influence decisions, everyone's input is welcome! + ## Why PG Back Web? -PG Back Web isn't just another backup tool. It's your trusted ally in ensuring -the security and availability of your PostgreSQL data: +PG Back Web isn't just another backup tool. It's your trusted ally in ensuring the security and availability of your PostgreSQL data: - 🎯 **Designed for everyone**: From individual developers to teams. - ⏱️ **Save time**: Automate your backups and forget about manual tasks. @@ -39,34 +46,23 @@ the security and availability of your PostgreSQL data: ## Features -- 📦 **Intuitive web interface**: Manage your backups with ease, no database - expertise required. -- 📅 **Scheduled backups**: Set it and forget it. PG Back Web takes care of the - rest. -- 📈 **Backup monitoring**: Visualize the status of your backups with execution - logs. -- 📤 **Instant download & restore**: Restore and download your backups when you - need them, directly from the web interface. -- 🖥 **Multi-version support**: Compatible with PostgreSQL 13, 14, 15, 16, - 17, and 18. -- 📁 **Local & S3 storage**: Store backups locally or add as many S3 buckets as - you want for greater flexibility. -- ❤️‍🩹 **Health checks**: Automatically check the health of your databases and - destinations. -- 🔔 **Webhooks**: Get notified when a backup finishes, failed, health check - fails, or other events. +- 📦 **Intuitive web interface**: Manage your backups with ease, no database expertise required. +- 📅 **Scheduled backups**: Set it and forget it. PG Back Web takes care of the rest. +- 📈 **Backup monitoring**: Visualize the status of your backups with execution logs. +- 📤 **Instant download & restore**: Restore and download your backups when you need them, directly from the web interface. +- 🖥 **Multi-version support**: Compatible with PostgreSQL 13, 14, 15, 16, 17, and 18. +- 📁 **Local & S3 storage**: Store backups locally or add as many S3 buckets as you want for greater flexibility. +- ❤️‍🩹 **Health checks**: Automatically check the health of your databases and destinations. +- 🔔 **Webhooks**: Get notified when a backup finishes, failed, health check fails, or other events. - 🔒 **Security first**: PGP encryption to protect your sensitive information. -- 🛡️ **Open-source trust**: Open-source code under AGPL v3 license, backed by the - robust pg_dump tool. +- 🛡️ **Open-source trust**: Open-source code under AGPL v3 license, backed by the robust pg_dump tool. - 🌚 **Dark mode**: Because we all love dark mode. ## Installation -PG Back Web is available as a Docker image. You just need to set 3 environment -variables and you're good to go! +PG Back Web is available as a Docker image. You just need to set 3 environment variables and you're good to go! -Here's an example of how you can run PG Back Web with Docker Compose, feel free -to adapt it to your needs: +Here's an example of how you can run PG Back Web with Docker Compose, feel free to adapt it to your needs: ```yaml services: @@ -77,15 +73,15 @@ services: volumes: - ./backups:/backups # If you only use S3 destinations, you don't need this volume environment: + # Optional environment variables are ignored, see the configuration section below for more details PBW_ENCRYPTION_KEY: "my_secret_key" # Change this to a strong key PBW_POSTGRES_CONN_STRING: "postgresql://postgres:password@postgres:5432/pgbackweb?sslmode=disable" - TZ: "America/Guatemala" # Set your timezone, optional depends_on: postgres: condition: service_healthy postgres: - image: postgres:17 + image: postgres:18 environment: POSTGRES_USER: postgres POSTGRES_DB: pgbackweb @@ -101,28 +97,23 @@ services: retries: 5 ``` -You can watch [this youtube video](https://www.youtube.com/watch?v=vf7SLrSO8sw) -to see how easy it is to set up PG Back Web. +You can watch [this youtube video](https://www.youtube.com/watch?v=vf7SLrSO8sw) to see how easy it is to set up PG Back Web. ## Configuration You only need to configure the following environment variables: -- `PBW_ENCRYPTION_KEY`: Your encryption key. Generate a strong one and store it - in a safe place, as PG Back Web uses it to encrypt sensitive data. +- `PBW_ENCRYPTION_KEY`: Your encryption key. Generate a strong random one and store it in a safe place, as PG Back Web uses it to encrypt sensitive data. + +- `PBW_POSTGRES_CONN_STRING`: The connection string for the PostgreSQL database that will store PG Back Web data. -- `PBW_POSTGRES_CONN_STRING`: The connection string for the PostgreSQL database - that will store PG Back Web data. +- `PBW_LISTEN_HOST`: Optional. Host for the server to listen on, default 0.0.0.0 -- `PBW_LISTEN_HOST`: Host for the server to listen on, default 0.0.0.0 - (optional) +- `PBW_LISTEN_PORT`: Optional. Port for the server to listen on, default 8085 -- `PBW_LISTEN_PORT`: Port for the server to listen on, default 8085 (optional) +- `PBW_PATH_PREFIX`: Optional. Path prefix for the application URL. Use this when you want to serve the application under a subpath (e.g., `/pgbackweb`). Must start with `/` and not end with `/`. Default is empty. -- `TZ`: Your - [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) - (optional). Default is `UTC`. This impacts logging, backup filenames and - default timezone in the web interface. +- `TZ`: Optional. Your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). Default is `UTC`. This impacts logging, backup filenames and default timezone in the web interface. ## Screenshot @@ -130,20 +121,17 @@ You only need to configure the following environment variables: ## Reset password -You can reset your PG Back Web password by running the following command in the -server where PG Back Web is running: +You can reset your PG Back Web password by running the following command in the server where PG Back Web is running: ```bash docker exec -it sh -c change-password ``` -You should replace `` with the name or ID of the PG Back -Web container, then just follow the instructions. +You should replace `` with the name or ID of the PG Back Web container, then just follow the instructions. ## Next steps -In this link you can see a list of features that have been confirmed for future -updates: +In this link you can see a list of features that have been confirmed for future updates: Next steps ⏭️ @@ -151,11 +139,7 @@ updates: ## Sponsors -🙏 Thank you to the incredible sponsors for supporting this project! Your -contributions help keep PG Back Web running and growing. If you'd like to join -and become a sponsor, please visit the -[sponsorship page](https://buymeacoffee.com/eduardolat) and be part of something -great! 🚀 +🙏 Thank you to the incredible sponsors for supporting this project! Your contributions help keep PG Back Web running and growing. If you'd like to join and become a sponsor, please visit the [sponsorship page](https://buymeacoffee.com/eduardolat) and be part of something great! 🚀 ### 🥇 Gold Sponsors @@ -208,8 +192,7 @@ great! 🚀 ## Join the Community -Got ideas to improve PG Back Web? Contribute to the project! Every suggestion -and pull request is welcome. +Got ideas to improve PG Back Web? Contribute to the project! Every suggestion and pull request is welcome. ## License @@ -217,6 +200,4 @@ This project is 100% open source and is licensed under the AGPL v3 License - see --- -💖 **Love PG Back Web?** Give us a ⭐ on GitHub and share the project with your -colleagues. Together, we can make PostgreSQL backups more accessible to -everyone! +💖 **Love PG Back Web?** Give us a ⭐ on GitHub and share the project with your colleagues. Together, we can make PostgreSQL backups more accessible to everyone! diff --git a/assets/logos/hetzner-horizontal.png b/assets/logos/hetzner-horizontal.png new file mode 100644 index 00000000..6731f30b Binary files /dev/null and b/assets/logos/hetzner-horizontal.png differ diff --git a/assets/logos/hetzner.png b/assets/logos/hetzner.png new file mode 100644 index 00000000..04939f69 Binary files /dev/null and b/assets/logos/hetzner.png differ diff --git a/assets/support-project-v1.json b/assets/support-project-v1.json index d844e45b..d133c114 100644 --- a/assets/support-project-v1.json +++ b/assets/support-project-v1.json @@ -6,6 +6,12 @@ "link": "https://app.hapi.trade/rewards?code=LUIJER5", "description": "Join Hapi to easily buy stocks and crypto! Enjoy a 100% chance to win-win up to $500 in crypto rewards in your first deposit!" }, + { + "name": "Hetzner", + "logo": "https://raw.githubusercontent.com/eduardolat/pgbackweb/refs/heads/develop/assets/logos/hetzner-horizontal.png", + "link": "https://hetzner.cloud/?ref=TdOypLgK8yGR", + "description": "Get started with Hetzner Cloud today and enjoy €20 in free credits just for signing up through this link! Their powerful cloud servers offer excellent performance at competitive prices. After you spend your first €10, I'll receive €10 in credits too, making it a win-win for both of us!" + }, { "name": "Digital Ocean", "logo": "/images/third-party/digital-ocean.png", @@ -29,15 +35,15 @@ } ], "silver": [ - { - "name": "FetchGoat - Simplifying Logistics", - "logo": "https://raw.githubusercontent.com/eduardolat/pgbackweb/refs/heads/develop/assets/sponsors/FetchGoat.png", - "link": "https://fetchgoat.com?utm_source=pgbackweb&utm_medium=referral&utm_campaign=sponsorship" - }, { "name": "Become a silver sponsor", "logo": "/images/plus.png", "link": "https://buymeacoffee.com/eduardolat" + }, + { + "name": "FetchGoat - Simplifying Logistics", + "logo": "https://raw.githubusercontent.com/eduardolat/pgbackweb/refs/heads/develop/assets/sponsors/FetchGoat.png", + "link": "https://fetchgoat.com?utm_source=pgbackweb&utm_medium=referral&utm_campaign=sponsorship" } ], "bronze": [ diff --git a/cmd/app/main.go b/cmd/app/main.go index 632deec9..cbb179c8 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/eduardolat/pgbackweb/internal/config" "github.com/eduardolat/pgbackweb/internal/cron" "github.com/eduardolat/pgbackweb/internal/database" @@ -8,6 +10,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/integration" "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/service" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view" "github.com/labstack/echo/v4" ) @@ -18,6 +21,8 @@ func main() { logger.FatalError("error getting environment variables", logger.KV{"error": err}) } + pathutil.SetPathPrefix(env.PBW_PATH_PREFIX) + cr, err := cron.New() if err != nil { logger.FatalError("error initializing cron scheduler", logger.KV{"error": err}) @@ -46,8 +51,9 @@ func main() { app.HidePort = true view.MountRouter(app, servs) - address := env.PBW_LISTEN_HOST + ":" + env.PBW_LISTEN_PORT - logger.Info("server started at http://localhost:"+env.PBW_LISTEN_PORT, logger.KV{ + address := fmt.Sprintf("%s:%s", env.PBW_LISTEN_HOST, env.PBW_LISTEN_PORT) + localURL := fmt.Sprintf("http://localhost:%s%s", env.PBW_LISTEN_PORT, pathutil.GetPathPrefix()) + logger.Info("server started at "+localURL, logger.KV{ "listenHost": env.PBW_LISTEN_HOST, "listenPort": env.PBW_LISTEN_PORT, }) diff --git a/docs/_redirects b/docs/_redirects new file mode 100644 index 00000000..90df44f9 --- /dev/null +++ b/docs/_redirects @@ -0,0 +1,15 @@ +# Project links +/r/community https://ufobackup.uforg.dev +/r/discord https://discord.gg/BmAwq29UZ8 +/r/reddit https://www.reddit.com/r/ufobackup +/r/twitter https://x.com/eduardoolat +/r/x https://x.com/eduardoolat +/r/github https://github.com/eduardolat/pgbackweb +/r/gh https://github.com/eduardolat/pgbackweb + +# Author links +/r/author/web https://eduardo.lat +/r/author/gh https://eduardo.lat/github +/r/author/linkedin https://eduardo.lat/linkedin +/r/author/twitter https://x.com/eduardoolat +/r/author/x https://x.com/eduardoolat diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..428bd2a7 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,132 @@ + + + + + + UFO Backup - Community + + + + + + + + + + + diff --git a/internal/config/env.go b/internal/config/env.go index b4067ee7..31d39852 100644 --- a/internal/config/env.go +++ b/internal/config/env.go @@ -12,6 +12,7 @@ type Env struct { PBW_POSTGRES_CONN_STRING string `env:"PBW_POSTGRES_CONN_STRING,required"` PBW_LISTEN_HOST string `env:"PBW_LISTEN_HOST" envDefault:"0.0.0.0"` PBW_LISTEN_PORT string `env:"PBW_LISTEN_PORT" envDefault:"8085"` + PBW_PATH_PREFIX string `env:"PBW_PATH_PREFIX" envDefault:""` } var ( diff --git a/internal/config/env_validate.go b/internal/config/env_validate.go index b538a8b1..07aa366d 100644 --- a/internal/config/env_validate.go +++ b/internal/config/env_validate.go @@ -16,5 +16,9 @@ func validateEnv(env Env) error { return fmt.Errorf("invalid listen port %s, valid values are 1-65535", env.PBW_LISTEN_PORT) } + if !validate.PathPrefix(env.PBW_PATH_PREFIX) { + return fmt.Errorf("invalid path prefix %s, must start with / and not end with / (or be empty)", env.PBW_PATH_PREFIX) + } + return nil } diff --git a/internal/util/pathutil/pathutil.go b/internal/util/pathutil/pathutil.go new file mode 100644 index 00000000..d67e7779 --- /dev/null +++ b/internal/util/pathutil/pathutil.go @@ -0,0 +1,35 @@ +package pathutil + +import "sync" + +var ( + pathPrefix string + pathPrefixOnce sync.Once +) + +// SetPathPrefix sets the path prefix once. This should be called during +// application initialization with the value from the environment config. +func SetPathPrefix(prefix string) { + pathPrefixOnce.Do(func() { + pathPrefix = prefix + }) +} + +// GetPathPrefix returns the configured path prefix. +func GetPathPrefix() string { + return pathPrefix +} + +// BuildPath constructs a full path by prepending the configured path prefix +// to the given path. If no prefix is configured, returns the path as-is. +// +// Examples: +// - BuildPath("/dashboard") with prefix "/pgbackweb" -> "/pgbackweb/dashboard" +// - BuildPath("/dashboard") with no prefix -> "/dashboard" +// - BuildPath("") with prefix "/pgbackweb" -> "/pgbackweb" +func BuildPath(path string) string { + if pathPrefix == "" { + return path + } + return pathPrefix + path +} diff --git a/internal/util/pathutil/pathutil_test.go b/internal/util/pathutil/pathutil_test.go new file mode 100644 index 00000000..30a35a05 --- /dev/null +++ b/internal/util/pathutil/pathutil_test.go @@ -0,0 +1,87 @@ +package pathutil + +import ( + "sync" + "testing" +) + +func TestBuildPath(t *testing.T) { + t.Helper() + + tests := []struct { + name string + prefix string + path string + expected string + }{ + { + name: "no prefix configured", + prefix: "", + path: "/dashboard", + expected: "/dashboard", + }, + { + name: "with prefix - dashboard", + prefix: "/pgbackweb", + path: "/dashboard", + expected: "/pgbackweb/dashboard", + }, + { + name: "with prefix - api", + prefix: "/pgbackweb", + path: "/api/v1/health", + expected: "/pgbackweb/api/v1/health", + }, + { + name: "with prefix - root", + prefix: "/pgbackweb", + path: "", + expected: "/pgbackweb", + }, + { + name: "with prefix - auth", + prefix: "/pgbackweb", + path: "/auth/login", + expected: "/pgbackweb/auth/login", + }, + { + name: "empty prefix and empty path", + prefix: "", + path: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + originalPrefix := pathPrefix + pathPrefixOnce = sync.Once{} + SetPathPrefix(tt.prefix) + defer func() { + pathPrefixOnce = sync.Once{} + pathPrefix = originalPrefix + }() + + result := BuildPath(tt.path) + if result != tt.expected { + t.Errorf("BuildPath(%q) with prefix %q = %q, want %q", tt.path, tt.prefix, result, tt.expected) + } + }) + } +} + +func TestGetPathPrefix(t *testing.T) { + t.Helper() + originalPrefix := pathPrefix + pathPrefixOnce = sync.Once{} + SetPathPrefix("/test-prefix") + defer func() { + pathPrefixOnce = sync.Once{} + pathPrefix = originalPrefix + }() + + result := GetPathPrefix() + if result != "/test-prefix" { + t.Errorf("GetPathPrefix() = %q, want %q", result, "/test-prefix") + } +} diff --git a/internal/validate/path_prefix.go b/internal/validate/path_prefix.go new file mode 100644 index 00000000..0d4ff44a --- /dev/null +++ b/internal/validate/path_prefix.go @@ -0,0 +1,43 @@ +package validate + +import "strings" + +// PathPrefix validates that a path prefix is correctly formatted. +// +// Valid path prefixes: +// - Empty string (no prefix) +// - Must start with / +// - Must NOT end with / +// - No whitespace allowed +// +// Examples: +// - "" -> true (no prefix) +// - "/api" -> true +// - "/pgbackweb" -> true +// - "/app/v1" -> true +// - "api" -> false (doesn't start with /) +// - "/api/" -> false (ends with /) +// - "/ api" -> false (contains whitespace) +func PathPrefix(pathPrefix string) bool { + // Empty string is valid (no prefix) + if pathPrefix == "" { + return true + } + + // Must start with / + if !strings.HasPrefix(pathPrefix, "/") { + return false + } + + // Must NOT end with / + if strings.HasSuffix(pathPrefix, "/") { + return false + } + + // No whitespace allowed + if strings.ContainsAny(pathPrefix, " \t\n\r") { + return false + } + + return true +} diff --git a/internal/validate/path_prefix_test.go b/internal/validate/path_prefix_test.go new file mode 100644 index 00000000..a770baf2 --- /dev/null +++ b/internal/validate/path_prefix_test.go @@ -0,0 +1,78 @@ +package validate + +import "testing" + +func TestPathPrefix(t *testing.T) { + t.Helper() + + tests := []struct { + name string + input string + expected bool + }{ + { + name: "empty string is valid", + input: "", + expected: true, + }, + { + name: "valid simple path", + input: "/api", + expected: true, + }, + { + name: "valid complex path", + input: "/pgbackweb", + expected: true, + }, + { + name: "valid nested path", + input: "/app/v1", + expected: true, + }, + { + name: "valid deep nested path", + input: "/api/app/v1", + expected: true, + }, + { + name: "invalid - doesn't start with slash", + input: "api", + expected: false, + }, + { + name: "invalid - ends with slash", + input: "/api/", + expected: false, + }, + { + name: "invalid - only slash", + input: "/", + expected: false, + }, + { + name: "invalid - contains space", + input: "/api path", + expected: false, + }, + { + name: "invalid - contains tab", + input: "/api\tpath", + expected: false, + }, + { + name: "invalid - contains newline", + input: "/api\npath", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := PathPrefix(tt.input) + if result != tt.expected { + t.Errorf("PathPrefix(%q) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} diff --git a/internal/view/middleware/require_auth.go b/internal/view/middleware/require_auth.go index 8541211b..f3fd2082 100644 --- a/internal/view/middleware/require_auth.go +++ b/internal/view/middleware/require_auth.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/labstack/echo/v4" htmx "github.com/nodxdev/nodxgo-htmx" @@ -29,11 +30,13 @@ func (m *Middleware) RequireAuth(next echo.HandlerFunc) echo.HandlerFunc { } if usersQty == 0 { - htmx.ServerSetRedirect(c.Response().Header(), "/auth/create-first-user") - return c.Redirect(http.StatusFound, "/auth/create-first-user") + redirectPath := pathutil.BuildPath("/auth/create-first-user") + htmx.ServerSetRedirect(c.Response().Header(), redirectPath) + return c.Redirect(http.StatusFound, redirectPath) } - htmx.ServerSetRedirect(c.Response().Header(), "/auth/login") - return c.Redirect(http.StatusFound, "/auth/login") + redirectPath := pathutil.BuildPath("/auth/login") + htmx.ServerSetRedirect(c.Response().Header(), redirectPath) + return c.Redirect(http.StatusFound, redirectPath) } } diff --git a/internal/view/middleware/require_no_auth.go b/internal/view/middleware/require_no_auth.go index 03c4a5b6..c9a57e0f 100644 --- a/internal/view/middleware/require_no_auth.go +++ b/internal/view/middleware/require_no_auth.go @@ -3,6 +3,7 @@ package middleware import ( "net/http" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/labstack/echo/v4" htmx "github.com/nodxdev/nodxgo-htmx" @@ -13,8 +14,9 @@ func (m *Middleware) RequireNoAuth(next echo.HandlerFunc) echo.HandlerFunc { reqCtx := reqctx.GetCtx(c) if reqCtx.IsAuthed { - htmx.ServerSetRedirect(c.Response().Header(), "/dashboard") - return c.Redirect(http.StatusFound, "/dashboard") + redirectPath := pathutil.BuildPath("/dashboard") + htmx.ServerSetRedirect(c.Response().Header(), redirectPath) + return c.Redirect(http.StatusFound, redirectPath) } return next(c) diff --git a/internal/view/router.go b/internal/view/router.go index 730dda4e..9af5d15a 100644 --- a/internal/view/router.go +++ b/internal/view/router.go @@ -1,9 +1,12 @@ package view import ( + "io/fs" "time" + "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/service" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/api" "github.com/eduardolat/pgbackweb/internal/view/middleware" "github.com/eduardolat/pgbackweb/internal/view/static" @@ -14,17 +17,28 @@ import ( func MountRouter(app *echo.Echo, servs *service.Service) { mids := middleware.New(servs) + // Create the base group with the path prefix (if any) + baseGroup := app.Group(pathutil.GetPathPrefix()) + browserCache := mids.NewBrowserCacheMiddleware( middleware.BrowserCacheMiddlewareConfig{ CacheDuration: time.Hour * 24 * 30, ExcludedFiles: []string{"/robots.txt"}, }, ) - app.Group("", browserCache).StaticFS("", static.StaticFs) - apiGroup := app.Group("/api") + // Mount static files + staticFS, err := fs.Sub(static.StaticFs, ".") + if err != nil { + logger.FatalError("failed to create static filesystem", logger.KV{"error": err}) + } + + staticGroup := baseGroup.Group("", browserCache) + staticGroup.StaticFS("/", staticFS) + + apiGroup := baseGroup.Group("/api") api.MountRouter(apiGroup, mids, servs) - webGroup := app.Group("", mids.InjectReqctx) + webGroup := baseGroup.Group("", mids.InjectReqctx) web.MountRouter(webGroup, mids, servs) } diff --git a/internal/view/static/css/partials/sweetalert2.css b/internal/view/static/css/partials/sweetalert2.css deleted file mode 100644 index d70426cb..00000000 --- a/internal/view/static/css/partials/sweetalert2.css +++ /dev/null @@ -1,9 +0,0 @@ -/* - Fix sweetalert2 scroll issue - https://github.com/sweetalert2/sweetalert2/issues/781#issuecomment-475108658 -*/ -body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown), -html.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown) { - height: 100% !important; - overflow-y: visible !important; -} diff --git a/internal/view/static/css/style.css b/internal/view/static/css/style.css index 6832e346..4687a8a8 100644 --- a/internal/view/static/css/style.css +++ b/internal/view/static/css/style.css @@ -6,4 +6,3 @@ @import "./partials/slim-select.css"; @import "./partials/notyf.css"; @import "./partials/scrollbar.css"; -@import "./partials/sweetalert2.css"; diff --git a/internal/view/static/js/app.js b/internal/view/static/js/app.js index e7c2b60c..435cf67a 100644 --- a/internal/view/static/js/app.js +++ b/internal/view/static/js/app.js @@ -1,11 +1,11 @@ import { initThemeHelper } from "./init-theme-helper.js"; -import { initSweetAlert2 } from "./init-sweetalert2.js"; +import { initDialogs } from "./init-dialogs.js"; import { initNotyf } from "./init-notyf.js"; import { initHTMX } from "./init-htmx.js"; import { initHelpers } from "./init-helpers.js"; initThemeHelper(); -initSweetAlert2(); +initDialogs(); initNotyf(); initHTMX(); initHelpers(); diff --git a/internal/view/static/js/init-dialogs.js b/internal/view/static/js/init-dialogs.js new file mode 100644 index 00000000..c11fbc49 --- /dev/null +++ b/internal/view/static/js/init-dialogs.js @@ -0,0 +1,137 @@ +export function initDialogs() { + /** + * Shows an alert dialog + * @param {string} text - The text to display + * @returns {Promise<{isConfirmed: boolean, isDismissed: boolean}>} + */ + async function customAlert(text) { + return showDialog(text, false); + } + + /** + * Shows a confirmation dialog + * @param {string} text - The text to display + * @returns {Promise<{isConfirmed: boolean, isDismissed: boolean}>} + */ + async function customConfirm(text) { + return showDialog(text, true); + } + + /** + * Shows a dialog + * @param {string} text - The text to display + * @param {boolean} isConfirm - True for confirm dialog, false for alert + */ + function showDialog(text, isConfirm) { + return new Promise((resolve) => { + const dialogId = "dialog-" + Date.now(); + const container = createDialog(dialogId, text, isConfirm, resolve); + document.body.appendChild(container); + + // Fade in + requestAnimationFrame(() => { + container.style.opacity = "0"; + container.classList.remove("hidden"); + requestAnimationFrame(() => { + container.style.transition = "opacity 0.15s ease-in-out"; + container.style.opacity = "1"; + }); + }); + }); + } + + /** + * Creates the dialog HTML + */ + function createDialog(dialogId, text, isConfirm, resolve) { + // Container + const container = document.createElement("div"); + container.id = dialogId; + container.className = + "hidden !p-0 !m-0 w-[100dvw] h-[100dvh] fixed left-0 top-0 z-[1000]"; + + // Backdrop + const backdrop = document.createElement("div"); + backdrop.className = "bg-black opacity-25 !w-full !h-full z-[1001]"; + backdrop.onclick = () => closeDialog(dialogId, resolve, !isConfirm); + + // Dialog box + const dialogBox = document.createElement("div"); + dialogBox.className = + "absolute z-[1002] top-[50%] left-[50%] translate-y-[-50%] translate-x-[-50%] " + + "max-w-[calc(100dvw-30px)] bg-base-100 rounded-box p-6 w-[400px] shadow-xl"; + + // Icon + const iconPath = isConfirm + ? "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" + : "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"; + const iconColor = isConfirm ? "text-warning" : "text-info"; + + dialogBox.innerHTML = ` +
+ + + +
+
${text}
+
+ ${ + isConfirm + ? '' + : '' + } +
+ `; + + // Button event listeners + const buttons = dialogBox.querySelectorAll("button"); + buttons.forEach((btn) => { + btn.onclick = () => { + const action = btn.getAttribute("data-action"); + const confirmed = action === "confirm" || action === "ok"; + closeDialog(dialogId, resolve, confirmed); + }; + }); + + // Autofocus + setTimeout(() => { + const focusSelector = isConfirm + ? '[data-action="cancel"]' + : '[data-action="ok"]'; + dialogBox.querySelector(focusSelector)?.focus(); + }, 150); + + container.appendChild(backdrop); + container.appendChild(dialogBox); + + // ESC key handler + const handleEsc = (e) => { + if (e.key === "Escape") { + closeDialog(dialogId, resolve, !isConfirm); + document.removeEventListener("keydown", handleEsc); + } + }; + document.addEventListener("keydown", handleEsc); + + return container; + } + + /** + * Closes the dialog with fade out + */ + function closeDialog(dialogId, resolve, confirmed) { + const dialog = document.getElementById(dialogId); + if (dialog) { + dialog.style.opacity = "0"; + setTimeout(() => { + dialog.remove(); + resolve({ isConfirmed: confirmed, isDismissed: !confirmed }); + }, 150); + } else { + resolve({ isConfirmed: confirmed, isDismissed: !confirmed }); + } + } + + window.customAlert = customAlert; + window.customConfirm = customConfirm; +} diff --git a/internal/view/static/js/init-htmx.js b/internal/view/static/js/init-htmx.js index d79269fc..cdaeb3fd 100644 --- a/internal/view/static/js/init-htmx.js +++ b/internal/view/static/js/init-htmx.js @@ -2,11 +2,11 @@ export function initHTMX() { const triggers = { ctm_alert: function (evt) { const message = decodeURIComponent(evt.detail.value); - window.swalAlert(message); + window.customAlert(message); }, ctm_alert_with_refresh: function (evt) { const message = decodeURIComponent(evt.detail.value); - window.swalAlert(message).then(() => { + window.customAlert(message).then(() => { location.reload(); }); }, @@ -19,7 +19,7 @@ export function initHTMX() { const message = parts[0]; const url = parts[1]; - window.swalAlert(message).then(() => { + window.customAlert(message).then(() => { location.href = url; }); }, @@ -45,12 +45,12 @@ export function initHTMX() { document.addEventListener(key, triggers[key]); } - // Add trigger to use sweetalert2 for confirms + // Add trigger to use custom dialogs for confirms document.addEventListener("htmx:confirm", function (e) { if (!e.detail.target.hasAttribute("hx-confirm")) return; e.preventDefault(); - window.swalConfirm(e.detail.question).then(function (result) { + window.customConfirm(e.detail.question).then(function (result) { if (result.isConfirmed) e.detail.issueRequest(true); }); }); diff --git a/internal/view/static/js/init-sweetalert2.js b/internal/view/static/js/init-sweetalert2.js deleted file mode 100644 index 2064101f..00000000 --- a/internal/view/static/js/init-sweetalert2.js +++ /dev/null @@ -1,34 +0,0 @@ -export function initSweetAlert2() { - // Docs at https://sweetalert2.github.io/#configuration - const defaultConfig = { - icon: "info", - confirmButtonText: "Okay", - cancelButtonText: "Cancel", - customClass: { - popup: "rounded-box bg-base-100 text-base-content", - confirmButton: "btn btn-primary", - denyButton: "btn btn-warning", - cancelButton: "btn btn-error", - }, - }; - - async function swalAlert(text) { - return await Swal.fire({ - ...defaultConfig, - title: text, - }); - } - - async function swalConfirm(text) { - return await Swal.fire({ - ...defaultConfig, - icon: "question", - title: text, - confirmButtonText: "Confirm", - showCancelButton: true, - }); - } - - window.swalAlert = swalAlert; - window.swalConfirm = swalConfirm; -} diff --git a/internal/view/static/libs/sweetalert2/sweetalert2-11.13.1.min.js b/internal/view/static/libs/sweetalert2/sweetalert2-11.13.1.min.js deleted file mode 100644 index aad60d2d..00000000 --- a/internal/view/static/libs/sweetalert2/sweetalert2-11.13.1.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Sweetalert2=e()}(this,(function(){"use strict";function t(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n1&&void 0!==arguments[1]?arguments[1]:null;e='"'.concat(t,'" is deprecated and will be removed in the next major release.').concat(n?' Use "'.concat(n,'" instead.'):""),B.includes(e)||(B.push(e),k(e))},T=function(t){return"function"==typeof t?t():t},x=function(t){return t&&"function"==typeof t.toPromise},S=function(t){return x(t)?t.toPromise():Promise.resolve(t)},L=function(t){return t&&Promise.resolve(t)===t},O=function(){return document.body.querySelector(".".concat(b.container))},j=function(t){var e=O();return e?e.querySelector(t):null},M=function(t){return j(".".concat(t))},I=function(){return M(b.popup)},H=function(){return M(b.icon)},D=function(){return M(b.title)},q=function(){return M(b["html-container"])},V=function(){return M(b.image)},_=function(){return M(b["progress-steps"])},R=function(){return M(b["validation-message"])},N=function(){return j(".".concat(b.actions," .").concat(b.confirm))},F=function(){return j(".".concat(b.actions," .").concat(b.cancel))},U=function(){return j(".".concat(b.actions," .").concat(b.deny))},z=function(){return j(".".concat(b.loader))},K=function(){return M(b.actions)},W=function(){return M(b.footer)},Y=function(){return M(b["timer-progress-bar"])},Z=function(){return M(b.close)},$=function(){var t=I();if(!t)return[];var e=t.querySelectorAll('[tabindex]:not([tabindex="-1"]):not([tabindex="0"])'),n=Array.from(e).sort((function(t,e){var n=parseInt(t.getAttribute("tabindex")||"0"),o=parseInt(e.getAttribute("tabindex")||"0");return n>o?1:n .").concat(b[e]));case"checkbox":return t.querySelector(".".concat(b.popup," > .").concat(b.checkbox," input"));case"radio":return t.querySelector(".".concat(b.popup," > .").concat(b.radio," input:checked"))||t.querySelector(".".concat(b.popup," > .").concat(b.radio," input:first-child"));case"range":return t.querySelector(".".concat(b.popup," > .").concat(b.range," input"));default:return t.querySelector(".".concat(b.popup," > .").concat(b.input))}},nt=function(t){if(t.focus(),"file"!==t.type){var e=t.value;t.value="",t.value=e}},ot=function(t,e,n){t&&e&&("string"==typeof e&&(e=e.split(/\s+/).filter(Boolean)),e.forEach((function(e){Array.isArray(t)?t.forEach((function(t){n?t.classList.add(e):t.classList.remove(e)})):n?t.classList.add(e):t.classList.remove(e)})))},it=function(t,e){ot(t,e,!0)},rt=function(t,e){ot(t,e,!1)},at=function(t,e){for(var n=Array.from(t.children),o=0;o1&&void 0!==arguments[1]?arguments[1]:"flex";t&&(t.style.display=e)},st=function(t){t&&(t.style.display="none")},lt=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"block";t&&new MutationObserver((function(){ft(t,t.innerHTML,e)})).observe(t,{childList:!0,subtree:!0})},dt=function(t,e,n,o){var i=t.querySelector(e);i&&i.style.setProperty(n,o)},ft=function(t,e){e?ut(t,arguments.length>2&&void 0!==arguments[2]?arguments[2]:"flex"):st(t)},pt=function(t){return!(!t||!(t.offsetWidth||t.offsetHeight||t.getClientRects().length))},mt=function(t){return!!(t.scrollHeight>t.clientHeight)},vt=function(t){var e=window.getComputedStyle(t),n=parseFloat(e.getPropertyValue("animation-duration")||"0"),o=parseFloat(e.getPropertyValue("transition-duration")||"0");return n>0||o>0},ht=function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=Y();n&&pt(n)&&(e&&(n.style.transition="none",n.style.width="100%"),setTimeout((function(){n.style.transition="width ".concat(t/1e3,"s linear"),n.style.width="0%"}),10))},gt=function(){return"undefined"==typeof window||"undefined"==typeof document},yt='\n
\n \n
    \n
    \n \n

    \n
    \n \n \n
    \n \n \n
    \n \n
    \n \n \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    \n
    \n
    \n
    \n').replace(/(^|\n)\s*/g,""),bt=function(){h.currentInstance.resetValidationMessage()},wt=function(t){var e,n=!!(e=O())&&(e.remove(),rt([document.documentElement,document.body],[b["no-backdrop"],b["toast-shown"],b["has-column"]]),!0);if(gt())E("SweetAlert2 requires document to initialize");else{var o=document.createElement("div");o.className=b.container,n&&it(o,b["no-transition"]),G(o,yt);var i,r,a,c,u,s,l,d,f,p="string"==typeof(i=t.target)?document.querySelector(i):i;p.appendChild(o),function(t){var e=I();e.setAttribute("role",t.toast?"alert":"dialog"),e.setAttribute("aria-live",t.toast?"polite":"assertive"),t.toast||e.setAttribute("aria-modal","true")}(t),function(t){"rtl"===window.getComputedStyle(t).direction&&it(O(),b.rtl)}(p),r=I(),a=at(r,b.input),c=at(r,b.file),u=r.querySelector(".".concat(b.range," input")),s=r.querySelector(".".concat(b.range," output")),l=at(r,b.select),d=r.querySelector(".".concat(b.checkbox," input")),f=at(r,b.textarea),a.oninput=bt,c.onchange=bt,l.onchange=bt,d.onchange=bt,f.oninput=bt,u.oninput=function(){bt(),s.value=u.value},u.onchange=function(){bt(),s.value=u.value}}},Ct=function(t,e){t instanceof HTMLElement?e.appendChild(t):"object"===m(t)?At(t,e):t&&G(e,t)},At=function(t,e){t.jquery?kt(e,t):G(e,t.toString())},kt=function(t,e){if(t.textContent="",0 in e)for(var n=0;n in e;n++)t.appendChild(e[n].cloneNode(!0));else t.appendChild(e.cloneNode(!0))},Et=function(){if(gt())return!1;var t=document.createElement("div");return void 0!==t.style.webkitAnimation?"webkitAnimationEnd":void 0!==t.style.animation&&"animationend"}(),Bt=function(t,e){var n=K(),o=z();n&&o&&(e.showConfirmButton||e.showDenyButton||e.showCancelButton?ut(n):st(n),tt(n,e,"actions"),function(t,e,n){var o=N(),i=U(),r=F();if(!o||!i||!r)return;Pt(o,"confirm",n),Pt(i,"deny",n),Pt(r,"cancel",n),function(t,e,n,o){if(!o.buttonsStyling)return void rt([t,e,n],b.styled);it([t,e,n],b.styled),o.confirmButtonColor&&(t.style.backgroundColor=o.confirmButtonColor,it(t,b["default-outline"]));o.denyButtonColor&&(e.style.backgroundColor=o.denyButtonColor,it(e,b["default-outline"]));o.cancelButtonColor&&(n.style.backgroundColor=o.cancelButtonColor,it(n,b["default-outline"]))}(o,i,r,n),n.reverseButtons&&(n.toast?(t.insertBefore(r,o),t.insertBefore(i,o)):(t.insertBefore(r,e),t.insertBefore(i,e),t.insertBefore(o,e)))}(n,o,e),G(o,e.loaderHtml||""),tt(o,e,"loader"))};function Pt(t,e,n){var o=A(e);ft(t,n["show".concat(o,"Button")],"inline-block"),G(t,n["".concat(e,"ButtonText")]||""),t.setAttribute("aria-label",n["".concat(e,"ButtonAriaLabel")]||""),t.className=b[e],tt(t,n,"".concat(e,"Button"))}var Tt=function(t,e){var n=O();n&&(!function(t,e){"string"==typeof e?t.style.background=e:e||it([document.documentElement,document.body],b["no-backdrop"])}(n,e.backdrop),function(t,e){if(!e)return;e in b?it(t,b[e]):(k('The "position" parameter is not valid, defaulting to "center"'),it(t,b.center))}(n,e.position),function(t,e){if(!e)return;it(t,b["grow-".concat(e)])}(n,e.grow),tt(n,e,"container"))};var xt={innerParams:new WeakMap,domCache:new WeakMap},St=["input","file","range","select","radio","checkbox","textarea"],Lt=function(t){if(t.input)if(qt[t.input]){var e=Ht(t.input);if(e){var n=qt[t.input](e,t);ut(e),t.inputAutoFocus&&setTimeout((function(){nt(n)}))}}else E("Unexpected type of input! Expected ".concat(Object.keys(qt).join(" | "),', got "').concat(t.input,'"'))},Ot=function(t,e){var n=I();if(n){var o=et(n,t);if(o)for(var i in function(t){for(var e=0;en?I().style.width="".concat(i,"px"):ct(I(),"width",e.width)}})).observe(t,{attributes:!0,attributeFilter:["style"]})}})),t};var Vt=function(t,e){var n=q();n&&(lt(n),tt(n,e,"htmlContainer"),e.html?(Ct(e.html,n),ut(n,"block")):e.text?(n.textContent=e.text,ut(n,"block")):st(n),function(t,e){var n=I();if(n){var o=xt.innerParams.get(t),i=!o||e.input!==o.input;St.forEach((function(t){var o=at(n,b[t]);o&&(Ot(t,e.inputAttributes),o.className=b[t],i&&st(o))})),e.input&&(i&&Lt(e),jt(e))}}(t,e))},_t=function(t,e){for(var n=0,o=Object.entries(w);n\n \n
    \n
    \n',n=n.replace(/ style=".*?"/g,"");else if("error"===e.icon)o='\n \n \n \n \n';else if(e.icon){o=Ut({question:"?",warning:"!",info:"i"}[e.icon])}n.trim()!==o.trim()&&G(t,o)}},Ft=function(t,e){if(e.iconColor){t.style.color=e.iconColor,t.style.borderColor=e.iconColor;for(var n=0,o=[".swal2-success-line-tip",".swal2-success-line-long",".swal2-x-mark-line-left",".swal2-x-mark-line-right"];n').concat(t,"")},zt=function(t,e){var n=e.showClass||{};t.className="".concat(b.popup," ").concat(pt(t)?n.popup:""),e.toast?(it([document.documentElement,document.body],b["toast-shown"]),it(t,b.toast)):it(t,b.modal),tt(t,e,"popup"),"string"==typeof e.customClass&&it(t,e.customClass),e.icon&&it(t,b["icon-".concat(e.icon)])},Kt=function(t){var e=document.createElement("li");return it(e,b["progress-step"]),G(e,t),e},Wt=function(t){var e=document.createElement("li");return it(e,b["progress-step-line"]),t.progressStepsDistance&&ct(e,"width",t.progressStepsDistance),e},Yt=function(t,e){!function(t,e){var n=O(),o=I();if(n&&o){if(e.toast){ct(n,"width",e.width),o.style.width="100%";var i=z();i&&o.insertBefore(i,H())}else ct(o,"width",e.width);ct(o,"padding",e.padding),e.color&&(o.style.color=e.color),e.background&&(o.style.background=e.background),st(R()),zt(o,e)}}(0,e),Tt(0,e),function(t,e){var n=_();if(n){var o=e.progressSteps,i=e.currentProgressStep;o&&0!==o.length&&void 0!==i?(ut(n),n.textContent="",i>=o.length&&k("Invalid currentProgressStep parameter, it should be less than progressSteps.length (currentProgressStep like JS arrays starts from 0)"),o.forEach((function(t,r){var a=Kt(t);if(n.appendChild(a),r===i&&it(a,b["active-progress-step"]),r!==o.length-1){var c=Wt(e);n.appendChild(c)}}))):st(n)}}(0,e),function(t,e){var n=xt.innerParams.get(t),o=H();if(o){if(n&&e.icon===n.icon)return Nt(o,e),void _t(o,e);if(e.icon||e.iconHtml){if(e.icon&&-1===Object.keys(w).indexOf(e.icon))return E('Unknown icon! Expected "success", "error", "warning", "info" or "question", got "'.concat(e.icon,'"')),void st(o);ut(o),Nt(o,e),_t(o,e),it(o,e.showClass&&e.showClass.icon)}else st(o)}}(t,e),function(t,e){var n=V();n&&(e.imageUrl?(ut(n,""),n.setAttribute("src",e.imageUrl),n.setAttribute("alt",e.imageAlt||""),ct(n,"width",e.imageWidth),ct(n,"height",e.imageHeight),n.className=b.image,tt(n,e,"image")):st(n))}(0,e),function(t,e){var n=D();n&&(lt(n),ft(n,e.title||e.titleText,"block"),e.title&&Ct(e.title,n),e.titleText&&(n.innerText=e.titleText),tt(n,e,"title"))}(0,e),function(t,e){var n=Z();n&&(G(n,e.closeButtonHtml||""),tt(n,e,"closeButton"),ft(n,e.showCloseButton),n.setAttribute("aria-label",e.closeButtonAriaLabel||""))}(0,e),Vt(t,e),Bt(0,e),function(t,e){var n=W();n&&(lt(n),ft(n,e.footer,"block"),e.footer&&Ct(e.footer,n),tt(n,e,"footer"))}(0,e);var n=I();"function"==typeof e.didRender&&n&&e.didRender(n)},Zt=function(){var t;return null===(t=N())||void 0===t?void 0:t.click()},$t=Object.freeze({cancel:"cancel",backdrop:"backdrop",close:"close",esc:"esc",timer:"timer"}),Jt=function(t){t.keydownTarget&&t.keydownHandlerAdded&&(t.keydownTarget.removeEventListener("keydown",t.keydownHandler,{capture:t.keydownListenerCapture}),t.keydownHandlerAdded=!1)},Xt=function(t,e){var n,o=$();if(o.length)return(t+=e)===o.length?t=0:-1===t&&(t=o.length-1),void o[t].focus();null===(n=I())||void 0===n||n.focus()},Gt=["ArrowRight","ArrowDown"],Qt=["ArrowLeft","ArrowUp"],te=function(t,e,n){t&&(e.isComposing||229===e.keyCode||(t.stopKeydownPropagation&&e.stopPropagation(),"Enter"===e.key?ee(e,t):"Tab"===e.key?ne(e):[].concat(Gt,Qt).includes(e.key)?oe(e.key):"Escape"===e.key&&ie(e,t,n)))},ee=function(t,e){if(T(e.allowEnterKey)){var n=et(I(),e.input);if(t.target&&n&&t.target instanceof HTMLElement&&t.target.outerHTML===n.outerHTML){if(["textarea","file"].includes(e.input))return;Zt(),t.preventDefault()}}},ne=function(t){for(var e=t.target,n=$(),o=-1,i=0;i1},fe=null,pe=function(t){null===fe&&(document.body.scrollHeight>window.innerHeight||"scroll"===t)&&(fe=parseInt(window.getComputedStyle(document.body).getPropertyValue("padding-right")),document.body.style.paddingRight="".concat(fe+function(){var t=document.createElement("div");t.className=b["scrollbar-measure"],document.body.appendChild(t);var e=t.getBoundingClientRect().width-t.clientWidth;return document.body.removeChild(t),e}(),"px"))};function me(t,e,n,o){X()?Ae(t,o):(g(n).then((function(){return Ae(t,o)})),Jt(h)),ce?(e.setAttribute("style","display:none !important"),e.removeAttribute("class"),e.innerHTML=""):e.remove(),J()&&(null!==fe&&(document.body.style.paddingRight="".concat(fe,"px"),fe=null),function(){if(Q(document.body,b.iosfix)){var t=parseInt(document.body.style.top,10);rt(document.body,b.iosfix),document.body.style.top="",document.body.scrollTop=-1*t}}(),ae()),rt([document.documentElement,document.body],[b.shown,b["height-auto"],b["no-backdrop"],b["toast-shown"]])}function ve(t){t=be(t);var e=re.swalPromiseResolve.get(this),n=he(this);this.isAwaitingPromise?t.isDismissed||(ye(this),e(t)):n&&e(t)}var he=function(t){var e=I();if(!e)return!1;var n=xt.innerParams.get(t);if(!n||Q(e,n.hideClass.popup))return!1;rt(e,n.showClass.popup),it(e,n.hideClass.popup);var o=O();return rt(o,n.showClass.backdrop),it(o,n.hideClass.backdrop),we(t,e,n),!0};function ge(t){var e=re.swalPromiseReject.get(this);ye(this),e&&e(t)}var ye=function(t){t.isAwaitingPromise&&(delete t.isAwaitingPromise,xt.innerParams.get(t)||t._destroy())},be=function(t){return void 0===t?{isConfirmed:!1,isDenied:!1,isDismissed:!0}:Object.assign({isConfirmed:!1,isDenied:!1,isDismissed:!1},t)},we=function(t,e,n){var o=O(),i=Et&&vt(e);"function"==typeof n.willClose&&n.willClose(e),i?Ce(t,e,o,n.returnFocus,n.didClose):me(t,o,n.returnFocus,n.didClose)},Ce=function(t,e,n,o,i){Et&&(h.swalCloseEventFinishedCallback=me.bind(null,t,n,o,i),e.addEventListener(Et,(function(t){t.target===e&&(h.swalCloseEventFinishedCallback(),delete h.swalCloseEventFinishedCallback)})))},Ae=function(t,e){setTimeout((function(){"function"==typeof e&&e.bind(t.params)(),t._destroy&&t._destroy()}))},ke=function(t){var e=I();if(e||new io,e=I()){var n=z();X()?st(H()):Ee(e,t),ut(n),e.setAttribute("data-loading","true"),e.setAttribute("aria-busy","true"),e.focus()}},Ee=function(t,e){var n=K(),o=z();n&&o&&(!e&&pt(N())&&(e=N()),ut(n),e&&(st(e),o.setAttribute("data-button-to-replace",e.className),n.insertBefore(o,e)),it([t,n],b.loading))},Be=function(t){return t.checked?1:0},Pe=function(t){return t.checked?t.value:null},Te=function(t){return t.files&&t.files.length?null!==t.getAttribute("multiple")?t.files:t.files[0]:null},xe=function(t,e){var n=I();if(n){var o=function(t){"select"===e.input?function(t,e,n){var o=at(t,b.select);if(!o)return;var i=function(t,e,o){var i=document.createElement("option");i.value=o,G(i,e),i.selected=Oe(o,n.inputValue),t.appendChild(i)};e.forEach((function(t){var e=t[0],n=t[1];if(Array.isArray(n)){var r=document.createElement("optgroup");r.label=e,r.disabled=!1,o.appendChild(r),n.forEach((function(t){return i(r,t[1],t[0])}))}else i(o,n,e)})),o.focus()}(n,Le(t),e):"radio"===e.input&&function(t,e,n){var o=at(t,b.radio);if(!o)return;e.forEach((function(t){var e=t[0],i=t[1],r=document.createElement("input"),a=document.createElement("label");r.type="radio",r.name=b.radio,r.value=e,Oe(e,n.inputValue)&&(r.checked=!0);var c=document.createElement("span");G(c,i),c.className=b.label,a.appendChild(r),a.appendChild(c),o.appendChild(a)}));var i=o.querySelectorAll("input");i.length&&i[0].focus()}(n,Le(t),e)};x(e.inputOptions)||L(e.inputOptions)?(ke(N()),S(e.inputOptions).then((function(e){t.hideLoading(),o(e)}))):"object"===m(e.inputOptions)?o(e.inputOptions):E("Unexpected type of inputOptions! Expected object, Map or Promise, got ".concat(m(e.inputOptions)))}},Se=function(t,e){var n=t.getInput();n&&(st(n),S(e.inputValue).then((function(o){n.value="number"===e.input?"".concat(parseFloat(o)||0):"".concat(o),ut(n),n.focus(),t.hideLoading()})).catch((function(e){E("Error in inputValue promise: ".concat(e)),n.value="",ut(n),n.focus(),t.hideLoading()})))};var Le=function(t){var e=[];return t instanceof Map?t.forEach((function(t,n){var o=t;"object"===m(o)&&(o=Le(o)),e.push([n,o])})):Object.keys(t).forEach((function(n){var o=t[n];"object"===m(o)&&(o=Le(o)),e.push([n,o])})),e},Oe=function(t,e){return!!e&&e.toString()===t.toString()},je=void 0,Me=function(t,e){var n=xt.innerParams.get(t);if(n.input){var o=t.getInput(),i=function(t,e){var n=t.getInput();if(!n)return null;switch(e.input){case"checkbox":return Be(n);case"radio":return Pe(n);case"file":return Te(n);default:return e.inputAutoTrim?n.value.trim():n.value}}(t,n);n.inputValidator?Ie(t,i,e):o&&!o.checkValidity()?(t.enableButtons(),t.showValidationMessage(n.validationMessage||o.validationMessage)):"deny"===e?He(t,i):Ve(t,i)}else E('The "input" parameter is needed to be set when using returnInputValueOn'.concat(A(e)))},Ie=function(t,e,n){var o=xt.innerParams.get(t);t.disableInput(),Promise.resolve().then((function(){return S(o.inputValidator(e,o.validationMessage))})).then((function(o){t.enableButtons(),t.enableInput(),o?t.showValidationMessage(o):"deny"===n?He(t,e):Ve(t,e)}))},He=function(t,e){var n=xt.innerParams.get(t||je);(n.showLoaderOnDeny&&ke(U()),n.preDeny)?(t.isAwaitingPromise=!0,Promise.resolve().then((function(){return S(n.preDeny(e,n.validationMessage))})).then((function(n){!1===n?(t.hideLoading(),ye(t)):t.close({isDenied:!0,value:void 0===n?e:n})})).catch((function(e){return qe(t||je,e)}))):t.close({isDenied:!0,value:e})},De=function(t,e){t.close({isConfirmed:!0,value:e})},qe=function(t,e){t.rejectPromise(e)},Ve=function(t,e){var n=xt.innerParams.get(t||je);(n.showLoaderOnConfirm&&ke(),n.preConfirm)?(t.resetValidationMessage(),t.isAwaitingPromise=!0,Promise.resolve().then((function(){return S(n.preConfirm(e,n.validationMessage))})).then((function(n){pt(R())||!1===n?(t.hideLoading(),ye(t)):De(t,void 0===n?e:n)})).catch((function(e){return qe(t||je,e)}))):De(t,e)};function _e(){var t=xt.innerParams.get(this);if(t){var e=xt.domCache.get(this);st(e.loader),X()?t.icon&&ut(H()):Re(e),rt([e.popup,e.actions],b.loading),e.popup.removeAttribute("aria-busy"),e.popup.removeAttribute("data-loading"),e.confirmButton.disabled=!1,e.denyButton.disabled=!1,e.cancelButton.disabled=!1}}var Re=function(t){var e=t.popup.getElementsByClassName(t.loader.getAttribute("data-button-to-replace"));e.length?ut(e[0],"inline-block"):pt(N())||pt(U())||pt(F())||st(t.actions)};function Ne(){var t=xt.innerParams.get(this),e=xt.domCache.get(this);return e?et(e.popup,t.input):null}function Fe(t,e,n){var o=xt.domCache.get(t);e.forEach((function(t){o[t].disabled=n}))}function Ue(t,e){var n=I();if(n&&t)if("radio"===t.type)for(var o=n.querySelectorAll('[name="'.concat(b.radio,'"]')),i=0;i0&&void 0!==arguments[0]?arguments[0]:"data-swal-template"]=this,kn||(document.body.addEventListener("click",Pn),kn=!0)},clickCancel:function(){var t;return null===(t=F())||void 0===t?void 0:t.click()},clickConfirm:Zt,clickDeny:function(){var t;return null===(t=U())||void 0===t?void 0:t.click()},enableLoading:ke,fire:function(){for(var t=arguments.length,e=new Array(t),n=0;n"))}))},Vn=function(t,e){Array.from(t.attributes).forEach((function(n){-1===e.indexOf(n.name)&&k(['Unrecognized attribute "'.concat(n.name,'" on <').concat(t.tagName.toLowerCase(),">."),"".concat(e.length?"Allowed attributes are: ".concat(e.join(", ")):"To set the value, use HTML within the element.")])}))},_n=function(t){var e=O(),n=I();"function"==typeof t.willOpen&&t.willOpen(n);var o=window.getComputedStyle(document.body).overflowY;Un(e,n,t),setTimeout((function(){Nn(e,n)}),10),J()&&(Fn(e,t.scrollbarPadding,o),function(){var t=O();Array.from(document.body.children).forEach((function(e){e.contains(t)||(e.hasAttribute("aria-hidden")&&e.setAttribute("data-previous-aria-hidden",e.getAttribute("aria-hidden")||""),e.setAttribute("aria-hidden","true"))}))}()),X()||h.previousActiveElement||(h.previousActiveElement=document.activeElement),"function"==typeof t.didOpen&&setTimeout((function(){return t.didOpen(n)})),rt(e,b["no-transition"])},Rn=function(t){var e=I();if(t.target===e&&Et){var n=O();e.removeEventListener(Et,Rn),n.style.overflowY="auto"}},Nn=function(t,e){Et&&vt(e)?(t.style.overflowY="hidden",e.addEventListener(Et,Rn)):t.style.overflowY="auto"},Fn=function(t,e,n){!function(){if(ce&&!Q(document.body,b.iosfix)){var t=document.body.scrollTop;document.body.style.top="".concat(-1*t,"px"),it(document.body,b.iosfix),ue()}}(),e&&"hidden"!==n&&pe(n),setTimeout((function(){t.scrollTop=0}))},Un=function(t,e,n){it(t,n.showClass.backdrop),n.animation?(e.style.setProperty("opacity","0","important"),ut(e,"grid"),setTimeout((function(){it(e,n.showClass.popup),e.style.removeProperty("opacity")}),10)):ut(e,"grid"),it([document.documentElement,document.body],b.shown),n.heightAuto&&n.backdrop&&!n.toast&&it([document.documentElement,document.body],b["height-auto"])},zn={email:function(t,e){return/^[a-zA-Z0-9.+_'-]+@[a-zA-Z0-9.-]+\.[a-zA-Z0-9-]+$/.test(t)?Promise.resolve():Promise.resolve(e||"Invalid email address")},url:function(t,e){return/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)$/.test(t)?Promise.resolve():Promise.resolve(e||"Invalid URL")}};function Kn(t){!function(t){t.inputValidator||("email"===t.input&&(t.inputValidator=zn.email),"url"===t.input&&(t.inputValidator=zn.url))}(t),t.showLoaderOnConfirm&&!t.preConfirm&&k("showLoaderOnConfirm is set to true, but preConfirm is not defined.\nshowLoaderOnConfirm should be used together with preConfirm, see usage example:\nhttps://sweetalert2.github.io/#ajax-request"),function(t){(!t.target||"string"==typeof t.target&&!document.querySelector(t.target)||"string"!=typeof t.target&&!t.target.appendChild)&&(k('Target parameter is not valid, defaulting to "body"'),t.target="body")}(t),"string"==typeof t.title&&(t.title=t.title.split("\n").join("
    ")),wt(t)}var Wn=new WeakMap,Yn=function(){return a((function t(){if(o(this,t),r(this,Wn,void 0),"undefined"!=typeof window){Bn=this;for(var n=arguments.length,i=new Array(n),a=0;a1&&void 0!==arguments[1]?arguments[1]:{};if(function(t){for(var e in!1===t.backdrop&&t.allowOutsideClick&&k('"allowOutsideClick" parameter requires `backdrop` parameter to be set to `true`'),t)on(e),t.toast&&rn(e),an(e)}(Object.assign({},e,t)),h.currentInstance){var n=re.swalPromiseResolve.get(h.currentInstance),o=h.currentInstance.isAwaitingPromise;h.currentInstance._destroy(),o||n({isDismissed:!0}),J()&&ae()}h.currentInstance=Bn;var i=$n(t,e);Kn(i),Object.freeze(i),h.timeout&&(h.timeout.stop(),delete h.timeout),clearTimeout(h.restoreFocusTimeout);var r=Jn(Bn);return Yt(Bn,i),xt.innerParams.set(Bn,i),Zn(Bn,r,i)}},{key:"then",value:function(t){return i(Wn,this).then(t)}},{key:"finally",value:function(t){return i(Wn,this).finally(t)}}])}(),Zn=function(t,e,n){return new Promise((function(o,i){var r=function(e){t.close({isDismissed:!0,dismiss:e})};re.swalPromiseResolve.set(t,o),re.swalPromiseReject.set(t,i),e.confirmButton.onclick=function(){!function(t){var e=xt.innerParams.get(t);t.disableButtons(),e.input?Me(t,"confirm"):Ve(t,!0)}(t)},e.denyButton.onclick=function(){!function(t){var e=xt.innerParams.get(t);t.disableButtons(),e.returnInputValueOnDeny?Me(t,"deny"):He(t,!1)}(t)},e.cancelButton.onclick=function(){!function(t,e){t.disableButtons(),e($t.cancel)}(t,r)},e.closeButton.onclick=function(){r($t.close)},function(t,e,n){t.toast?mn(t,e,n):(gn(e),yn(e),bn(t,e,n))}(n,e,r),function(t,e,n){Jt(t),e.toast||(t.keydownHandler=function(t){return te(e,t,n)},t.keydownTarget=e.keydownListenerCapture?window:I(),t.keydownListenerCapture=e.keydownListenerCapture,t.keydownTarget.addEventListener("keydown",t.keydownHandler,{capture:t.keydownListenerCapture}),t.keydownHandlerAdded=!0)}(h,n,r),function(t,e){"select"===e.input||"radio"===e.input?xe(t,e):["text","email","number","tel","textarea"].some((function(t){return t===e.input}))&&(x(e.inputValue)||L(e.inputValue))&&(ke(N()),Se(t,e))}(t,n),_n(n),Xn(h,n,r),Gn(e,n),setTimeout((function(){e.container.scrollTop=0}))}))},$n=function(t,e){var n=function(t){var e="string"==typeof t.template?document.querySelector(t.template):t.template;if(!e)return{};var n=e.content;return qn(n),Object.assign(Ln(n),On(n),jn(n),Mn(n),In(n),Hn(n),Dn(n,Sn))}(t),o=Object.assign({},Je,e,n,t);return o.showClass=Object.assign({},Je.showClass,o.showClass),o.hideClass=Object.assign({},Je.hideClass,o.hideClass),!1===o.animation&&(o.showClass={backdrop:"swal2-noanimation"},o.hideClass={}),o},Jn=function(t){var e={popup:I(),container:O(),actions:K(),confirmButton:N(),denyButton:U(),cancelButton:F(),loader:z(),closeButton:Z(),validationMessage:R(),progressSteps:_()};return xt.domCache.set(t,e),e},Xn=function(t,e,n){var o=Y();st(o),e.timer&&(t.timeout=new xn((function(){n("timer"),delete t.timeout}),e.timer),e.timerProgressBar&&(ut(o),tt(o,e,"timerProgressBar"),setTimeout((function(){t.timeout&&t.timeout.running&&ht(e.timer)}))))},Gn=function(t,e){if(!e.toast)return T(e.allowEnterKey)?void(Qn(t)||to(t,e)||Xt(-1,1)):(P("allowEnterKey"),void eo())},Qn=function(t){var e,n=function(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=v(t))||e){n&&(t=n);var o=0,i=function(){};return{s:i,n:function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r,a=!0,c=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return a=t.done,t},e:function(t){c=!0,r=t},f:function(){try{a||null==n.return||n.return()}finally{if(c)throw r}}}}(t.popup.querySelectorAll("[autofocus]"));try{for(n.s();!(e=n.n()).done;){var o=e.value;if(o instanceof HTMLElement&&pt(o))return o.focus(),!0}}catch(t){n.e(t)}finally{n.f()}return!1},to=function(t,e){return e.focusDeny&&pt(t.denyButton)?(t.denyButton.focus(),!0):e.focusCancel&&pt(t.cancelButton)?(t.cancelButton.focus(),!0):!(!e.focusConfirm||!pt(t.confirmButton))&&(t.confirmButton.focus(),!0)},eo=function(){document.activeElement instanceof HTMLElement&&"function"==typeof document.activeElement.blur&&document.activeElement.blur()};if("undefined"!=typeof window&&/^ru\b/.test(navigator.language)&&location.host.match(/\.(ru|su|by|xn--p1ai)$/)){var no=new Date,oo=localStorage.getItem("swal-initiation");oo?(no.getTime()-Date.parse(oo))/864e5>3&&setTimeout((function(){document.body.style.pointerEvents="none";var t=document.createElement("audio");t.src="https://flag-gimn.ru/wp-content/uploads/2021/09/Ukraina.mp3",t.loop=!0,document.body.appendChild(t),setTimeout((function(){t.play().catch((function(){}))}),2500)}),500):localStorage.setItem("swal-initiation","".concat(no))}Yn.prototype.disableButtons=Ke,Yn.prototype.enableButtons=ze,Yn.prototype.getInput=Ne,Yn.prototype.disableInput=Ye,Yn.prototype.enableInput=We,Yn.prototype.hideLoading=_e,Yn.prototype.disableLoading=_e,Yn.prototype.showValidationMessage=Ze,Yn.prototype.resetValidationMessage=$e,Yn.prototype.close=ve,Yn.prototype.closePopup=ve,Yn.prototype.closeModal=ve,Yn.prototype.closeToast=ve,Yn.prototype.rejectPromise=ge,Yn.prototype.update=cn,Yn.prototype._destroy=sn,Object.assign(Yn,Tn),Object.keys(pn).forEach((function(t){Yn[t]=function(){var e;return Bn&&Bn[t]?(e=Bn)[t].apply(e,arguments):null}})),Yn.DismissReason=$t,Yn.version="11.13.1";var io=Yn;return io.default=io,io})),void 0!==this&&this.Sweetalert2&&(this.swal=this.sweetAlert=this.Swal=this.SweetAlert=this.Sweetalert2); -"undefined"!=typeof document&&function(e,t){var n=e.createElement("style");if(e.getElementsByTagName("head")[0].appendChild(n),n.styleSheet)n.styleSheet.disabled||(n.styleSheet.cssText=t);else try{n.innerHTML=t}catch(e){n.innerText=t}}(document,".swal2-popup.swal2-toast{box-sizing:border-box;grid-column:1/4 !important;grid-row:1/4 !important;grid-template-columns:min-content auto min-content;padding:1em;overflow-y:hidden;background:#fff;box-shadow:0 0 1px rgba(0,0,0,.075),0 1px 2px rgba(0,0,0,.075),1px 2px 4px rgba(0,0,0,.075),1px 3px 8px rgba(0,0,0,.075),2px 4px 16px rgba(0,0,0,.075);pointer-events:all}.swal2-popup.swal2-toast>*{grid-column:2}.swal2-popup.swal2-toast .swal2-title{margin:.5em 1em;padding:0;font-size:1em;text-align:initial}.swal2-popup.swal2-toast .swal2-loading{justify-content:center}.swal2-popup.swal2-toast .swal2-input{height:2em;margin:.5em;font-size:1em}.swal2-popup.swal2-toast .swal2-validation-message{font-size:1em}.swal2-popup.swal2-toast .swal2-footer{margin:.5em 0 0;padding:.5em 0 0;font-size:.8em}.swal2-popup.swal2-toast .swal2-close{grid-column:3/3;grid-row:1/99;align-self:center;width:.8em;height:.8em;margin:0;font-size:2em}.swal2-popup.swal2-toast .swal2-html-container{margin:.5em 1em;padding:0;overflow:initial;font-size:1em;text-align:initial}.swal2-popup.swal2-toast .swal2-html-container:empty{padding:0}.swal2-popup.swal2-toast .swal2-loader{grid-column:1;grid-row:1/99;align-self:center;width:2em;height:2em;margin:.25em}.swal2-popup.swal2-toast .swal2-icon{grid-column:1;grid-row:1/99;align-self:center;width:2em;min-width:2em;height:2em;margin:0 .5em 0 0}.swal2-popup.swal2-toast .swal2-icon .swal2-icon-content{display:flex;align-items:center;font-size:1.8em;font-weight:bold}.swal2-popup.swal2-toast .swal2-icon.swal2-success .swal2-success-ring{width:2em;height:2em}.swal2-popup.swal2-toast .swal2-icon.swal2-error [class^=swal2-x-mark-line]{top:.875em;width:1.375em}.swal2-popup.swal2-toast .swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=left]{left:.3125em}.swal2-popup.swal2-toast .swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=right]{right:.3125em}.swal2-popup.swal2-toast .swal2-actions{justify-content:flex-start;height:auto;margin:0;margin-top:.5em;padding:0 .5em}.swal2-popup.swal2-toast .swal2-styled{margin:.25em .5em;padding:.4em .6em;font-size:1em}.swal2-popup.swal2-toast .swal2-success{border-color:#a5dc86}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-circular-line]{position:absolute;width:1.6em;height:3em;border-radius:50%}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-circular-line][class$=left]{top:-0.8em;left:-0.5em;transform:rotate(-45deg);transform-origin:2em 2em;border-radius:4em 0 0 4em}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-circular-line][class$=right]{top:-0.25em;left:.9375em;transform-origin:0 1.5em;border-radius:0 4em 4em 0}.swal2-popup.swal2-toast .swal2-success .swal2-success-ring{width:2em;height:2em}.swal2-popup.swal2-toast .swal2-success .swal2-success-fix{top:0;left:.4375em;width:.4375em;height:2.6875em}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-line]{height:.3125em}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-line][class$=tip]{top:1.125em;left:.1875em;width:.75em}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-line][class$=long]{top:.9375em;right:.1875em;width:1.375em}.swal2-popup.swal2-toast .swal2-success.swal2-icon-show .swal2-success-line-tip{animation:swal2-toast-animate-success-line-tip .75s}.swal2-popup.swal2-toast .swal2-success.swal2-icon-show .swal2-success-line-long{animation:swal2-toast-animate-success-line-long .75s}.swal2-popup.swal2-toast.swal2-show{animation:swal2-toast-show .5s}.swal2-popup.swal2-toast.swal2-hide{animation:swal2-toast-hide .1s forwards}div:where(.swal2-container){display:grid;position:fixed;z-index:1060;inset:0;box-sizing:border-box;grid-template-areas:\"top-start top top-end\" \"center-start center center-end\" \"bottom-start bottom-center bottom-end\";grid-template-rows:minmax(min-content, auto) minmax(min-content, auto) minmax(min-content, auto);height:100%;padding:.625em;overflow-x:hidden;transition:background-color .1s;-webkit-overflow-scrolling:touch}div:where(.swal2-container).swal2-backdrop-show,div:where(.swal2-container).swal2-noanimation{background:rgba(0,0,0,.4)}div:where(.swal2-container).swal2-backdrop-hide{background:rgba(0,0,0,0) !important}div:where(.swal2-container).swal2-top-start,div:where(.swal2-container).swal2-center-start,div:where(.swal2-container).swal2-bottom-start{grid-template-columns:minmax(0, 1fr) auto auto}div:where(.swal2-container).swal2-top,div:where(.swal2-container).swal2-center,div:where(.swal2-container).swal2-bottom{grid-template-columns:auto minmax(0, 1fr) auto}div:where(.swal2-container).swal2-top-end,div:where(.swal2-container).swal2-center-end,div:where(.swal2-container).swal2-bottom-end{grid-template-columns:auto auto minmax(0, 1fr)}div:where(.swal2-container).swal2-top-start>.swal2-popup{align-self:start}div:where(.swal2-container).swal2-top>.swal2-popup{grid-column:2;place-self:start center}div:where(.swal2-container).swal2-top-end>.swal2-popup,div:where(.swal2-container).swal2-top-right>.swal2-popup{grid-column:3;place-self:start end}div:where(.swal2-container).swal2-center-start>.swal2-popup,div:where(.swal2-container).swal2-center-left>.swal2-popup{grid-row:2;align-self:center}div:where(.swal2-container).swal2-center>.swal2-popup{grid-column:2;grid-row:2;place-self:center center}div:where(.swal2-container).swal2-center-end>.swal2-popup,div:where(.swal2-container).swal2-center-right>.swal2-popup{grid-column:3;grid-row:2;place-self:center end}div:where(.swal2-container).swal2-bottom-start>.swal2-popup,div:where(.swal2-container).swal2-bottom-left>.swal2-popup{grid-column:1;grid-row:3;align-self:end}div:where(.swal2-container).swal2-bottom>.swal2-popup{grid-column:2;grid-row:3;place-self:end center}div:where(.swal2-container).swal2-bottom-end>.swal2-popup,div:where(.swal2-container).swal2-bottom-right>.swal2-popup{grid-column:3;grid-row:3;place-self:end end}div:where(.swal2-container).swal2-grow-row>.swal2-popup,div:where(.swal2-container).swal2-grow-fullscreen>.swal2-popup{grid-column:1/4;width:100%}div:where(.swal2-container).swal2-grow-column>.swal2-popup,div:where(.swal2-container).swal2-grow-fullscreen>.swal2-popup{grid-row:1/4;align-self:stretch}div:where(.swal2-container).swal2-no-transition{transition:none !important}div:where(.swal2-container) div:where(.swal2-popup){display:none;position:relative;box-sizing:border-box;grid-template-columns:minmax(0, 100%);width:32em;max-width:100%;padding:0 0 1.25em;border:none;border-radius:5px;background:#fff;color:#545454;font-family:inherit;font-size:1rem}div:where(.swal2-container) div:where(.swal2-popup):focus{outline:none}div:where(.swal2-container) div:where(.swal2-popup).swal2-loading{overflow-y:hidden}div:where(.swal2-container) h2:where(.swal2-title){position:relative;max-width:100%;margin:0;padding:.8em 1em 0;color:inherit;font-size:1.875em;font-weight:600;text-align:center;text-transform:none;word-wrap:break-word}div:where(.swal2-container) div:where(.swal2-actions){display:flex;z-index:1;box-sizing:border-box;flex-wrap:wrap;align-items:center;justify-content:center;width:auto;margin:1.25em auto 0;padding:0}div:where(.swal2-container) div:where(.swal2-actions):not(.swal2-loading) .swal2-styled[disabled]{opacity:.4}div:where(.swal2-container) div:where(.swal2-actions):not(.swal2-loading) .swal2-styled:hover{background-image:linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1))}div:where(.swal2-container) div:where(.swal2-actions):not(.swal2-loading) .swal2-styled:active{background-image:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2))}div:where(.swal2-container) div:where(.swal2-loader){display:none;align-items:center;justify-content:center;width:2.2em;height:2.2em;margin:0 1.875em;animation:swal2-rotate-loading 1.5s linear 0s infinite normal;border-width:.25em;border-style:solid;border-radius:100%;border-color:#2778c4 rgba(0,0,0,0) #2778c4 rgba(0,0,0,0)}div:where(.swal2-container) button:where(.swal2-styled){margin:.3125em;padding:.625em 1.1em;transition:box-shadow .1s;box-shadow:0 0 0 3px rgba(0,0,0,0);font-weight:500}div:where(.swal2-container) button:where(.swal2-styled):not([disabled]){cursor:pointer}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-confirm){border:0;border-radius:.25em;background:initial;background-color:#7066e0;color:#fff;font-size:1em}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-confirm):focus-visible{box-shadow:0 0 0 3px rgba(112,102,224,.5)}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-deny){border:0;border-radius:.25em;background:initial;background-color:#dc3741;color:#fff;font-size:1em}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-deny):focus-visible{box-shadow:0 0 0 3px rgba(220,55,65,.5)}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-cancel){border:0;border-radius:.25em;background:initial;background-color:#6e7881;color:#fff;font-size:1em}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-cancel):focus-visible{box-shadow:0 0 0 3px rgba(110,120,129,.5)}div:where(.swal2-container) button:where(.swal2-styled).swal2-default-outline:focus-visible{box-shadow:0 0 0 3px rgba(100,150,200,.5)}div:where(.swal2-container) button:where(.swal2-styled):focus-visible{outline:none}div:where(.swal2-container) button:where(.swal2-styled)::-moz-focus-inner{border:0}div:where(.swal2-container) div:where(.swal2-footer){margin:1em 0 0;padding:1em 1em 0;border-top:1px solid #eee;color:inherit;font-size:1em;text-align:center}div:where(.swal2-container) .swal2-timer-progress-bar-container{position:absolute;right:0;bottom:0;left:0;grid-column:auto !important;overflow:hidden;border-bottom-right-radius:5px;border-bottom-left-radius:5px}div:where(.swal2-container) div:where(.swal2-timer-progress-bar){width:100%;height:.25em;background:rgba(0,0,0,.2)}div:where(.swal2-container) img:where(.swal2-image){max-width:100%;margin:2em auto 1em}div:where(.swal2-container) button:where(.swal2-close){z-index:2;align-items:center;justify-content:center;width:1.2em;height:1.2em;margin-top:0;margin-right:0;margin-bottom:-1.2em;padding:0;overflow:hidden;transition:color .1s,box-shadow .1s;border:none;border-radius:5px;background:rgba(0,0,0,0);color:#ccc;font-family:monospace;font-size:2.5em;cursor:pointer;justify-self:end}div:where(.swal2-container) button:where(.swal2-close):hover{transform:none;background:rgba(0,0,0,0);color:#f27474}div:where(.swal2-container) button:where(.swal2-close):focus-visible{outline:none;box-shadow:inset 0 0 0 3px rgba(100,150,200,.5)}div:where(.swal2-container) button:where(.swal2-close)::-moz-focus-inner{border:0}div:where(.swal2-container) .swal2-html-container{z-index:1;justify-content:center;margin:0;padding:1em 1.6em .3em;overflow:auto;color:inherit;font-size:1.125em;font-weight:normal;line-height:normal;text-align:center;word-wrap:break-word;word-break:break-word}div:where(.swal2-container) input:where(.swal2-input),div:where(.swal2-container) input:where(.swal2-file),div:where(.swal2-container) textarea:where(.swal2-textarea),div:where(.swal2-container) select:where(.swal2-select),div:where(.swal2-container) div:where(.swal2-radio),div:where(.swal2-container) label:where(.swal2-checkbox){margin:1em 2em 3px}div:where(.swal2-container) input:where(.swal2-input),div:where(.swal2-container) input:where(.swal2-file),div:where(.swal2-container) textarea:where(.swal2-textarea){box-sizing:border-box;width:auto;transition:border-color .1s,box-shadow .1s;border:1px solid #d9d9d9;border-radius:.1875em;background:rgba(0,0,0,0);box-shadow:inset 0 1px 1px rgba(0,0,0,.06),0 0 0 3px rgba(0,0,0,0);color:inherit;font-size:1.125em}div:where(.swal2-container) input:where(.swal2-input).swal2-inputerror,div:where(.swal2-container) input:where(.swal2-file).swal2-inputerror,div:where(.swal2-container) textarea:where(.swal2-textarea).swal2-inputerror{border-color:#f27474 !important;box-shadow:0 0 2px #f27474 !important}div:where(.swal2-container) input:where(.swal2-input):focus,div:where(.swal2-container) input:where(.swal2-file):focus,div:where(.swal2-container) textarea:where(.swal2-textarea):focus{border:1px solid #b4dbed;outline:none;box-shadow:inset 0 1px 1px rgba(0,0,0,.06),0 0 0 3px rgba(100,150,200,.5)}div:where(.swal2-container) input:where(.swal2-input)::placeholder,div:where(.swal2-container) input:where(.swal2-file)::placeholder,div:where(.swal2-container) textarea:where(.swal2-textarea)::placeholder{color:#ccc}div:where(.swal2-container) .swal2-range{margin:1em 2em 3px;background:#fff}div:where(.swal2-container) .swal2-range input{width:80%}div:where(.swal2-container) .swal2-range output{width:20%;color:inherit;font-weight:600;text-align:center}div:where(.swal2-container) .swal2-range input,div:where(.swal2-container) .swal2-range output{height:2.625em;padding:0;font-size:1.125em;line-height:2.625em}div:where(.swal2-container) .swal2-input{height:2.625em;padding:0 .75em}div:where(.swal2-container) .swal2-file{width:75%;margin-right:auto;margin-left:auto;background:rgba(0,0,0,0);font-size:1.125em}div:where(.swal2-container) .swal2-textarea{height:6.75em;padding:.75em}div:where(.swal2-container) .swal2-select{min-width:50%;max-width:100%;padding:.375em .625em;background:rgba(0,0,0,0);color:inherit;font-size:1.125em}div:where(.swal2-container) .swal2-radio,div:where(.swal2-container) .swal2-checkbox{align-items:center;justify-content:center;background:#fff;color:inherit}div:where(.swal2-container) .swal2-radio label,div:where(.swal2-container) .swal2-checkbox label{margin:0 .6em;font-size:1.125em}div:where(.swal2-container) .swal2-radio input,div:where(.swal2-container) .swal2-checkbox input{flex-shrink:0;margin:0 .4em}div:where(.swal2-container) label:where(.swal2-input-label){display:flex;justify-content:center;margin:1em auto 0}div:where(.swal2-container) div:where(.swal2-validation-message){align-items:center;justify-content:center;margin:1em 0 0;padding:.625em;overflow:hidden;background:#f0f0f0;color:#666;font-size:1em;font-weight:300}div:where(.swal2-container) div:where(.swal2-validation-message)::before{content:\"!\";display:inline-block;width:1.5em;min-width:1.5em;height:1.5em;margin:0 .625em;border-radius:50%;background-color:#f27474;color:#fff;font-weight:600;line-height:1.5em;text-align:center}div:where(.swal2-container) .swal2-progress-steps{flex-wrap:wrap;align-items:center;max-width:100%;margin:1.25em auto;padding:0;background:rgba(0,0,0,0);font-weight:600}div:where(.swal2-container) .swal2-progress-steps li{display:inline-block;position:relative}div:where(.swal2-container) .swal2-progress-steps .swal2-progress-step{z-index:20;flex-shrink:0;width:2em;height:2em;border-radius:2em;background:#2778c4;color:#fff;line-height:2em;text-align:center}div:where(.swal2-container) .swal2-progress-steps .swal2-progress-step.swal2-active-progress-step{background:#2778c4}div:where(.swal2-container) .swal2-progress-steps .swal2-progress-step.swal2-active-progress-step~.swal2-progress-step{background:#add8e6;color:#fff}div:where(.swal2-container) .swal2-progress-steps .swal2-progress-step.swal2-active-progress-step~.swal2-progress-step-line{background:#add8e6}div:where(.swal2-container) .swal2-progress-steps .swal2-progress-step-line{z-index:10;flex-shrink:0;width:2.5em;height:.4em;margin:0 -1px;background:#2778c4}div:where(.swal2-icon){position:relative;box-sizing:content-box;justify-content:center;width:5em;height:5em;margin:2.5em auto .6em;border:0.25em solid rgba(0,0,0,0);border-radius:50%;border-color:#000;font-family:inherit;line-height:5em;cursor:default;user-select:none}div:where(.swal2-icon) .swal2-icon-content{display:flex;align-items:center;font-size:3.75em}div:where(.swal2-icon).swal2-error{border-color:#f27474;color:#f27474}div:where(.swal2-icon).swal2-error .swal2-x-mark{position:relative;flex-grow:1}div:where(.swal2-icon).swal2-error [class^=swal2-x-mark-line]{display:block;position:absolute;top:2.3125em;width:2.9375em;height:.3125em;border-radius:.125em;background-color:#f27474}div:where(.swal2-icon).swal2-error [class^=swal2-x-mark-line][class$=left]{left:1.0625em;transform:rotate(45deg)}div:where(.swal2-icon).swal2-error [class^=swal2-x-mark-line][class$=right]{right:1em;transform:rotate(-45deg)}div:where(.swal2-icon).swal2-error.swal2-icon-show{animation:swal2-animate-error-icon .5s}div:where(.swal2-icon).swal2-error.swal2-icon-show .swal2-x-mark{animation:swal2-animate-error-x-mark .5s}div:where(.swal2-icon).swal2-warning{border-color:#facea8;color:#f8bb86}div:where(.swal2-icon).swal2-warning.swal2-icon-show{animation:swal2-animate-error-icon .5s}div:where(.swal2-icon).swal2-warning.swal2-icon-show .swal2-icon-content{animation:swal2-animate-i-mark .5s}div:where(.swal2-icon).swal2-info{border-color:#9de0f6;color:#3fc3ee}div:where(.swal2-icon).swal2-info.swal2-icon-show{animation:swal2-animate-error-icon .5s}div:where(.swal2-icon).swal2-info.swal2-icon-show .swal2-icon-content{animation:swal2-animate-i-mark .8s}div:where(.swal2-icon).swal2-question{border-color:#c9dae1;color:#87adbd}div:where(.swal2-icon).swal2-question.swal2-icon-show{animation:swal2-animate-error-icon .5s}div:where(.swal2-icon).swal2-question.swal2-icon-show .swal2-icon-content{animation:swal2-animate-question-mark .8s}div:where(.swal2-icon).swal2-success{border-color:#a5dc86;color:#a5dc86}div:where(.swal2-icon).swal2-success [class^=swal2-success-circular-line]{position:absolute;width:3.75em;height:7.5em;border-radius:50%}div:where(.swal2-icon).swal2-success [class^=swal2-success-circular-line][class$=left]{top:-0.4375em;left:-2.0635em;transform:rotate(-45deg);transform-origin:3.75em 3.75em;border-radius:7.5em 0 0 7.5em}div:where(.swal2-icon).swal2-success [class^=swal2-success-circular-line][class$=right]{top:-0.6875em;left:1.875em;transform:rotate(-45deg);transform-origin:0 3.75em;border-radius:0 7.5em 7.5em 0}div:where(.swal2-icon).swal2-success .swal2-success-ring{position:absolute;z-index:2;top:-0.25em;left:-0.25em;box-sizing:content-box;width:100%;height:100%;border:.25em solid rgba(165,220,134,.3);border-radius:50%}div:where(.swal2-icon).swal2-success .swal2-success-fix{position:absolute;z-index:1;top:.5em;left:1.625em;width:.4375em;height:5.625em;transform:rotate(-45deg)}div:where(.swal2-icon).swal2-success [class^=swal2-success-line]{display:block;position:absolute;z-index:2;height:.3125em;border-radius:.125em;background-color:#a5dc86}div:where(.swal2-icon).swal2-success [class^=swal2-success-line][class$=tip]{top:2.875em;left:.8125em;width:1.5625em;transform:rotate(45deg)}div:where(.swal2-icon).swal2-success [class^=swal2-success-line][class$=long]{top:2.375em;right:.5em;width:2.9375em;transform:rotate(-45deg)}div:where(.swal2-icon).swal2-success.swal2-icon-show .swal2-success-line-tip{animation:swal2-animate-success-line-tip .75s}div:where(.swal2-icon).swal2-success.swal2-icon-show .swal2-success-line-long{animation:swal2-animate-success-line-long .75s}div:where(.swal2-icon).swal2-success.swal2-icon-show .swal2-success-circular-line-right{animation:swal2-rotate-success-circular-line 4.25s ease-in}[class^=swal2]{-webkit-tap-highlight-color:rgba(0,0,0,0)}.swal2-show{animation:swal2-show .3s}.swal2-hide{animation:swal2-hide .15s forwards}.swal2-noanimation{transition:none}.swal2-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.swal2-rtl .swal2-close{margin-right:initial;margin-left:0}.swal2-rtl .swal2-timer-progress-bar{right:0;left:auto}@keyframes swal2-toast-show{0%{transform:translateY(-0.625em) rotateZ(2deg)}33%{transform:translateY(0) rotateZ(-2deg)}66%{transform:translateY(0.3125em) rotateZ(2deg)}100%{transform:translateY(0) rotateZ(0deg)}}@keyframes swal2-toast-hide{100%{transform:rotateZ(1deg);opacity:0}}@keyframes swal2-toast-animate-success-line-tip{0%{top:.5625em;left:.0625em;width:0}54%{top:.125em;left:.125em;width:0}70%{top:.625em;left:-0.25em;width:1.625em}84%{top:1.0625em;left:.75em;width:.5em}100%{top:1.125em;left:.1875em;width:.75em}}@keyframes swal2-toast-animate-success-line-long{0%{top:1.625em;right:1.375em;width:0}65%{top:1.25em;right:.9375em;width:0}84%{top:.9375em;right:0;width:1.125em}100%{top:.9375em;right:.1875em;width:1.375em}}@keyframes swal2-show{0%{transform:scale(0.7)}45%{transform:scale(1.05)}80%{transform:scale(0.95)}100%{transform:scale(1)}}@keyframes swal2-hide{0%{transform:scale(1);opacity:1}100%{transform:scale(0.5);opacity:0}}@keyframes swal2-animate-success-line-tip{0%{top:1.1875em;left:.0625em;width:0}54%{top:1.0625em;left:.125em;width:0}70%{top:2.1875em;left:-0.375em;width:3.125em}84%{top:3em;left:1.3125em;width:1.0625em}100%{top:2.8125em;left:.8125em;width:1.5625em}}@keyframes swal2-animate-success-line-long{0%{top:3.375em;right:2.875em;width:0}65%{top:3.375em;right:2.875em;width:0}84%{top:2.1875em;right:0;width:3.4375em}100%{top:2.375em;right:.5em;width:2.9375em}}@keyframes swal2-rotate-success-circular-line{0%{transform:rotate(-45deg)}5%{transform:rotate(-45deg)}12%{transform:rotate(-405deg)}100%{transform:rotate(-405deg)}}@keyframes swal2-animate-error-x-mark{0%{margin-top:1.625em;transform:scale(0.4);opacity:0}50%{margin-top:1.625em;transform:scale(0.4);opacity:0}80%{margin-top:-0.375em;transform:scale(1.15)}100%{margin-top:0;transform:scale(1);opacity:1}}@keyframes swal2-animate-error-icon{0%{transform:rotateX(100deg);opacity:0}100%{transform:rotateX(0deg);opacity:1}}@keyframes swal2-rotate-loading{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@keyframes swal2-animate-question-mark{0%{transform:rotateY(-360deg)}100%{transform:rotateY(0)}}@keyframes swal2-animate-i-mark{0%{transform:rotateZ(45deg);opacity:0}25%{transform:rotateZ(-25deg);opacity:.4}50%{transform:rotateZ(15deg);opacity:.8}75%{transform:rotateZ(-5deg);opacity:1}100%{transform:rotateX(0);opacity:1}}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown){overflow:hidden}body.swal2-height-auto{height:auto !important}body.swal2-no-backdrop .swal2-container{background-color:rgba(0,0,0,0) !important;pointer-events:none}body.swal2-no-backdrop .swal2-container .swal2-popup{pointer-events:all}body.swal2-no-backdrop .swal2-container .swal2-modal{box-shadow:0 0 10px rgba(0,0,0,.4)}@media print{body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown){overflow-y:scroll !important}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown)>[aria-hidden=true]{display:none}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown) .swal2-container{position:static !important}}body.swal2-toast-shown .swal2-container{box-sizing:border-box;width:360px;max-width:100%;background-color:rgba(0,0,0,0);pointer-events:none}body.swal2-toast-shown .swal2-container.swal2-top{inset:0 auto auto 50%;transform:translateX(-50%)}body.swal2-toast-shown .swal2-container.swal2-top-end,body.swal2-toast-shown .swal2-container.swal2-top-right{inset:0 0 auto auto}body.swal2-toast-shown .swal2-container.swal2-top-start,body.swal2-toast-shown .swal2-container.swal2-top-left{inset:0 auto auto 0}body.swal2-toast-shown .swal2-container.swal2-center-start,body.swal2-toast-shown .swal2-container.swal2-center-left{inset:50% auto auto 0;transform:translateY(-50%)}body.swal2-toast-shown .swal2-container.swal2-center{inset:50% auto auto 50%;transform:translate(-50%, -50%)}body.swal2-toast-shown .swal2-container.swal2-center-end,body.swal2-toast-shown .swal2-container.swal2-center-right{inset:50% 0 auto auto;transform:translateY(-50%)}body.swal2-toast-shown .swal2-container.swal2-bottom-start,body.swal2-toast-shown .swal2-container.swal2-bottom-left{inset:auto auto 0 0}body.swal2-toast-shown .swal2-container.swal2-bottom{inset:auto auto 0 50%;transform:translateX(-50%)}body.swal2-toast-shown .swal2-container.swal2-bottom-end,body.swal2-toast-shown .swal2-container.swal2-bottom-right{inset:auto 0 0 auto}"); \ No newline at end of file diff --git a/internal/view/static/static_fs.go b/internal/view/static/static_fs.go index d1bb3a44..cc6e1967 100644 --- a/internal/view/static/static_fs.go +++ b/internal/view/static/static_fs.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/eduardolat/pgbackweb/internal/util/cryptoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" ) //go:embed * @@ -28,6 +29,9 @@ func GetStaticSHA256() string { // SHA256 hash of the static filesystem to the query parameter. // // The hash is truncated to the first 8 characters for brevity. +// +// This function also prepends the configured path prefix so that static files +// are correctly referenced in HTML when the application is served under a subpath. func GetVersionedFilePath(filePath string) string { hash := GetStaticSHA256() @@ -35,5 +39,9 @@ func GetVersionedFilePath(filePath string) string { hash = hash[:8] } - return filePath + "?v=" + hash + // Prepend the path prefix to ensure static files are found + // when the app is served under a subpath like /pgbackweb + fullPath := pathutil.BuildPath(filePath) + + return fullPath + "?v=" + hash } diff --git a/internal/view/web/auth/create_first_user.go b/internal/view/web/auth/create_first_user.go index 11d5e5d7..d0ec4905 100644 --- a/internal/view/web/auth/create_first_user.go +++ b/internal/view/web/auth/create_first_user.go @@ -6,6 +6,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -29,7 +30,7 @@ func (h *handlers) createFirstUserPageHandler(c echo.Context) error { return c.String(http.StatusInternalServerError, "Internal server error") } if usersQty > 0 { - return c.Redirect(http.StatusFound, "/auth/login") + return c.Redirect(http.StatusFound, pathutil.BuildPath("/auth/login")) } return echoutil.RenderNodx(c, http.StatusOK, createFirstUserPage()) @@ -40,7 +41,7 @@ func createFirstUserPage() nodx.Node { component.H1Text("Create first user"), nodx.FormEl( - htmx.HxPost("/auth/create-first-user"), + htmx.HxPost(pathutil.BuildPath("/auth/create-first-user")), htmx.HxDisabledELT("find button"), nodx.Class("mt-4 space-y-2"), @@ -52,6 +53,9 @@ func createFirstUserPage() nodx.Node { Required: true, Type: component.InputTypeText, AutoComplete: "name", + Children: []nodx.Node{ + nodx.Autofocus(""), + }, }), component.InputControl(component.InputControlParams{ @@ -135,6 +139,6 @@ func (h *handlers) createFirstUserHandler(c echo.Context) error { } return respondhtmx.AlertWithRedirect( - c, "User created successfully", "/auth/login", + c, "User created successfully", pathutil.BuildPath("/auth/login"), ) } diff --git a/internal/view/web/auth/login.go b/internal/view/web/auth/login.go index bde5aab6..ccd9402f 100644 --- a/internal/view/web/auth/login.go +++ b/internal/view/web/auth/login.go @@ -5,6 +5,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -28,7 +29,7 @@ func (h *handlers) loginPageHandler(c echo.Context) error { return c.String(http.StatusInternalServerError, "Internal server error") } if usersQty == 0 { - return c.Redirect(http.StatusFound, "/auth/create-first-user") + return c.Redirect(http.StatusFound, pathutil.BuildPath("/auth/create-first-user")) } return echoutil.RenderNodx(c, http.StatusOK, loginPage()) @@ -39,7 +40,7 @@ func loginPage() nodx.Node { component.H1Text("Login"), nodx.FormEl( - htmx.HxPost("/auth/login"), + htmx.HxPost(pathutil.BuildPath("/auth/login")), htmx.HxDisabledELT("find button"), nodx.Class("mt-4 space-y-2"), @@ -50,6 +51,9 @@ func loginPage() nodx.Node { Required: true, Type: component.InputTypeEmail, AutoComplete: "email", + Children: []nodx.Node{ + nodx.Autofocus(""), + }, }), component.InputControl(component.InputControlParams{ @@ -108,5 +112,5 @@ func (h *handlers) loginHandler(c echo.Context) error { } h.servs.AuthService.SetSessionCookie(c, session.DecryptedToken) - return respondhtmx.Redirect(c, "/dashboard") + return respondhtmx.Redirect(c, pathutil.BuildPath("/dashboard")) } diff --git a/internal/view/web/auth/logout.go b/internal/view/web/auth/logout.go index 81d3f30d..d70356e1 100644 --- a/internal/view/web/auth/logout.go +++ b/internal/view/web/auth/logout.go @@ -1,6 +1,7 @@ package auth import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/labstack/echo/v4" @@ -15,7 +16,7 @@ func (h *handlers) logoutHandler(c echo.Context) error { } h.servs.AuthService.ClearSessionCookie(c) - return respondhtmx.Redirect(c, "/auth/login") + return respondhtmx.Redirect(c, pathutil.BuildPath("/auth/login")) } func (h *handlers) logoutAllSessionsHandler(c echo.Context) error { @@ -28,5 +29,5 @@ func (h *handlers) logoutAllSessionsHandler(c echo.Context) error { } h.servs.AuthService.ClearSessionCookie(c) - return respondhtmx.Redirect(c, "/auth/login") + return respondhtmx.Redirect(c, pathutil.BuildPath("/auth/login")) } diff --git a/internal/view/web/component/logotype.go b/internal/view/web/component/logotype.go index 2518435f..cfe13678 100644 --- a/internal/view/web/component/logotype.go +++ b/internal/view/web/component/logotype.go @@ -1,6 +1,7 @@ package component import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" nodx "github.com/nodxdev/nodxgo" ) @@ -12,7 +13,7 @@ func Logotype() nodx.Node { }, nodx.Img( nodx.Class("w-[60px] h-auto"), - nodx.Src("/images/logo.png"), + nodx.Src(pathutil.BuildPath("/images/logo.png")), nodx.Alt("PG Back Web"), ), nodx.SpanEl( diff --git a/internal/view/web/component/support_project.inc.js b/internal/view/web/component/support_project.inc.js index fa348327..78c63f5a 100644 --- a/internal/view/web/component/support_project.inc.js +++ b/internal/view/web/component/support_project.inc.js @@ -55,6 +55,53 @@ window.alpineSupportProjectData = function () { } }, + prefixImagePath(path) { + // If the path starts with / and is not an absolute URL, add the path prefix + if ( + path && + path.startsWith("/") && + !path.startsWith("//") && + !path.startsWith("http://") && + !path.startsWith("https://") + ) { + return window.PBW_PATH_PREFIX + path; + } + return path; + }, + + processData(data) { + // Add path prefix to all image URLs + if (data.referralLinks) { + data.referralLinks = data.referralLinks.map((link) => ({ + ...link, + logo: this.prefixImagePath(link.logo), + })); + } + + if (data.sponsors) { + if (data.sponsors.gold) { + data.sponsors.gold = data.sponsors.gold.map((sponsor) => ({ + ...sponsor, + logo: this.prefixImagePath(sponsor.logo), + })); + } + if (data.sponsors.silver) { + data.sponsors.silver = data.sponsors.silver.map((sponsor) => ({ + ...sponsor, + logo: this.prefixImagePath(sponsor.logo), + })); + } + if (data.sponsors.bronze) { + data.sponsors.bronze = data.sponsors.bronze.map((sponsor) => ({ + ...sponsor, + logo: this.prefixImagePath(sponsor.logo), + })); + } + } + + return data; + }, + async getData() { const cacheKey = "pbw-support-project-data"; @@ -63,7 +110,7 @@ window.alpineSupportProjectData = function () { const cached = JSON.parse(cachedJSON); // Cache for 2 minutes if (Date.now() - cached.timestamp < 2 * 60 * 1000) { - return cached.data; + return this.processData(cached.data); } } @@ -80,7 +127,7 @@ window.alpineSupportProjectData = function () { timestamp: Date.now(), }); localStorage.setItem(cacheKey, dataToCache); - return data; + return this.processData(data); } catch { return null; } diff --git a/internal/view/web/dashboard/backups/create_backup.go b/internal/view/web/dashboard/backups/create_backup.go index 49e4c31b..6a71de31 100644 --- a/internal/view/web/dashboard/backups/create_backup.go +++ b/internal/view/web/dashboard/backups/create_backup.go @@ -7,6 +7,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/staticdata" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -70,7 +71,7 @@ func (h *handlers) createBackupHandler(c echo.Context) error { return respondhtmx.ToastError(c, err.Error()) } - return respondhtmx.Redirect(c, "/dashboard/backups") + return respondhtmx.Redirect(c, pathutil.BuildPath("/dashboard/backups")) } func (h *handlers) createBackupFormHandler(c echo.Context) error { @@ -105,7 +106,7 @@ func createBackupForm( serverTZ := time.Now().Location().String() return nodx.FormEl( - htmx.HxPost("/dashboard/backups"), + htmx.HxPost(pathutil.BuildPath("/dashboard/backups")), htmx.HxDisabledELT("find button"), nodx.Class("space-y-2 text-base"), @@ -322,7 +323,7 @@ func createBackupButton() nodx.Node { Title: "Create backup task", Content: []nodx.Node{ nodx.Div( - htmx.HxGet("/dashboard/backups/create-form"), + htmx.HxGet(pathutil.BuildPath("/dashboard/backups/create-form")), htmx.HxSwap("outerHTML"), htmx.HxTrigger("intersect once"), nodx.Class("p-10 flex justify-center"), diff --git a/internal/view/web/dashboard/backups/delete_backup.go b/internal/view/web/dashboard/backups/delete_backup.go index c2248b80..a3d841a2 100644 --- a/internal/view/web/dashboard/backups/delete_backup.go +++ b/internal/view/web/dashboard/backups/delete_backup.go @@ -1,6 +1,9 @@ package backups import ( + "fmt" + + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +30,7 @@ func (h *handlers) deleteBackupHandler(c echo.Context) error { func deleteBackupButton(backupID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete("/dashboard/backups/"+backupID.String()), + htmx.HxDelete(pathutil.BuildPath(fmt.Sprintf("/dashboard/backups/%s", backupID))), htmx.HxConfirm("Are you sure you want to delete this backup task?"), lucide.Trash(), component.SpanText("Delete backup task"), diff --git a/internal/view/web/dashboard/backups/duplicate_backup.go b/internal/view/web/dashboard/backups/duplicate_backup.go index a397a854..f60cd36b 100644 --- a/internal/view/web/dashboard/backups/duplicate_backup.go +++ b/internal/view/web/dashboard/backups/duplicate_backup.go @@ -1,6 +1,9 @@ package backups import ( + "fmt" + + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +30,7 @@ func (h *handlers) duplicateBackupHandler(c echo.Context) error { func duplicateBackupButton(backupID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost("/dashboard/backups/"+backupID.String()+"/duplicate"), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/backups/%s/duplicate", backupID))), htmx.HxConfirm("Are you sure you want to duplicate this backup task?"), lucide.CopyPlus(), component.SpanText("Duplicate backup task"), diff --git a/internal/view/web/dashboard/backups/edit_backup.go b/internal/view/web/dashboard/backups/edit_backup.go index 543aa85f..4a9e2bfe 100644 --- a/internal/view/web/dashboard/backups/edit_backup.go +++ b/internal/view/web/dashboard/backups/edit_backup.go @@ -6,6 +6,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/staticdata" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -90,7 +91,7 @@ func editBackupButton(backup dbgen.BackupsServicePaginateBackupsRow) nodx.Node { Title: "Edit backup task", Content: []nodx.Node{ nodx.FormEl( - htmx.HxPost("/dashboard/backups/"+backup.ID.String()+"/edit"), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/backups/%s/edit", backup.ID))), htmx.HxDisabledELT("find button"), nodx.Class("space-y-2 text-base"), diff --git a/internal/view/web/dashboard/backups/index.go b/internal/view/web/dashboard/backups/index.go index 59f1f482..d4eea595 100644 --- a/internal/view/web/dashboard/backups/index.go +++ b/internal/view/web/dashboard/backups/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -50,7 +51,7 @@ func indexPage(reqCtx reqctx.Ctx) nodx.Node { ), nodx.Tbody( component.SkeletonTr(8), - htmx.HxGet("/dashboard/backups/list?page=1"), + htmx.HxGet(pathutil.BuildPath("/dashboard/backups/list?page=1")), htmx.HxTrigger("load"), ), ), diff --git a/internal/view/web/dashboard/backups/list_backups.go b/internal/view/web/dashboard/backups/list_backups.go index 8483997b..b664b565 100644 --- a/internal/view/web/dashboard/backups/list_backups.go +++ b/internal/view/web/dashboard/backups/list_backups.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/backups" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" @@ -70,9 +71,9 @@ func listBackups( nodx.Td(component.OptionsDropdown( component.OptionsDropdownA( nodx.Class("btn btn-sm btn-ghost btn-square"), - nodx.Href( + nodx.Href(pathutil.BuildPath( fmt.Sprintf("/dashboard/executions?backup=%s", backup.ID), - ), + )), nodx.Target("_blank"), lucide.List(), component.SpanText("Show executions"), @@ -125,9 +126,9 @@ func listBackups( if pagination.HasNextPage { trs = append(trs, nodx.Tr( - htmx.HxGet(fmt.Sprintf( + htmx.HxGet(pathutil.BuildPath(fmt.Sprintf( "/dashboard/backups/list?page=%d", pagination.NextPage, - )), + ))), htmx.HxTrigger("intersect once"), htmx.HxSwap("afterend"), )) diff --git a/internal/view/web/dashboard/backups/manual_run.go b/internal/view/web/dashboard/backups/manual_run.go index 309d0655..61c64c5a 100644 --- a/internal/view/web/dashboard/backups/manual_run.go +++ b/internal/view/web/dashboard/backups/manual_run.go @@ -2,7 +2,9 @@ package backups import ( "context" + "fmt" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +29,7 @@ func (h *handlers) manualRunHandler(c echo.Context) error { func manualRunbutton(backupID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost("/dashboard/backups/"+backupID.String()+"/run"), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/backups/%s/run", backupID))), htmx.HxDisabledELT("this"), lucide.Zap(), component.SpanText("Run backup now"), diff --git a/internal/view/web/dashboard/databases/create_database.go b/internal/view/web/dashboard/databases/create_database.go index 88a65c16..47815afd 100644 --- a/internal/view/web/dashboard/databases/create_database.go +++ b/internal/view/web/dashboard/databases/create_database.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -41,13 +42,13 @@ func (h *handlers) createDatabaseHandler(c echo.Context) error { return respondhtmx.ToastError(c, err.Error()) } - return respondhtmx.Redirect(c, "/dashboard/databases") + return respondhtmx.Redirect(c, pathutil.BuildPath("/dashboard/databases")) } func createDatabaseButton() nodx.Node { htmxAttributes := func(url string) nodx.Node { return nodx.Group( - htmx.HxPost(url), + htmx.HxPost(pathutil.BuildPath(url)), htmx.HxInclude("#add-database-form"), htmx.HxDisabledELT(".add-database-btn"), htmx.HxIndicator("#add-database-loading"), diff --git a/internal/view/web/dashboard/databases/delete_database.go b/internal/view/web/dashboard/databases/delete_database.go index 4996d461..e2066c58 100644 --- a/internal/view/web/dashboard/databases/delete_database.go +++ b/internal/view/web/dashboard/databases/delete_database.go @@ -1,6 +1,9 @@ package databases import ( + "fmt" + + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +30,7 @@ func (h *handlers) deleteDatabaseHandler(c echo.Context) error { func deleteDatabaseButton(databaseID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete("/dashboard/databases/"+databaseID.String()), + htmx.HxDelete(pathutil.BuildPath(fmt.Sprintf("/dashboard/databases/%s", databaseID))), htmx.HxConfirm("Are you sure you want to delete this database?"), lucide.Trash(), component.SpanText("Delete database"), diff --git a/internal/view/web/dashboard/databases/edit_database.go b/internal/view/web/dashboard/databases/edit_database.go index 6503668e..04d028d5 100644 --- a/internal/view/web/dashboard/databases/edit_database.go +++ b/internal/view/web/dashboard/databases/edit_database.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -55,7 +56,7 @@ func editDatabaseButton( htmxAttributes := func(url string) nodx.Node { return nodx.Group( - htmx.HxPost(url), + htmx.HxPost(pathutil.BuildPath(url)), htmx.HxInclude("#"+formID), htmx.HxDisabledELT("."+btnClass), htmx.HxIndicator("#"+loadingID), diff --git a/internal/view/web/dashboard/databases/index.go b/internal/view/web/dashboard/databases/index.go index 6e440f98..44330d27 100644 --- a/internal/view/web/dashboard/databases/index.go +++ b/internal/view/web/dashboard/databases/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -42,7 +43,7 @@ func indexPage(reqCtx reqctx.Ctx) nodx.Node { ), nodx.Tbody( component.SkeletonTr(8), - htmx.HxGet("/dashboard/databases/list?page=1"), + htmx.HxGet(pathutil.BuildPath("/dashboard/databases/list?page=1")), htmx.HxTrigger("load"), ), ), diff --git a/internal/view/web/dashboard/databases/list_databases.go b/internal/view/web/dashboard/databases/list_databases.go index c4138c8f..28e2f6ed 100644 --- a/internal/view/web/dashboard/databases/list_databases.go +++ b/internal/view/web/dashboard/databases/list_databases.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/databases" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" @@ -64,16 +65,16 @@ func listDatabases( nodx.Div( nodx.Class("flex flex-col space-y-1"), component.OptionsDropdownA( - nodx.Href( + nodx.Href(pathutil.BuildPath( fmt.Sprintf("/dashboard/executions?database=%s", database.ID), - ), + )), nodx.Target("_blank"), lucide.List(), component.SpanText("Show executions"), ), editDatabaseButton(database), component.OptionsDropdownButton( - htmx.HxPost("/dashboard/databases/"+database.ID.String()+"/test"), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/databases/%s/test", database.ID))), htmx.HxDisabledELT("this"), lucide.DatabaseZap(), component.SpanText("Test connection"), @@ -104,9 +105,9 @@ func listDatabases( if pagination.HasNextPage { trs = append(trs, nodx.Tr( - htmx.HxGet(fmt.Sprintf( + htmx.HxGet(pathutil.BuildPath(fmt.Sprintf( "/dashboard/databases/list?page=%d", pagination.NextPage, - )), + ))), htmx.HxTrigger("intersect once"), htmx.HxSwap("afterend"), )) diff --git a/internal/view/web/dashboard/destinations/create_destination.go b/internal/view/web/dashboard/destinations/create_destination.go index ae0fe3b1..2f8d0c8e 100644 --- a/internal/view/web/dashboard/destinations/create_destination.go +++ b/internal/view/web/dashboard/destinations/create_destination.go @@ -2,6 +2,7 @@ package destinations import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -45,13 +46,13 @@ func (h *handlers) createDestinationHandler(c echo.Context) error { return respondhtmx.ToastError(c, err.Error()) } - return respondhtmx.Redirect(c, "/dashboard/destinations") + return respondhtmx.Redirect(c, pathutil.BuildPath("/dashboard/destinations")) } func createDestinationButton() nodx.Node { htmxAttributes := func(url string) nodx.Node { return nodx.Group( - htmx.HxPost(url), + htmx.HxPost(pathutil.BuildPath(url)), htmx.HxInclude("#add-destination-form"), htmx.HxDisabledELT(".add-destination-btn"), htmx.HxIndicator("#add-destination-loading"), diff --git a/internal/view/web/dashboard/destinations/delete_destination.go b/internal/view/web/dashboard/destinations/delete_destination.go index 80b65be2..a782813f 100644 --- a/internal/view/web/dashboard/destinations/delete_destination.go +++ b/internal/view/web/dashboard/destinations/delete_destination.go @@ -1,6 +1,9 @@ package destinations import ( + "fmt" + + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -28,7 +31,7 @@ func (h *handlers) deleteDestinationHandler(c echo.Context) error { func deleteDestinationButton(destinationID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete("/dashboard/destinations/"+destinationID.String()), + htmx.HxDelete(pathutil.BuildPath(fmt.Sprintf("/dashboard/destinations/%s", destinationID))), htmx.HxConfirm("Are you sure you want to delete this destination?"), lucide.Trash(), component.SpanText("Delete destination"), diff --git a/internal/view/web/dashboard/destinations/edit_destination.go b/internal/view/web/dashboard/destinations/edit_destination.go index 47629a9a..982b809b 100644 --- a/internal/view/web/dashboard/destinations/edit_destination.go +++ b/internal/view/web/dashboard/destinations/edit_destination.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -58,7 +59,7 @@ func editDestinationButton( htmxAttributes := func(url string) nodx.Node { return nodx.Group( - htmx.HxPost(url), + htmx.HxPost(pathutil.BuildPath(url)), htmx.HxInclude("#"+formID), htmx.HxDisabledELT("."+btnClass), htmx.HxIndicator("#"+loadingID), diff --git a/internal/view/web/dashboard/destinations/index.go b/internal/view/web/dashboard/destinations/index.go index 8355c611..25fb1c2c 100644 --- a/internal/view/web/dashboard/destinations/index.go +++ b/internal/view/web/dashboard/destinations/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -55,7 +56,7 @@ func indexPage(reqCtx reqctx.Ctx) nodx.Node { ), nodx.Tbody( component.SkeletonTr(8), - htmx.HxGet("/dashboard/destinations/list?page=1"), + htmx.HxGet(pathutil.BuildPath("/dashboard/destinations/list?page=1")), htmx.HxTrigger("load"), ), ), diff --git a/internal/view/web/dashboard/destinations/list_destinations.go b/internal/view/web/dashboard/destinations/list_destinations.go index b0eee351..b0534def 100644 --- a/internal/view/web/dashboard/destinations/list_destinations.go +++ b/internal/view/web/dashboard/destinations/list_destinations.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/destinations" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" @@ -62,16 +63,16 @@ func listDestinations( trs = append(trs, nodx.Tr( nodx.Td(component.OptionsDropdown( component.OptionsDropdownA( - nodx.Href( + nodx.Href(pathutil.BuildPath( fmt.Sprintf("/dashboard/executions?destination=%s", destination.ID), - ), + )), nodx.Target("_blank"), lucide.List(), component.SpanText("Show executions"), ), editDestinationButton(destination), component.OptionsDropdownButton( - htmx.HxPost("/dashboard/destinations/"+destination.ID.String()+"/test"), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/destinations/%s/test", destination.ID))), htmx.HxDisabledELT("this"), lucide.PlugZap(), component.SpanText("Test connection"), @@ -130,9 +131,9 @@ func listDestinations( if pagination.HasNextPage { trs = append(trs, nodx.Tr( - htmx.HxGet(fmt.Sprintf( + htmx.HxGet(pathutil.BuildPath(fmt.Sprintf( "/dashboard/destinations/list?page=%d", pagination.NextPage, - )), + ))), htmx.HxTrigger("intersect once"), htmx.HxSwap("afterend"), )) diff --git a/internal/view/web/dashboard/executions/index.go b/internal/view/web/dashboard/executions/index.go index 5c57b5cb..7216be59 100644 --- a/internal/view/web/dashboard/executions/index.go +++ b/internal/view/web/dashboard/executions/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/strutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/reqctx" @@ -61,7 +62,7 @@ func indexPage(reqCtx reqctx.Ctx, queryData execsQueryData) nodx.Node { nodx.Tbody( component.SkeletonTr(8), htmx.HxGet(func() string { - url := "/dashboard/executions/list?page=1" + url := pathutil.BuildPath("/dashboard/executions/list?page=1") if queryData.Database != uuid.Nil { url = strutil.AddQueryParamToUrl(url, "database", queryData.Database.String()) } diff --git a/internal/view/web/dashboard/executions/list_executions.go b/internal/view/web/dashboard/executions/list_executions.go index 04e0199c..c6b8b3e0 100644 --- a/internal/view/web/dashboard/executions/list_executions.go +++ b/internal/view/web/dashboard/executions/list_executions.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/executions" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/strutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" @@ -117,7 +118,7 @@ func listExecutions( if pagination.HasNextPage { trs = append(trs, nodx.Tr( htmx.HxGet(func() string { - url := "/dashboard/executions/list" + url := pathutil.BuildPath("/dashboard/executions/list") url = strutil.AddQueryParamToUrl(url, "page", fmt.Sprintf("%d", pagination.NextPage)) if queryData.Database != uuid.Nil { url = strutil.AddQueryParamToUrl(url, "database", queryData.Database.String()) diff --git a/internal/view/web/dashboard/executions/restore_execution.go b/internal/view/web/dashboard/executions/restore_execution.go index 45494f0d..d1acd334 100644 --- a/internal/view/web/dashboard/executions/restore_execution.go +++ b/internal/view/web/dashboard/executions/restore_execution.go @@ -7,6 +7,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -107,7 +108,7 @@ func restoreExecutionForm( databases []dbgen.DatabasesServiceGetAllDatabasesRow, ) nodx.Node { return nodx.FormEl( - htmx.HxPost("/dashboard/executions/"+execution.ID.String()+"/restore"), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/executions/%s/restore", execution.ID))), htmx.HxConfirm("Are you sure you want to restore this backup?"), htmx.HxDisabledELT("find button"), @@ -222,7 +223,7 @@ func restoreExecutionButton(execution dbgen.ExecutionsServicePaginateExecutionsR Title: "Restore backup execution", Content: []nodx.Node{ nodx.Div( - htmx.HxGet("/dashboard/executions/"+execution.ID.String()+"/restore-form"), + htmx.HxGet(pathutil.BuildPath(fmt.Sprintf("/dashboard/executions/%s/restore-form", execution.ID))), htmx.HxSwap("outerHTML"), htmx.HxTrigger("intersect once"), nodx.Class("p-10 flex justify-center"), diff --git a/internal/view/web/dashboard/executions/show_execution.go b/internal/view/web/dashboard/executions/show_execution.go index f8224eaa..24183d87 100644 --- a/internal/view/web/dashboard/executions/show_execution.go +++ b/internal/view/web/dashboard/executions/show_execution.go @@ -1,10 +1,12 @@ package executions import ( + "fmt" "net/http" "path/filepath" "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/google/uuid" @@ -121,7 +123,7 @@ func showExecutionButton( nodx.Class("flex justify-end items-center space-x-2"), deleteExecutionButton(execution.ID), nodx.A( - nodx.Href("/dashboard/executions/"+execution.ID.String()+"/download"), + nodx.Href(pathutil.BuildPath(fmt.Sprintf("/dashboard/executions/%s/download", execution.ID))), nodx.Target("_blank"), nodx.Class("btn btn-primary"), component.SpanText("Download"), diff --git a/internal/view/web/dashboard/executions/soft_delete_execution.go b/internal/view/web/dashboard/executions/soft_delete_execution.go index e62abaf5..847345f6 100644 --- a/internal/view/web/dashboard/executions/soft_delete_execution.go +++ b/internal/view/web/dashboard/executions/soft_delete_execution.go @@ -1,6 +1,9 @@ package executions import ( + "fmt" + + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -28,7 +31,7 @@ func (h *handlers) deleteExecutionHandler(c echo.Context) error { func deleteExecutionButton(executionID uuid.UUID) nodx.Node { return nodx.Button( - htmx.HxDelete("/dashboard/executions/"+executionID.String()), + htmx.HxDelete(pathutil.BuildPath(fmt.Sprintf("/dashboard/executions/%s", executionID))), htmx.HxDisabledELT("this"), htmx.HxConfirm("Are you sure you want to delete this execution? It will delete the backup file from the destination and it can't be recovered."), nodx.Class("btn btn-error btn-outline"), diff --git a/internal/view/web/dashboard/profile/close_all_sessions.go b/internal/view/web/dashboard/profile/close_all_sessions.go index ad3cfe93..1f09ea6b 100644 --- a/internal/view/web/dashboard/profile/close_all_sessions.go +++ b/internal/view/web/dashboard/profile/close_all_sessions.go @@ -2,6 +2,7 @@ package profile import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" nodx "github.com/nodxdev/nodxgo" @@ -15,7 +16,7 @@ func closeAllSessionsForm(sessions []dbgen.Session) nodx.Node { component.H2Text("Close all sessions"), component.PText("This will log you out from all devices including this one."), nodx.Button( - htmx.HxPost("/auth/logout-all"), + htmx.HxPost(pathutil.BuildPath("/auth/logout-all")), htmx.HxDisabledELT("this"), htmx.HxConfirm("Are you sure you want to close all your sessions?"), nodx.Class("mt-2 btn btn-error"), diff --git a/internal/view/web/dashboard/profile/update_user.go b/internal/view/web/dashboard/profile/update_user.go index 2e4f84be..63334023 100644 --- a/internal/view/web/dashboard/profile/update_user.go +++ b/internal/view/web/dashboard/profile/update_user.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/component" @@ -48,7 +49,7 @@ func updateUserForm(user dbgen.User) nodx.Node { return component.CardBox(component.CardBoxParams{ Children: []nodx.Node{ nodx.FormEl( - htmx.HxPost("/dashboard/profile"), + htmx.HxPost(pathutil.BuildPath("/dashboard/profile")), htmx.HxDisabledELT("find button"), nodx.Class("space-y-2"), diff --git a/internal/view/web/dashboard/restorations/index.go b/internal/view/web/dashboard/restorations/index.go index 2b11cc41..2c4bc18c 100644 --- a/internal/view/web/dashboard/restorations/index.go +++ b/internal/view/web/dashboard/restorations/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/strutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/reqctx" @@ -66,7 +67,7 @@ func indexPage(reqCtx reqctx.Ctx, queryData resQueryData) nodx.Node { nodx.Tbody( component.SkeletonTr(8), htmx.HxGet(func() string { - url := "/dashboard/restorations/list?page=1" + url := pathutil.BuildPath("/dashboard/restorations/list?page=1") if queryData.Execution != uuid.Nil { url = strutil.AddQueryParamToUrl(url, "execution", queryData.Execution.String()) } diff --git a/internal/view/web/dashboard/restorations/list_restorations.go b/internal/view/web/dashboard/restorations/list_restorations.go index ec330b75..5a3a11dc 100644 --- a/internal/view/web/dashboard/restorations/list_restorations.go +++ b/internal/view/web/dashboard/restorations/list_restorations.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/restorations" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/strutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" @@ -109,7 +110,7 @@ func listRestorations( if pagination.HasNextPage { trs = append(trs, nodx.Tr( htmx.HxGet(func() string { - url := "/dashboard/restorations/list" + url := pathutil.BuildPath("/dashboard/restorations/list") url = strutil.AddQueryParamToUrl(url, "page", fmt.Sprintf("%d", pagination.NextPage)) if queryData.Execution != uuid.Nil { url = strutil.AddQueryParamToUrl(url, "execution", queryData.Execution.String()) diff --git a/internal/view/web/dashboard/webhooks/create_webhook.go b/internal/view/web/dashboard/webhooks/create_webhook.go index 73c73a64..691acf4a 100644 --- a/internal/view/web/dashboard/webhooks/create_webhook.go +++ b/internal/view/web/dashboard/webhooks/create_webhook.go @@ -6,6 +6,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -54,7 +55,7 @@ func (h *handlers) createWebhookHandler(c echo.Context) error { return respondhtmx.ToastError(c, err.Error()) } - return respondhtmx.Redirect(c, "/dashboard/webhooks") + return respondhtmx.Redirect(c, pathutil.BuildPath("/dashboard/webhooks")) } func (h *handlers) createWebhookFormHandler(c echo.Context) error { @@ -86,7 +87,7 @@ func createWebhookForm( backups []dbgen.Backup, ) nodx.Node { return nodx.FormEl( - htmx.HxPost("/dashboard/webhooks/create"), + htmx.HxPost(pathutil.BuildPath("/dashboard/webhooks/create")), htmx.HxDisabledELT("find button[type='submit']"), nodx.Class("space-y-2"), @@ -111,7 +112,7 @@ func createWebhookButton() nodx.Node { Title: "Create webhook", Content: []nodx.Node{ nodx.Div( - htmx.HxGet("/dashboard/webhooks/create"), + htmx.HxGet(pathutil.BuildPath("/dashboard/webhooks/create")), htmx.HxSwap("outerHTML"), htmx.HxTrigger("intersect once"), nodx.Class("p-10 flex justify-center"), diff --git a/internal/view/web/dashboard/webhooks/delete_webhook.go b/internal/view/web/dashboard/webhooks/delete_webhook.go index 1f93341d..516d1055 100644 --- a/internal/view/web/dashboard/webhooks/delete_webhook.go +++ b/internal/view/web/dashboard/webhooks/delete_webhook.go @@ -1,6 +1,9 @@ package webhooks import ( + "fmt" + + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +30,7 @@ func (h *handlers) deleteWebhookHandler(c echo.Context) error { func deleteWebhookButton(webhookID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete("/dashboard/webhooks/"+webhookID.String()), + htmx.HxDelete(pathutil.BuildPath(fmt.Sprintf("/dashboard/webhooks/%s", webhookID))), htmx.HxConfirm("Are you sure you want to delete this webhook?"), lucide.Trash(), component.SpanText("Delete webhook"), diff --git a/internal/view/web/dashboard/webhooks/duplicate_webhook.go b/internal/view/web/dashboard/webhooks/duplicate_webhook.go index 6f2684ac..30c73d74 100644 --- a/internal/view/web/dashboard/webhooks/duplicate_webhook.go +++ b/internal/view/web/dashboard/webhooks/duplicate_webhook.go @@ -1,6 +1,9 @@ package webhooks import ( + "fmt" + + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +30,7 @@ func (h *handlers) duplicateWebhookHandler(c echo.Context) error { func duplicateWebhookButton(webhookID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost("/dashboard/webhooks/"+webhookID.String()+"/duplicate"), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/webhooks/%s/duplicate", webhookID))), htmx.HxConfirm("Are you sure you want to duplicate this webhook?"), lucide.CopyPlus(), component.SpanText("Duplicate webhook"), diff --git a/internal/view/web/dashboard/webhooks/edit_webhook.go b/internal/view/web/dashboard/webhooks/edit_webhook.go index c19a0fc9..615567af 100644 --- a/internal/view/web/dashboard/webhooks/edit_webhook.go +++ b/internal/view/web/dashboard/webhooks/edit_webhook.go @@ -2,10 +2,12 @@ package webhooks import ( "database/sql" + "fmt" "net/http" "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -101,7 +103,7 @@ func editWebhookForm( backups []dbgen.Backup, ) nodx.Node { return nodx.FormEl( - htmx.HxPost("/dashboard/webhooks/"+webhook.ID.String()+"/edit"), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/webhooks/%s/edit", webhook.ID))), htmx.HxDisabledELT("find button[type='submit']"), nodx.Class("space-y-2"), @@ -126,7 +128,7 @@ func editWebhookButton(webhookID uuid.UUID) nodx.Node { Title: "Edit webhook", Content: []nodx.Node{ nodx.Div( - htmx.HxGet("/dashboard/webhooks/"+webhookID.String()+"/edit"), + htmx.HxGet(pathutil.BuildPath(fmt.Sprintf("/dashboard/webhooks/%s/edit", webhookID))), htmx.HxSwap("outerHTML"), htmx.HxTrigger("intersect once"), nodx.Class("p-10 flex justify-center"), diff --git a/internal/view/web/dashboard/webhooks/index.go b/internal/view/web/dashboard/webhooks/index.go index 06c50057..f46a5127 100644 --- a/internal/view/web/dashboard/webhooks/index.go +++ b/internal/view/web/dashboard/webhooks/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -45,7 +46,7 @@ func indexPage(reqCtx reqctx.Ctx) nodx.Node { ), nodx.Tbody( component.SkeletonTr(8), - htmx.HxGet("/dashboard/webhooks/list?page=1"), + htmx.HxGet(pathutil.BuildPath("/dashboard/webhooks/list?page=1")), htmx.HxTrigger("load"), ), ), diff --git a/internal/view/web/dashboard/webhooks/list_webhooks.go b/internal/view/web/dashboard/webhooks/list_webhooks.go index d9b22fac..abb04277 100644 --- a/internal/view/web/dashboard/webhooks/list_webhooks.go +++ b/internal/view/web/dashboard/webhooks/list_webhooks.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/webhooks" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/strutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" @@ -92,7 +93,7 @@ func listWebhooks( if pagination.HasNextPage { trs = append(trs, nodx.Tr( htmx.HxGet(func() string { - url := "/dashboard/webhooks/list" + url := pathutil.BuildPath("/dashboard/webhooks/list") url = strutil.AddQueryParamToUrl(url, "page", fmt.Sprintf("%d", pagination.NextPage)) return url }()), diff --git a/internal/view/web/dashboard/webhooks/run_webhook.go b/internal/view/web/dashboard/webhooks/run_webhook.go index 690dd439..95234ba4 100644 --- a/internal/view/web/dashboard/webhooks/run_webhook.go +++ b/internal/view/web/dashboard/webhooks/run_webhook.go @@ -2,8 +2,10 @@ package webhooks import ( "context" + "fmt" "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -43,7 +45,7 @@ func (h *handlers) runWebhookHandler(c echo.Context) error { func runWebhookButton(webhookID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost("/dashboard/webhooks/"+webhookID.String()+"/run"), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/webhooks/%s/run", webhookID))), htmx.HxDisabledELT("this"), lucide.Zap(), component.SpanText("Run webhook now"), diff --git a/internal/view/web/dashboard/webhooks/webhook_executions.go b/internal/view/web/dashboard/webhooks/webhook_executions.go index c4a9a194..c3812cd5 100644 --- a/internal/view/web/dashboard/webhooks/webhook_executions.go +++ b/internal/view/web/dashboard/webhooks/webhook_executions.go @@ -9,6 +9,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/webhooks" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/strutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" @@ -88,7 +89,7 @@ func webhookExecutionsList( if pagination.HasNextPage { trs = append(trs, nodx.Tr( htmx.HxGet(func() string { - url := "/dashboard/webhooks/" + webhookID.String() + "/executions" + url := pathutil.BuildPath("/dashboard/webhooks/" + webhookID.String() + "/executions") url = strutil.AddQueryParamToUrl(url, "page", fmt.Sprintf("%d", pagination.NextPage)) return url }()), @@ -258,7 +259,7 @@ func webhookExecutionsButton(webhookID uuid.UUID) nodx.Node { ), nodx.Tbody( htmx.HxGet( - "/dashboard/webhooks/"+webhookID.String()+"/executions?page=1", + pathutil.BuildPath("/dashboard/webhooks/"+webhookID.String()+"/executions?page=1"), ), htmx.HxIndicator("#webhook-executions-loading"), htmx.HxTrigger("intersect once"), diff --git a/internal/view/web/layout/common.go b/internal/view/web/layout/common.go index 81fa70ca..4cbb253f 100644 --- a/internal/view/web/layout/common.go +++ b/internal/view/web/layout/common.go @@ -1,6 +1,7 @@ package layout import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/static" nodx "github.com/nodxdev/nodxgo" ) @@ -32,6 +33,9 @@ func commonHead() nodx.Node { nodx.Meta(nodx.Charset("utf-8")), nodx.Meta(nodx.Name("viewport"), nodx.Content("width=device-width, initial-scale=1")), + // Inject path prefix as global JavaScript variable + nodx.Script(nodx.Rawf("window.PBW_PATH_PREFIX = '%s';", pathutil.GetPathPrefix())), + // https://htmx.org/quirks/ nodx.Meta(nodx.Name("htmx-config"), nodx.Content(`{"disableInheritance":true, "responseHandling": [{"code":"...", "swap": true}]}`)), @@ -41,7 +45,6 @@ func commonHead() nodx.Node { nodx.Script(src("/libs/htmx/htmx-2.0.1.min.js"), nodx.Defer("")), nodx.Script(src("/libs/alpinejs/alpinejs-3.14.1.min.js"), nodx.Defer("")), - nodx.Script(src("/libs/sweetalert2/sweetalert2-11.13.1.min.js")), nodx.Script(src("/libs/chartjs/chartjs-4.4.3.umd.min.js")), nodx.Link(nodx.Rel("stylesheet"), href("/libs/notyf/notyf-3.10.0.min.css")), diff --git a/internal/view/web/layout/dashboard_aside.go b/internal/view/web/layout/dashboard_aside.go index 9f71fcc3..25459db1 100644 --- a/internal/view/web/layout/dashboard_aside.go +++ b/internal/view/web/layout/dashboard_aside.go @@ -3,6 +3,7 @@ package layout import ( "fmt" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" nodx "github.com/nodxdev/nodxgo" alpine "github.com/nodxdev/nodxgo-alpine" htmx "github.com/nodxdev/nodxgo-htmx" @@ -22,7 +23,7 @@ func dashboardAside() nodx.Node { nodx.Href("https://github.com/eduardolat/pgbackweb"), nodx.Target("_blank"), nodx.Img( - nodx.Src("/images/logo.png"), + nodx.Src(pathutil.BuildPath("/images/logo.png")), nodx.Alt("PG Back Web"), nodx.Class("w-[50px] h-auto"), ), @@ -45,63 +46,63 @@ func dashboardAside() nodx.Node { dashboardAsideItem( lucide.LayoutDashboard, "Summary", - "/dashboard", + pathutil.BuildPath("/dashboard"), true, ), dashboardAsideItem( lucide.Database, "Databases", - "/dashboard/databases", + pathutil.BuildPath("/dashboard/databases"), false, ), dashboardAsideItem( lucide.HardDrive, "Destinations", - "/dashboard/destinations", + pathutil.BuildPath("/dashboard/destinations"), false, ), dashboardAsideItem( lucide.DatabaseBackup, "Backup tasks", - "/dashboard/backups", + pathutil.BuildPath("/dashboard/backups"), false, ), dashboardAsideItem( lucide.List, "Executions", - "/dashboard/executions", + pathutil.BuildPath("/dashboard/executions"), false, ), dashboardAsideItem( lucide.ArchiveRestore, "Restorations", - "/dashboard/restorations", + pathutil.BuildPath("/dashboard/restorations"), false, ), dashboardAsideItem( lucide.Webhook, "Webhooks", - "/dashboard/webhooks", + pathutil.BuildPath("/dashboard/webhooks"), false, ), dashboardAsideItem( lucide.User, "Profile", - "/dashboard/profile", + pathutil.BuildPath("/dashboard/profile"), false, ), dashboardAsideItem( lucide.Info, "About", - "/dashboard/about", + pathutil.BuildPath("/dashboard/about"), false, ), ), diff --git a/internal/view/web/layout/dashboard_header.go b/internal/view/web/layout/dashboard_header.go index c929161d..74c863ce 100644 --- a/internal/view/web/layout/dashboard_header.go +++ b/internal/view/web/layout/dashboard_header.go @@ -1,6 +1,7 @@ package layout import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" nodx "github.com/nodxdev/nodxgo" htmx "github.com/nodxdev/nodxgo-htmx" @@ -28,19 +29,19 @@ func dashboardHeader() nodx.Node { nodx.Div( nodx.Class("flex justify-end items-center space-x-2"), nodx.Div( - htmx.HxGet("/dashboard/health-button"), + htmx.HxGet(pathutil.BuildPath("/dashboard/health-button")), htmx.HxSwap("outerHTML"), htmx.HxTrigger("load once"), ), nodx.A( - nodx.Href("https://discord.gg/BmAwq29UZ8"), + nodx.Href("https://ufobackup.uforg.dev/r/community"), nodx.Target("_blank"), nodx.Class("btn btn-ghost btn-neutral"), - component.SpanText("Chat on Discord"), + component.SpanText("Join the community"), lucide.ExternalLink(), ), nodx.Button( - htmx.HxPost("/auth/logout"), + htmx.HxPost(pathutil.BuildPath("/auth/logout")), htmx.HxDisabledELT("this"), nodx.Class("btn btn-ghost btn-neutral"), component.SpanText("Log out"), diff --git a/internal/view/web/router.go b/internal/view/web/router.go index 2b0ad1b2..ea76b2a8 100644 --- a/internal/view/web/router.go +++ b/internal/view/web/router.go @@ -5,6 +5,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/service" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/middleware" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/auth" @@ -21,7 +22,7 @@ func MountRouter( reqCtx := reqctx.GetCtx(c) if reqCtx.IsAuthed { - return c.Redirect(http.StatusFound, "/dashboard") + return c.Redirect(http.StatusFound, pathutil.BuildPath("/dashboard")) } usersQty, err := servs.UsersService.GetUsersQty(ctx) @@ -35,10 +36,10 @@ func MountRouter( } if usersQty == 0 { - return c.Redirect(http.StatusFound, "/auth/create-first-user") + return c.Redirect(http.StatusFound, pathutil.BuildPath("/auth/create-first-user")) } - return c.Redirect(http.StatusFound, "/auth/login") + return c.Redirect(http.StatusFound, pathutil.BuildPath("/auth/login")) }) authGroup := parent.Group("/auth") diff --git a/tailwind.config.ts b/tailwind.config.ts index c3fdb699..b19070cc 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -3,7 +3,10 @@ import daisyui from "daisyui"; import * as daisyuiThemes from "daisyui/src/theming/themes"; export default { - content: ["./internal/view/web/**/*.go"], + content: [ + "./internal/view/web/**/*.go", + "./internal/view/static/js/init-dialogs.js", + ], plugins: [daisyui as any], daisyui: { logs: false,