Version 0.02 from scratch
- commit
- dfcd2efb39add2618adf315934c9dd285950fff3
- author
- bnema · · 2023-09-11 16:44 UTC
- committer
- bnema · · 2023-09-11 19:05 UTC
- parent
- 1df4e7b
- signature
- unsigned
69 files · +0 -4018
this diff is incomplete: 2 files are too large to show. the totals above count everything; clone the repository to read the rest.
D cmd/main.go +0 -46
| @@ -1,46 +0,0 @@ | |||
| 1 | - | package main | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "os" | |
| 5 | - | ||
| 6 | - | "github.com/labstack/echo/v4" | |
| 7 | - | root "gogs.bnema.dev/gordon-echo" | |
| 8 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |
| 9 | - | "gogs.bnema.dev/gordon-echo/internal/http/routes" | |
| 10 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |
| 11 | - | ) | |
| 12 | - | ||
| 13 | - | func main() { | |
| 14 | - | // Port = os.Getenv("PORT") or fallback to default port (1323) | |
| 15 | - | port := os.Getenv("PORT") | |
| 16 | - | if port == "" { | |
| 17 | - | port = "1323" | |
| 18 | - | } | |
| 19 | - | // Initialize Gordon | |
| 20 | - | gordon, err := app.NewApp( | |
| 21 | - | &app.Config{ | |
| 22 | - | TemplateFS: root.TemplateFS, | |
| 23 | - | PublicFS: root.PublicFS, | |
| 24 | - | ModelFS: root.ModelFS, | |
| 25 | - | }, | |
| 26 | - | ) | |
| 27 | - | if err != nil { | |
| 28 | - | // Handle initialization error and exit | |
| 29 | - | gordon.AppLogger.Error().Err(err).Msg("Failed to initialize app") | |
| 30 | - | return | |
| 31 | - | } | |
| 32 | - | utils.CaptureSTDOUT(gordon.AppLogger) | |
| 33 | - | ||
| 34 | - | e := echo.New() | |
| 35 | - | e.HideBanner = true | |
| 36 | - | e.HidePort = true | |
| 37 | - | e.Logger = utils.NewEchoLoggerWrapper(gordon.AppLogger) | |
| 38 | - | e = routes.ConfigureRouter(e, gordon, gordon.Config) | |
| 39 | - | ||
| 40 | - | startFunc := func() error { | |
| 41 | - | gordon.AppLogger.Info().Msgf("Server is listening on port %s", port) | |
| 42 | - | return e.Start(":" + port) | |
| 43 | - | } | |
| 44 | - | utils.RunAppCatchSIGINT(startFunc, &gordon.AppLogger.Logger) | |
| 45 | - | ||
| 46 | - | } | |
| @@ -1,46 +0,0 @@ | |||||
| 1 | - | package main | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "os" | |||
| 5 | - | ||||
| 6 | - | "github.com/labstack/echo/v4" | |||
| 7 | - | root "gogs.bnema.dev/gordon-echo" | |||
| 8 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |||
| 9 | - | "gogs.bnema.dev/gordon-echo/internal/http/routes" | |||
| 10 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |||
| 11 | - | ) | |||
| 12 | - | ||||
| 13 | - | func main() { | |||
| 14 | - | // Port = os.Getenv("PORT") or fallback to default port (1323) | |||
| 15 | - | port := os.Getenv("PORT") | |||
| 16 | - | if port == "" { | |||
| 17 | - | port = "1323" | |||
| 18 | - | } | |||
| 19 | - | // Initialize Gordon | |||
| 20 | - | gordon, err := app.NewApp( | |||
| 21 | - | &app.Config{ | |||
| 22 | - | TemplateFS: root.TemplateFS, | |||
| 23 | - | PublicFS: root.PublicFS, | |||
| 24 | - | ModelFS: root.ModelFS, | |||
| 25 | - | }, | |||
| 26 | - | ) | |||
| 27 | - | if err != nil { | |||
| 28 | - | // Handle initialization error and exit | |||
| 29 | - | gordon.AppLogger.Error().Err(err).Msg("Failed to initialize app") | |||
| 30 | - | return | |||
| 31 | - | } | |||
| 32 | - | utils.CaptureSTDOUT(gordon.AppLogger) | |||
| 33 | - | ||||
| 34 | - | e := echo.New() | |||
| 35 | - | e.HideBanner = true | |||
| 36 | - | e.HidePort = true | |||
| 37 | - | e.Logger = utils.NewEchoLoggerWrapper(gordon.AppLogger) | |||
| 38 | - | e = routes.ConfigureRouter(e, gordon, gordon.Config) | |||
| 39 | - | ||||
| 40 | - | startFunc := func() error { | |||
| 41 | - | gordon.AppLogger.Info().Msgf("Server is listening on port %s", port) | |||
| 42 | - | return e.Start(":" + port) | |||
| 43 | - | } | |||
| 44 | - | utils.RunAppCatchSIGINT(startFunc, &gordon.AppLogger.Logger) | |||
| 45 | - | ||||
| 46 | - | } | |||
D embed.go +0 -22
| @@ -1,22 +0,0 @@ | |||
| 1 | - | package embed | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "embed" | |
| 5 | - | ||
| 6 | - | "github.com/labstack/echo/v4" | |
| 7 | - | ) | |
| 8 | - | ||
| 9 | - | // Embedding the public and templates directories | |
| 10 | - | ||
| 11 | - | //go:embed internal/ui/public/* | |
| 12 | - | var public embed.FS | |
| 13 | - | ||
| 14 | - | //go:embed internal/ui/templates/* | |
| 15 | - | var templates embed.FS | |
| 16 | - | ||
| 17 | - | //go:embed pkg/templating/models/* | |
| 18 | - | var models embed.FS | |
| 19 | - | ||
| 20 | - | var PublicFS = echo.MustSubFS(public, "internal/ui/public") | |
| 21 | - | var TemplateFS = echo.MustSubFS(templates, "internal/ui/templates") | |
| 22 | - | var ModelFS = echo.MustSubFS(models, "pkg/templating/models") | |
| @@ -1,22 +0,0 @@ | |||||
| 1 | - | package embed | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "embed" | |||
| 5 | - | ||||
| 6 | - | "github.com/labstack/echo/v4" | |||
| 7 | - | ) | |||
| 8 | - | ||||
| 9 | - | // Embedding the public and templates directories | |||
| 10 | - | ||||
| 11 | - | //go:embed internal/ui/public/* | |||
| 12 | - | var public embed.FS | |||
| 13 | - | ||||
| 14 | - | //go:embed internal/ui/templates/* | |||
| 15 | - | var templates embed.FS | |||
| 16 | - | ||||
| 17 | - | //go:embed pkg/templating/models/* | |||
| 18 | - | var models embed.FS | |||
| 19 | - | ||||
| 20 | - | var PublicFS = echo.MustSubFS(public, "internal/ui/public") | |||
| 21 | - | var TemplateFS = echo.MustSubFS(templates, "internal/ui/templates") | |||
| 22 | - | var ModelFS = echo.MustSubFS(models, "pkg/templating/models") | |||
D go.mod +0 -58
| @@ -1,58 +0,0 @@ | |||
| 1 | - | module gogs.bnema.dev/gordon-echo | |
| 2 | - | ||
| 3 | - | go 1.21.0 | |
| 4 | - | ||
| 5 | - | require ( | |
| 6 | - | github.com/PuerkitoBio/goquery v1.8.1 | |
| 7 | - | github.com/docker/docker v24.0.5+incompatible | |
| 8 | - | github.com/labstack/echo/v4 v4.11.1 | |
| 9 | - | github.com/microcosm-cc/bluemonday v1.0.25 | |
| 10 | - | github.com/tidwall/gjson v1.16.0 | |
| 11 | - | gopkg.in/yaml.v3 v3.0.1 | |
| 12 | - | ) | |
| 13 | - | ||
| 14 | - | require ( | |
| 15 | - | github.com/Microsoft/go-winio v0.6.1 // indirect | |
| 16 | - | github.com/andybalholm/cascadia v1.3.1 // indirect | |
| 17 | - | github.com/aymerick/douceur v0.2.0 // indirect | |
| 18 | - | github.com/containerd/containerd v1.7.5 // indirect | |
| 19 | - | github.com/docker/distribution v2.8.2+incompatible // indirect | |
| 20 | - | github.com/docker/go-connections v0.4.0 // indirect | |
| 21 | - | github.com/docker/go-units v0.5.0 // indirect | |
| 22 | - | github.com/gogo/protobuf v1.3.2 // indirect | |
| 23 | - | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect | |
| 24 | - | github.com/gorilla/context v1.1.1 // indirect | |
| 25 | - | github.com/gorilla/css v1.0.0 // indirect | |
| 26 | - | github.com/gorilla/securecookie v1.1.1 // indirect | |
| 27 | - | github.com/gorilla/sessions v1.2.1 // indirect | |
| 28 | - | github.com/klauspost/compress v1.16.7 // indirect | |
| 29 | - | github.com/moby/patternmatcher v0.6.0 // indirect | |
| 30 | - | github.com/moby/sys/sequential v0.5.0 // indirect | |
| 31 | - | github.com/moby/term v0.5.0 // indirect | |
| 32 | - | github.com/morikuni/aec v1.0.0 // indirect | |
| 33 | - | github.com/opencontainers/go-digest v1.0.0 // indirect | |
| 34 | - | github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect | |
| 35 | - | github.com/opencontainers/runc v1.1.9 // indirect | |
| 36 | - | github.com/pkg/errors v0.9.1 // indirect | |
| 37 | - | github.com/sirupsen/logrus v1.9.0 // indirect | |
| 38 | - | github.com/tidwall/match v1.1.1 // indirect | |
| 39 | - | github.com/tidwall/pretty v1.2.1 // indirect | |
| 40 | - | golang.org/x/mod v0.9.0 // indirect | |
| 41 | - | golang.org/x/time v0.3.0 // indirect | |
| 42 | - | golang.org/x/tools v0.7.0 // indirect | |
| 43 | - | gotest.tools/v3 v3.5.0 // indirect | |
| 44 | - | ) | |
| 45 | - | ||
| 46 | - | require ( | |
| 47 | - | github.com/labstack/echo-contrib v0.15.0 | |
| 48 | - | github.com/labstack/gommon v0.4.0 | |
| 49 | - | github.com/mattn/go-colorable v0.1.13 // indirect | |
| 50 | - | github.com/mattn/go-isatty v0.0.19 // indirect | |
| 51 | - | github.com/rs/zerolog v1.30.0 | |
| 52 | - | github.com/valyala/bytebufferpool v1.0.0 // indirect | |
| 53 | - | github.com/valyala/fasttemplate v1.2.2 // indirect | |
| 54 | - | golang.org/x/crypto v0.11.0 // indirect | |
| 55 | - | golang.org/x/net v0.12.0 // indirect | |
| 56 | - | golang.org/x/sys v0.10.0 // indirect | |
| 57 | - | golang.org/x/text v0.11.0 // indirect | |
| 58 | - | ) | |
| @@ -1,58 +0,0 @@ | |||||
| 1 | - | module gogs.bnema.dev/gordon-echo | |||
| 2 | - | ||||
| 3 | - | go 1.21.0 | |||
| 4 | - | ||||
| 5 | - | require ( | |||
| 6 | - | github.com/PuerkitoBio/goquery v1.8.1 | |||
| 7 | - | github.com/docker/docker v24.0.5+incompatible | |||
| 8 | - | github.com/labstack/echo/v4 v4.11.1 | |||
| 9 | - | github.com/microcosm-cc/bluemonday v1.0.25 | |||
| 10 | - | github.com/tidwall/gjson v1.16.0 | |||
| 11 | - | gopkg.in/yaml.v3 v3.0.1 | |||
| 12 | - | ) | |||
| 13 | - | ||||
| 14 | - | require ( | |||
| 15 | - | github.com/Microsoft/go-winio v0.6.1 // indirect | |||
| 16 | - | github.com/andybalholm/cascadia v1.3.1 // indirect | |||
| 17 | - | github.com/aymerick/douceur v0.2.0 // indirect | |||
| 18 | - | github.com/containerd/containerd v1.7.5 // indirect | |||
| 19 | - | github.com/docker/distribution v2.8.2+incompatible // indirect | |||
| 20 | - | github.com/docker/go-connections v0.4.0 // indirect | |||
| 21 | - | github.com/docker/go-units v0.5.0 // indirect | |||
| 22 | - | github.com/gogo/protobuf v1.3.2 // indirect | |||
| 23 | - | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect | |||
| 24 | - | github.com/gorilla/context v1.1.1 // indirect | |||
| 25 | - | github.com/gorilla/css v1.0.0 // indirect | |||
| 26 | - | github.com/gorilla/securecookie v1.1.1 // indirect | |||
| 27 | - | github.com/gorilla/sessions v1.2.1 // indirect | |||
| 28 | - | github.com/klauspost/compress v1.16.7 // indirect | |||
| 29 | - | github.com/moby/patternmatcher v0.6.0 // indirect | |||
| 30 | - | github.com/moby/sys/sequential v0.5.0 // indirect | |||
| 31 | - | github.com/moby/term v0.5.0 // indirect | |||
| 32 | - | github.com/morikuni/aec v1.0.0 // indirect | |||
| 33 | - | github.com/opencontainers/go-digest v1.0.0 // indirect | |||
| 34 | - | github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect | |||
| 35 | - | github.com/opencontainers/runc v1.1.9 // indirect | |||
| 36 | - | github.com/pkg/errors v0.9.1 // indirect | |||
| 37 | - | github.com/sirupsen/logrus v1.9.0 // indirect | |||
| 38 | - | github.com/tidwall/match v1.1.1 // indirect | |||
| 39 | - | github.com/tidwall/pretty v1.2.1 // indirect | |||
| 40 | - | golang.org/x/mod v0.9.0 // indirect | |||
| 41 | - | golang.org/x/time v0.3.0 // indirect | |||
| 42 | - | golang.org/x/tools v0.7.0 // indirect | |||
| 43 | - | gotest.tools/v3 v3.5.0 // indirect | |||
| 44 | - | ) | |||
| 45 | - | ||||
| 46 | - | require ( | |||
| 47 | - | github.com/labstack/echo-contrib v0.15.0 | |||
| 48 | - | github.com/labstack/gommon v0.4.0 | |||
| 49 | - | github.com/mattn/go-colorable v0.1.13 // indirect | |||
| 50 | - | github.com/mattn/go-isatty v0.0.19 // indirect | |||
| 51 | - | github.com/rs/zerolog v1.30.0 | |||
| 52 | - | github.com/valyala/bytebufferpool v1.0.0 // indirect | |||
| 53 | - | github.com/valyala/fasttemplate v1.2.2 // indirect | |||
| 54 | - | golang.org/x/crypto v0.11.0 // indirect | |||
| 55 | - | golang.org/x/net v0.12.0 // indirect | |||
| 56 | - | golang.org/x/sys v0.10.0 // indirect | |||
| 57 | - | golang.org/x/text v0.11.0 // indirect | |||
| 58 | - | ) | |||
D go.sum +0 -180 generated file: collapsed
D internal/app/app.go +0 -122
| @@ -1,122 +0,0 @@ | |||
| 1 | - | package app | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "io" | |
| 6 | - | "io/fs" | |
| 7 | - | "os" | |
| 8 | - | "path/filepath" | |
| 9 | - | ||
| 10 | - | "github.com/rs/zerolog" | |
| 11 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |
| 12 | - | ) | |
| 13 | - | ||
| 14 | - | const ( | |
| 15 | - | DefaultBuildDir = "tmp" | |
| 16 | - | DefaultLogDir = "logs" | |
| 17 | - | EnvVarName = "ENV" | |
| 18 | - | ProdEnvValue = "prod" | |
| 19 | - | BuildDirEnvVarName = "BUILD_DIR" | |
| 20 | - | LogDirEnvVarName = "LOG_DIR" | |
| 21 | - | ) | |
| 22 | - | ||
| 23 | - | type App struct { | |
| 24 | - | AppLogger *utils.Logger | |
| 25 | - | HttpLogger *utils.Logger | |
| 26 | - | Config *Config | |
| 27 | - | } | |
| 28 | - | type Config struct { | |
| 29 | - | BuildDir string | |
| 30 | - | LogDir string | |
| 31 | - | TemplateFS fs.FS | |
| 32 | - | PublicFS fs.FS | |
| 33 | - | ModelFS fs.FS | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | func (c *Config) GetTemplateFS() fs.FS { | |
| 37 | - | return c.TemplateFS | |
| 38 | - | } | |
| 39 | - | func (c *Config) GetPublicFS() fs.FS { | |
| 40 | - | return c.PublicFS | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | func (c *Config) GetModelFS() fs.FS { | |
| 44 | - | return c.ModelFS | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | func defineEnv(config *Config) { | |
| 48 | - | // Get the environment variable | |
| 49 | - | env := os.Getenv(EnvVarName) | |
| 50 | - | // If the environment variable is empty, consider it is dev -> (/tmp) | |
| 51 | - | buildDir := os.Getenv(BuildDirEnvVarName) | |
| 52 | - | if buildDir == "" { | |
| 53 | - | buildDir = DefaultBuildDir | |
| 54 | - | } | |
| 55 | - | logDir := os.Getenv(LogDirEnvVarName) | |
| 56 | - | if logDir == "" { | |
| 57 | - | logDir = DefaultLogDir | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | switch env { | |
| 61 | - | case ProdEnvValue: | |
| 62 | - | // If it is prod we want everything at the same level of the binary | |
| 63 | - | buildDir = "." | |
| 64 | - | logDir = "./" + logDir | |
| 65 | - | default: | |
| 66 | - | // If it is dev we want everything in /tmp | |
| 67 | - | buildDir = "./" + buildDir | |
| 68 | - | logDir = filepath.Join(buildDir, logDir) | |
| 69 | - | } | |
| 70 | - | config.BuildDir = buildDir | |
| 71 | - | config.LogDir = logDir | |
| 72 | - | } | |
| 73 | - | ||
| 74 | - | func NewApp(config *Config) (*App, error) { | |
| 75 | - | defineEnv(config) | |
| 76 | - | SetupLogging(config) | |
| 77 | - | app := &App{ | |
| 78 | - | AppLogger: utils.NewLogger().SetOutput(initializeLoggerOutput(config, "app.log")), | |
| 79 | - | HttpLogger: utils.NewLogger().SetOutput(initializeLoggerOutput(config, "http.log")), | |
| 80 | - | Config: config, | |
| 81 | - | } | |
| 82 | - | return app, nil | |
| 83 | - | } | |
| 84 | - | ||
| 85 | - | func initializeLoggerOutput(cfg *Config, logFile string) io.Writer { | |
| 86 | - | logPath := filepath.Join(cfg.LogDir, logFile) | |
| 87 | - | dir := filepath.Dir(logPath) | |
| 88 | - | if _, err := os.Stat(dir); os.IsNotExist(err) { | |
| 89 | - | if mkdirErr := os.MkdirAll(dir, 0755); mkdirErr != nil { | |
| 90 | - | panic(mkdirErr) | |
| 91 | - | } | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) | |
| 95 | - | if err != nil { | |
| 96 | - | panic(err) | |
| 97 | - | } | |
| 98 | - | consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout} | |
| 99 | - | return zerolog.MultiLevelWriter(file, consoleWriter) | |
| 100 | - | } | |
| 101 | - | func SetupLogging(config *Config) { | |
| 102 | - | // Check if the log directory exists | |
| 103 | - | if _, err := os.Stat(config.LogDir); os.IsNotExist(err) { | |
| 104 | - | // If not, create it | |
| 105 | - | err := os.MkdirAll(config.LogDir, 0755) | |
| 106 | - | if err != nil { | |
| 107 | - | panic(fmt.Sprintf("Failed to create log directory at path: %s, error: %s", config.LogDir, err)) | |
| 108 | - | } | |
| 109 | - | } | |
| 110 | - | } | |
| 111 | - | ||
| 112 | - | func GetBuildDir() string { | |
| 113 | - | env := os.Getenv(EnvVarName) | |
| 114 | - | buildDir := os.Getenv(BuildDirEnvVarName) | |
| 115 | - | if buildDir == "" { | |
| 116 | - | buildDir = DefaultBuildDir | |
| 117 | - | } | |
| 118 | - | if env == ProdEnvValue { | |
| 119 | - | return "." | |
| 120 | - | } | |
| 121 | - | return "./" + buildDir | |
| 122 | - | } | |
| @@ -1,122 +0,0 @@ | |||||
| 1 | - | package app | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "fmt" | |||
| 5 | - | "io" | |||
| 6 | - | "io/fs" | |||
| 7 | - | "os" | |||
| 8 | - | "path/filepath" | |||
| 9 | - | ||||
| 10 | - | "github.com/rs/zerolog" | |||
| 11 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |||
| 12 | - | ) | |||
| 13 | - | ||||
| 14 | - | const ( | |||
| 15 | - | DefaultBuildDir = "tmp" | |||
| 16 | - | DefaultLogDir = "logs" | |||
| 17 | - | EnvVarName = "ENV" | |||
| 18 | - | ProdEnvValue = "prod" | |||
| 19 | - | BuildDirEnvVarName = "BUILD_DIR" | |||
| 20 | - | LogDirEnvVarName = "LOG_DIR" | |||
| 21 | - | ) | |||
| 22 | - | ||||
| 23 | - | type App struct { | |||
| 24 | - | AppLogger *utils.Logger | |||
| 25 | - | HttpLogger *utils.Logger | |||
| 26 | - | Config *Config | |||
| 27 | - | } | |||
| 28 | - | type Config struct { | |||
| 29 | - | BuildDir string | |||
| 30 | - | LogDir string | |||
| 31 | - | TemplateFS fs.FS | |||
| 32 | - | PublicFS fs.FS | |||
| 33 | - | ModelFS fs.FS | |||
| 34 | - | } | |||
| 35 | - | ||||
| 36 | - | func (c *Config) GetTemplateFS() fs.FS { | |||
| 37 | - | return c.TemplateFS | |||
| 38 | - | } | |||
| 39 | - | func (c *Config) GetPublicFS() fs.FS { | |||
| 40 | - | return c.PublicFS | |||
| 41 | - | } | |||
| 42 | - | ||||
| 43 | - | func (c *Config) GetModelFS() fs.FS { | |||
| 44 | - | return c.ModelFS | |||
| 45 | - | } | |||
| 46 | - | ||||
| 47 | - | func defineEnv(config *Config) { | |||
| 48 | - | // Get the environment variable | |||
| 49 | - | env := os.Getenv(EnvVarName) | |||
| 50 | - | // If the environment variable is empty, consider it is dev -> (/tmp) | |||
| 51 | - | buildDir := os.Getenv(BuildDirEnvVarName) | |||
| 52 | - | if buildDir == "" { | |||
| 53 | - | buildDir = DefaultBuildDir | |||
| 54 | - | } | |||
| 55 | - | logDir := os.Getenv(LogDirEnvVarName) | |||
| 56 | - | if logDir == "" { | |||
| 57 | - | logDir = DefaultLogDir | |||
| 58 | - | } | |||
| 59 | - | ||||
| 60 | - | switch env { | |||
| 61 | - | case ProdEnvValue: | |||
| 62 | - | // If it is prod we want everything at the same level of the binary | |||
| 63 | - | buildDir = "." | |||
| 64 | - | logDir = "./" + logDir | |||
| 65 | - | default: | |||
| 66 | - | // If it is dev we want everything in /tmp | |||
| 67 | - | buildDir = "./" + buildDir | |||
| 68 | - | logDir = filepath.Join(buildDir, logDir) | |||
| 69 | - | } | |||
| 70 | - | config.BuildDir = buildDir | |||
| 71 | - | config.LogDir = logDir | |||
| 72 | - | } | |||
| 73 | - | ||||
| 74 | - | func NewApp(config *Config) (*App, error) { | |||
| 75 | - | defineEnv(config) | |||
| 76 | - | SetupLogging(config) | |||
| 77 | - | app := &App{ | |||
| 78 | - | AppLogger: utils.NewLogger().SetOutput(initializeLoggerOutput(config, "app.log")), | |||
| 79 | - | HttpLogger: utils.NewLogger().SetOutput(initializeLoggerOutput(config, "http.log")), | |||
| 80 | - | Config: config, | |||
| 81 | - | } | |||
| 82 | - | return app, nil | |||
| 83 | - | } | |||
| 84 | - | ||||
| 85 | - | func initializeLoggerOutput(cfg *Config, logFile string) io.Writer { | |||
| 86 | - | logPath := filepath.Join(cfg.LogDir, logFile) | |||
| 87 | - | dir := filepath.Dir(logPath) | |||
| 88 | - | if _, err := os.Stat(dir); os.IsNotExist(err) { | |||
| 89 | - | if mkdirErr := os.MkdirAll(dir, 0755); mkdirErr != nil { | |||
| 90 | - | panic(mkdirErr) | |||
| 91 | - | } | |||
| 92 | - | } | |||
| 93 | - | ||||
| 94 | - | file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) | |||
| 95 | - | if err != nil { | |||
| 96 | - | panic(err) | |||
| 97 | - | } | |||
| 98 | - | consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout} | |||
| 99 | - | return zerolog.MultiLevelWriter(file, consoleWriter) | |||
| 100 | - | } | |||
| 101 | - | func SetupLogging(config *Config) { | |||
| 102 | - | // Check if the log directory exists | |||
| 103 | - | if _, err := os.Stat(config.LogDir); os.IsNotExist(err) { | |||
| 104 | - | // If not, create it | |||
| 105 | - | err := os.MkdirAll(config.LogDir, 0755) | |||
| 106 | - | if err != nil { | |||
| 107 | - | panic(fmt.Sprintf("Failed to create log directory at path: %s, error: %s", config.LogDir, err)) | |||
| 108 | - | } | |||
| 109 | - | } | |||
| 110 | - | } | |||
| 111 | - | ||||
| 112 | - | func GetBuildDir() string { | |||
| 113 | - | env := os.Getenv(EnvVarName) | |||
| 114 | - | buildDir := os.Getenv(BuildDirEnvVarName) | |||
| 115 | - | if buildDir == "" { | |||
| 116 | - | buildDir = DefaultBuildDir | |||
| 117 | - | } | |||
| 118 | - | if env == ProdEnvValue { | |||
| 119 | - | return "." | |||
| 120 | - | } | |||
| 121 | - | return "./" + buildDir | |||
| 122 | - | } | |||
D internal/http/handlers/errors.go +0 -24
| @@ -1,24 +0,0 @@ | |||
| 1 | - | package handlers | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "net/http" | |
| 6 | - | ||
| 7 | - | "github.com/labstack/echo/v4" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | func ErrorNumberHandler(err error, c echo.Context) { | |
| 11 | - | code := http.StatusInternalServerError | |
| 12 | - | if he, ok := err.(*echo.HTTPError); ok { | |
| 13 | - | code = he.Code | |
| 14 | - | } | |
| 15 | - | ||
| 16 | - | switch code { | |
| 17 | - | case http.StatusNotFound: | |
| 18 | - | c.String(code, fmt.Sprintf("An error occurred: %v", err)) | |
| 19 | - | case http.StatusForbidden: | |
| 20 | - | c.String(code, fmt.Sprintf("An error occurred: %v", err)) | |
| 21 | - | default: | |
| 22 | - | c.String(code, fmt.Sprintf("An error occurred: %v", err)) | |
| 23 | - | } | |
| 24 | - | } | |
| @@ -1,24 +0,0 @@ | |||||
| 1 | - | package handlers | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "fmt" | |||
| 5 | - | "net/http" | |||
| 6 | - | ||||
| 7 | - | "github.com/labstack/echo/v4" | |||
| 8 | - | ) | |||
| 9 | - | ||||
| 10 | - | func ErrorNumberHandler(err error, c echo.Context) { | |||
| 11 | - | code := http.StatusInternalServerError | |||
| 12 | - | if he, ok := err.(*echo.HTTPError); ok { | |||
| 13 | - | code = he.Code | |||
| 14 | - | } | |||
| 15 | - | ||||
| 16 | - | switch code { | |||
| 17 | - | case http.StatusNotFound: | |||
| 18 | - | c.String(code, fmt.Sprintf("An error occurred: %v", err)) | |||
| 19 | - | case http.StatusForbidden: | |||
| 20 | - | c.String(code, fmt.Sprintf("An error occurred: %v", err)) | |||
| 21 | - | default: | |||
| 22 | - | c.String(code, fmt.Sprintf("An error occurred: %v", err)) | |||
| 23 | - | } | |||
| 24 | - | } | |||
D internal/http/handlers/htmx-fragment-header.go +0 -60
| @@ -1,60 +0,0 @@ | |||
| 1 | - | package handlers | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "net/http" | |
| 5 | - | ||
| 6 | - | "github.com/labstack/echo/v4" | |
| 7 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |
| 8 | - | "gogs.bnema.dev/gordon-echo/internal/http/middlewares" | |
| 9 | - | "gogs.bnema.dev/gordon-echo/pkg/htmx" | |
| 10 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |
| 11 | - | ) | |
| 12 | - | ||
| 13 | - | func HTMXHandler(c echo.Context, a *app.App, ac *app.Config) error { | |
| 14 | - | logger := a.AppLogger | |
| 15 | - | // Detect and extract HTMX data from the request | |
| 16 | - | htmxRequest, err := htmx.GetRequest(c) | |
| 17 | - | if err != nil { | |
| 18 | - | logger.Debug().Err(err).Msg("Failed to extract HTMX data") | |
| 19 | - | return err | |
| 20 | - | } | |
| 21 | - | ||
| 22 | - | // If the request is not an HTMX request, return an error | |
| 23 | - | if !htmxRequest.Enabled { | |
| 24 | - | logger.Error().Msg("Request is not an HTMX request") | |
| 25 | - | return c.String(http.StatusBadRequest, "Not an HTMX request") | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | // If it's a GET request, handle the fragment | |
| 29 | - | if c.Request().Method == http.MethodGet { | |
| 30 | - | return htmxFragmentHandler(c, a, ac) | |
| 31 | - | } | |
| 32 | - | ||
| 33 | - | // For other HTTP methods, return an error | |
| 34 | - | return c.String(http.StatusBadRequest, "Invalid request method") | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | func htmxFragmentHandler(c echo.Context, a *app.App, ac *app.Config) error { | |
| 38 | - | logger := a.AppLogger | |
| 39 | - | fragment := c.Request().Header.Get("HX-Fragment") | |
| 40 | - | if fragment == "" { | |
| 41 | - | logger.Error().Msg("Missing fragment header") | |
| 42 | - | return c.String(http.StatusBadRequest, "Missing fragment header") | |
| 43 | - | } | |
| 44 | - | ||
| 45 | - | lang := c.Get(middlewares.LangKey).(string) | |
| 46 | - | var data utils.StringsYamlData | |
| 47 | - | err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data) | |
| 48 | - | if err != nil { | |
| 49 | - | logger.Error().Err(err).Msg("Failed to populate data from YAML") | |
| 50 | - | return err | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | content, err := htmx.GetHTMLFragmentByID(fragment, data.CurrentLang, a, ac) | |
| 54 | - | if err != nil { // <-- This check was missing | |
| 55 | - | logger.Error().Err(err).Msg("Failed to get fragment") | |
| 56 | - | return c.String(http.StatusInternalServerError, err.Error()) | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | return c.HTML(http.StatusOK, content) | |
| 60 | - | } | |
| @@ -1,60 +0,0 @@ | |||||
| 1 | - | package handlers | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "net/http" | |||
| 5 | - | ||||
| 6 | - | "github.com/labstack/echo/v4" | |||
| 7 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |||
| 8 | - | "gogs.bnema.dev/gordon-echo/internal/http/middlewares" | |||
| 9 | - | "gogs.bnema.dev/gordon-echo/pkg/htmx" | |||
| 10 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |||
| 11 | - | ) | |||
| 12 | - | ||||
| 13 | - | func HTMXHandler(c echo.Context, a *app.App, ac *app.Config) error { | |||
| 14 | - | logger := a.AppLogger | |||
| 15 | - | // Detect and extract HTMX data from the request | |||
| 16 | - | htmxRequest, err := htmx.GetRequest(c) | |||
| 17 | - | if err != nil { | |||
| 18 | - | logger.Debug().Err(err).Msg("Failed to extract HTMX data") | |||
| 19 | - | return err | |||
| 20 | - | } | |||
| 21 | - | ||||
| 22 | - | // If the request is not an HTMX request, return an error | |||
| 23 | - | if !htmxRequest.Enabled { | |||
| 24 | - | logger.Error().Msg("Request is not an HTMX request") | |||
| 25 | - | return c.String(http.StatusBadRequest, "Not an HTMX request") | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | // If it's a GET request, handle the fragment | |||
| 29 | - | if c.Request().Method == http.MethodGet { | |||
| 30 | - | return htmxFragmentHandler(c, a, ac) | |||
| 31 | - | } | |||
| 32 | - | ||||
| 33 | - | // For other HTTP methods, return an error | |||
| 34 | - | return c.String(http.StatusBadRequest, "Invalid request method") | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | func htmxFragmentHandler(c echo.Context, a *app.App, ac *app.Config) error { | |||
| 38 | - | logger := a.AppLogger | |||
| 39 | - | fragment := c.Request().Header.Get("HX-Fragment") | |||
| 40 | - | if fragment == "" { | |||
| 41 | - | logger.Error().Msg("Missing fragment header") | |||
| 42 | - | return c.String(http.StatusBadRequest, "Missing fragment header") | |||
| 43 | - | } | |||
| 44 | - | ||||
| 45 | - | lang := c.Get(middlewares.LangKey).(string) | |||
| 46 | - | var data utils.StringsYamlData | |||
| 47 | - | err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data) | |||
| 48 | - | if err != nil { | |||
| 49 | - | logger.Error().Err(err).Msg("Failed to populate data from YAML") | |||
| 50 | - | return err | |||
| 51 | - | } | |||
| 52 | - | ||||
| 53 | - | content, err := htmx.GetHTMLFragmentByID(fragment, data.CurrentLang, a, ac) | |||
| 54 | - | if err != nil { // <-- This check was missing | |||
| 55 | - | logger.Error().Err(err).Msg("Failed to get fragment") | |||
| 56 | - | return c.String(http.StatusInternalServerError, err.Error()) | |||
| 57 | - | } | |||
| 58 | - | ||||
| 59 | - | return c.HTML(http.StatusOK, content) | |||
| 60 | - | } | |||
D internal/http/handlers/htmx-installer.go +0 -75
| @@ -1,75 +0,0 @@ | |||
| 1 | - | package handlers | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "net/http" | |
| 6 | - | ||
| 7 | - | "github.com/labstack/echo/v4" | |
| 8 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |
| 9 | - | "gogs.bnema.dev/gordon-echo/pkg/scripts" | |
| 10 | - | ) | |
| 11 | - | ||
| 12 | - | var eventsChan = make(chan string) | |
| 13 | - | ||
| 14 | - | // TraefikInstallerHandler handles the installation of Traefik | |
| 15 | - | func TraefikInstallerHandler(c echo.Context, a *app.App) error { | |
| 16 | - | logger := a.AppLogger | |
| 17 | - | eventsChan <- "Starting Traefik installation..." | |
| 18 | - | // Retrieve the logger from the context | |
| 19 | - | // Reject POST requests if they are not allowed with the RejectPOSTPolicy function | |
| 20 | - | if err := RejectPOSTPolicy(c); err != nil { | |
| 21 | - | logger.Error().Err(err).Msg("POST request rejected") | |
| 22 | - | return err | |
| 23 | - | } | |
| 24 | - | ||
| 25 | - | // Get data from the form install-traefik-form (input fields : topdomain, adminemail) and sanitize it | |
| 26 | - | topDomain, err := SanitizeUserInput(c.FormValue("topdomain")) | |
| 27 | - | if err != nil { | |
| 28 | - | logger.Error().Err(err).Msg("Failed to sanitize user input") | |
| 29 | - | return err | |
| 30 | - | } | |
| 31 | - | adminEmail, err := SanitizeUserInput(c.FormValue("adminemail")) | |
| 32 | - | if err != nil { | |
| 33 | - | logger.Error().Err(err).Msg("Failed to sanitize user input") | |
| 34 | - | return err | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | // Call the InstallTraefik function from pkg/scripts/install-traefik.go | |
| 38 | - | if success, err := scripts.InstallTraefik(topDomain, adminEmail, a); err != nil { | |
| 39 | - | logger.Error().Err(err).Msg("Failed to install Traefik") | |
| 40 | - | return err | |
| 41 | - | } else if !success { | |
| 42 | - | logger.Error().Msg("Failed to install Traefik") | |
| 43 | - | return err | |
| 44 | - | } else { | |
| 45 | - | eventsChan <- "Traefik installation completed!" | |
| 46 | - | return c.String(200, "Traefik installed successfully") | |
| 47 | - | } | |
| 48 | - | } | |
| 49 | - | ||
| 50 | - | func TraefikInstallerSSEHandler(c echo.Context, a *app.App) error { | |
| 51 | - | w := c.Response().Writer | |
| 52 | - | flusher, ok := w.(http.Flusher) | |
| 53 | - | if !ok { | |
| 54 | - | return c.String(http.StatusInternalServerError, "Streaming unsupported!") | |
| 55 | - | } | |
| 56 | - | ||
| 57 | - | c.Response().Header().Set("Content-Type", "text/event-stream") | |
| 58 | - | c.Response().Header().Set("Cache-Control", "no-cache") | |
| 59 | - | c.Response().Header().Set("Connection", "keep-alive") | |
| 60 | - | ||
| 61 | - | for { | |
| 62 | - | // Listen for a new message to be sent to the client | |
| 63 | - | msg, open := <-eventsChan | |
| 64 | - | if !open { | |
| 65 | - | fmt.Println("Connection closed") | |
| 66 | - | break | |
| 67 | - | } | |
| 68 | - | _, err := fmt.Fprintf(w, "data: %s\n\n", msg) | |
| 69 | - | if err != nil { | |
| 70 | - | return err | |
| 71 | - | } | |
| 72 | - | flusher.Flush() | |
| 73 | - | } | |
| 74 | - | return nil | |
| 75 | - | } | |
| @@ -1,75 +0,0 @@ | |||||
| 1 | - | package handlers | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "fmt" | |||
| 5 | - | "net/http" | |||
| 6 | - | ||||
| 7 | - | "github.com/labstack/echo/v4" | |||
| 8 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |||
| 9 | - | "gogs.bnema.dev/gordon-echo/pkg/scripts" | |||
| 10 | - | ) | |||
| 11 | - | ||||
| 12 | - | var eventsChan = make(chan string) | |||
| 13 | - | ||||
| 14 | - | // TraefikInstallerHandler handles the installation of Traefik | |||
| 15 | - | func TraefikInstallerHandler(c echo.Context, a *app.App) error { | |||
| 16 | - | logger := a.AppLogger | |||
| 17 | - | eventsChan <- "Starting Traefik installation..." | |||
| 18 | - | // Retrieve the logger from the context | |||
| 19 | - | // Reject POST requests if they are not allowed with the RejectPOSTPolicy function | |||
| 20 | - | if err := RejectPOSTPolicy(c); err != nil { | |||
| 21 | - | logger.Error().Err(err).Msg("POST request rejected") | |||
| 22 | - | return err | |||
| 23 | - | } | |||
| 24 | - | ||||
| 25 | - | // Get data from the form install-traefik-form (input fields : topdomain, adminemail) and sanitize it | |||
| 26 | - | topDomain, err := SanitizeUserInput(c.FormValue("topdomain")) | |||
| 27 | - | if err != nil { | |||
| 28 | - | logger.Error().Err(err).Msg("Failed to sanitize user input") | |||
| 29 | - | return err | |||
| 30 | - | } | |||
| 31 | - | adminEmail, err := SanitizeUserInput(c.FormValue("adminemail")) | |||
| 32 | - | if err != nil { | |||
| 33 | - | logger.Error().Err(err).Msg("Failed to sanitize user input") | |||
| 34 | - | return err | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | // Call the InstallTraefik function from pkg/scripts/install-traefik.go | |||
| 38 | - | if success, err := scripts.InstallTraefik(topDomain, adminEmail, a); err != nil { | |||
| 39 | - | logger.Error().Err(err).Msg("Failed to install Traefik") | |||
| 40 | - | return err | |||
| 41 | - | } else if !success { | |||
| 42 | - | logger.Error().Msg("Failed to install Traefik") | |||
| 43 | - | return err | |||
| 44 | - | } else { | |||
| 45 | - | eventsChan <- "Traefik installation completed!" | |||
| 46 | - | return c.String(200, "Traefik installed successfully") | |||
| 47 | - | } | |||
| 48 | - | } | |||
| 49 | - | ||||
| 50 | - | func TraefikInstallerSSEHandler(c echo.Context, a *app.App) error { | |||
| 51 | - | w := c.Response().Writer | |||
| 52 | - | flusher, ok := w.(http.Flusher) | |||
| 53 | - | if !ok { | |||
| 54 | - | return c.String(http.StatusInternalServerError, "Streaming unsupported!") | |||
| 55 | - | } | |||
| 56 | - | ||||
| 57 | - | c.Response().Header().Set("Content-Type", "text/event-stream") | |||
| 58 | - | c.Response().Header().Set("Cache-Control", "no-cache") | |||
| 59 | - | c.Response().Header().Set("Connection", "keep-alive") | |||
| 60 | - | ||||
| 61 | - | for { | |||
| 62 | - | // Listen for a new message to be sent to the client | |||
| 63 | - | msg, open := <-eventsChan | |||
| 64 | - | if !open { | |||
| 65 | - | fmt.Println("Connection closed") | |||
| 66 | - | break | |||
| 67 | - | } | |||
| 68 | - | _, err := fmt.Fprintf(w, "data: %s\n\n", msg) | |||
| 69 | - | if err != nil { | |||
| 70 | - | return err | |||
| 71 | - | } | |||
| 72 | - | flusher.Flush() | |||
| 73 | - | } | |||
| 74 | - | return nil | |||
| 75 | - | } | |||
D internal/http/handlers/post.go +0 -38
| @@ -1,38 +0,0 @@ | |||
| 1 | - | package handlers | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "net/http" | |
| 6 | - | ||
| 7 | - | "github.com/labstack/echo/v4" | |
| 8 | - | "gogs.bnema.dev/gordon-echo/pkg/templating" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | func RejectPOSTPolicy(c echo.Context) error { | |
| 12 | - | // Maximum POST size in bytes | |
| 13 | - | const maxPOSTSize = 1024 * 1024 // = 1 MB | |
| 14 | - | // Check the request method | |
| 15 | - | if c.Request().Method != http.MethodPost { | |
| 16 | - | return fmt.Errorf("invalid request method") | |
| 17 | - | } | |
| 18 | - | // Check for unusually large POST bodies, which might indicate an attempt at a DoS attack | |
| 19 | - | if c.Request().ContentLength > maxPOSTSize { | |
| 20 | - | return fmt.Errorf("request too large: %d bytes", c.Request().ContentLength) | |
| 21 | - | } | |
| 22 | - | ||
| 23 | - | // Check for other suspicious behaviors, e.g., missing user agent, too many request headers, etc. | |
| 24 | - | if c.Request().UserAgent() == "" { | |
| 25 | - | return fmt.Errorf("missing user agent") | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | // If all checks pass | |
| 29 | - | return nil | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | func SanitizeUserInput(input string) (string, error) { | |
| 33 | - | sanitized, err := templating.SanitizeHTML(input) | |
| 34 | - | if err != nil { | |
| 35 | - | return "", fmt.Errorf("Failed to sanitize user input: %w", err) | |
| 36 | - | } | |
| 37 | - | return sanitized, nil | |
| 38 | - | } | |
| @@ -1,38 +0,0 @@ | |||||
| 1 | - | package handlers | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "fmt" | |||
| 5 | - | "net/http" | |||
| 6 | - | ||||
| 7 | - | "github.com/labstack/echo/v4" | |||
| 8 | - | "gogs.bnema.dev/gordon-echo/pkg/templating" | |||
| 9 | - | ) | |||
| 10 | - | ||||
| 11 | - | func RejectPOSTPolicy(c echo.Context) error { | |||
| 12 | - | // Maximum POST size in bytes | |||
| 13 | - | const maxPOSTSize = 1024 * 1024 // = 1 MB | |||
| 14 | - | // Check the request method | |||
| 15 | - | if c.Request().Method != http.MethodPost { | |||
| 16 | - | return fmt.Errorf("invalid request method") | |||
| 17 | - | } | |||
| 18 | - | // Check for unusually large POST bodies, which might indicate an attempt at a DoS attack | |||
| 19 | - | if c.Request().ContentLength > maxPOSTSize { | |||
| 20 | - | return fmt.Errorf("request too large: %d bytes", c.Request().ContentLength) | |||
| 21 | - | } | |||
| 22 | - | ||||
| 23 | - | // Check for other suspicious behaviors, e.g., missing user agent, too many request headers, etc. | |||
| 24 | - | if c.Request().UserAgent() == "" { | |||
| 25 | - | return fmt.Errorf("missing user agent") | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | // If all checks pass | |||
| 29 | - | return nil | |||
| 30 | - | } | |||
| 31 | - | ||||
| 32 | - | func SanitizeUserInput(input string) (string, error) { | |||
| 33 | - | sanitized, err := templating.SanitizeHTML(input) | |||
| 34 | - | if err != nil { | |||
| 35 | - | return "", fmt.Errorf("Failed to sanitize user input: %w", err) | |||
| 36 | - | } | |||
| 37 | - | return sanitized, nil | |||
| 38 | - | } | |||
D internal/http/middlewares/detect-color-scheme.go +0 -38
| @@ -1,38 +0,0 @@ | |||
| 1 | - | package middlewares | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "github.com/labstack/echo-contrib/session" | |
| 5 | - | "github.com/labstack/echo/v4" | |
| 6 | - | ) | |
| 7 | - | ||
| 8 | - | // ColorSchemeKey is a key for setting and getting the color scheme from the context | |
| 9 | - | const ColorSchemeKey = "CurrentColorScheme" | |
| 10 | - | ||
| 11 | - | // ColorSchemeDetection detects the current color scheme and sets it in the context | |
| 12 | - | func ColorSchemeDetection(next echo.HandlerFunc) echo.HandlerFunc { | |
| 13 | - | return func(c echo.Context) error { | |
| 14 | - | colorScheme := detectCurrentColorScheme(c) | |
| 15 | - | c.Set(ColorSchemeKey, colorScheme) | |
| 16 | - | return next(c) | |
| 17 | - | } | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | func detectCurrentColorScheme(c echo.Context) string { | |
| 21 | - | // Check if the color scheme key is set in the session storage | |
| 22 | - | sess, err := session.Get("session", c) | |
| 23 | - | if err != nil { | |
| 24 | - | // If session storage is not available, proceed with "Sec-CH-Prefers-Color-Scheme" header detection | |
| 25 | - | return "light" | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | if colorSchemeValue, ok := sess.Values["colorScheme"]; ok && colorSchemeValue != nil { | |
| 29 | - | return colorSchemeValue.(string) // Return the color scheme value from the session storage | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | // If color scheme key is not set, proceed with "Sec-CH-Prefers-Color-Scheme" header detection | |
| 33 | - | header := c.Request().Header.Get("Sec-CH-Prefers-Color-Scheme") | |
| 34 | - | if header == "dark" { | |
| 35 | - | return "dark" | |
| 36 | - | } | |
| 37 | - | return "light" // Default to light mode | |
| 38 | - | } | |
| @@ -1,38 +0,0 @@ | |||||
| 1 | - | package middlewares | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "github.com/labstack/echo-contrib/session" | |||
| 5 | - | "github.com/labstack/echo/v4" | |||
| 6 | - | ) | |||
| 7 | - | ||||
| 8 | - | // ColorSchemeKey is a key for setting and getting the color scheme from the context | |||
| 9 | - | const ColorSchemeKey = "CurrentColorScheme" | |||
| 10 | - | ||||
| 11 | - | // ColorSchemeDetection detects the current color scheme and sets it in the context | |||
| 12 | - | func ColorSchemeDetection(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 13 | - | return func(c echo.Context) error { | |||
| 14 | - | colorScheme := detectCurrentColorScheme(c) | |||
| 15 | - | c.Set(ColorSchemeKey, colorScheme) | |||
| 16 | - | return next(c) | |||
| 17 | - | } | |||
| 18 | - | } | |||
| 19 | - | ||||
| 20 | - | func detectCurrentColorScheme(c echo.Context) string { | |||
| 21 | - | // Check if the color scheme key is set in the session storage | |||
| 22 | - | sess, err := session.Get("session", c) | |||
| 23 | - | if err != nil { | |||
| 24 | - | // If session storage is not available, proceed with "Sec-CH-Prefers-Color-Scheme" header detection | |||
| 25 | - | return "light" | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | if colorSchemeValue, ok := sess.Values["colorScheme"]; ok && colorSchemeValue != nil { | |||
| 29 | - | return colorSchemeValue.(string) // Return the color scheme value from the session storage | |||
| 30 | - | } | |||
| 31 | - | ||||
| 32 | - | // If color scheme key is not set, proceed with "Sec-CH-Prefers-Color-Scheme" header detection | |||
| 33 | - | header := c.Request().Header.Get("Sec-CH-Prefers-Color-Scheme") | |||
| 34 | - | if header == "dark" { | |||
| 35 | - | return "dark" | |||
| 36 | - | } | |||
| 37 | - | return "light" // Default to light mode | |||
| 38 | - | } | |||
D internal/http/middlewares/detect-lang.go +0 -40
| @@ -1,40 +0,0 @@ | |||
| 1 | - | package middlewares | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "strings" | |
| 5 | - | ||
| 6 | - | "github.com/labstack/echo-contrib/session" | |
| 7 | - | "github.com/labstack/echo/v4" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | // LangKey is a key for setting and getting the language from the context | |
| 11 | - | const LangKey = "CurrentLang" | |
| 12 | - | ||
| 13 | - | // LanguageDetectiondetects the current language and sets it in the context | |
| 14 | - | func LanguageDetection(next echo.HandlerFunc) echo.HandlerFunc { | |
| 15 | - | return func(c echo.Context) error { | |
| 16 | - | lang := detectCurrentLanguage(c) | |
| 17 | - | c.Set(LangKey, lang) | |
| 18 | - | return next(c) | |
| 19 | - | } | |
| 20 | - | } | |
| 21 | - | ||
| 22 | - | func detectCurrentLanguage(c echo.Context) string { | |
| 23 | - | // Check if the lang key is set in the session storage | |
| 24 | - | sess, err := session.Get("session", c) | |
| 25 | - | if err != nil { | |
| 26 | - | // If session storage is not available, proceed with "Accept-Language" header detection | |
| 27 | - | return "en" | |
| 28 | - | } | |
| 29 | - | ||
| 30 | - | if langValue, ok := sess.Values["lang"]; ok && langValue != nil { | |
| 31 | - | return langValue.(string) // Return the lang value from the session storage | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | // If lang key is not set, proceed with "Accept-Language" header detection | |
| 35 | - | header := c.Request().Header.Get("Accept-Language") | |
| 36 | - | if strings.Contains(header, "fr") { | |
| 37 | - | return "fr" | |
| 38 | - | } | |
| 39 | - | return "en" // Default to English | |
| 40 | - | } | |
| @@ -1,40 +0,0 @@ | |||||
| 1 | - | package middlewares | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "strings" | |||
| 5 | - | ||||
| 6 | - | "github.com/labstack/echo-contrib/session" | |||
| 7 | - | "github.com/labstack/echo/v4" | |||
| 8 | - | ) | |||
| 9 | - | ||||
| 10 | - | // LangKey is a key for setting and getting the language from the context | |||
| 11 | - | const LangKey = "CurrentLang" | |||
| 12 | - | ||||
| 13 | - | // LanguageDetectiondetects the current language and sets it in the context | |||
| 14 | - | func LanguageDetection(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 15 | - | return func(c echo.Context) error { | |||
| 16 | - | lang := detectCurrentLanguage(c) | |||
| 17 | - | c.Set(LangKey, lang) | |||
| 18 | - | return next(c) | |||
| 19 | - | } | |||
| 20 | - | } | |||
| 21 | - | ||||
| 22 | - | func detectCurrentLanguage(c echo.Context) string { | |||
| 23 | - | // Check if the lang key is set in the session storage | |||
| 24 | - | sess, err := session.Get("session", c) | |||
| 25 | - | if err != nil { | |||
| 26 | - | // If session storage is not available, proceed with "Accept-Language" header detection | |||
| 27 | - | return "en" | |||
| 28 | - | } | |||
| 29 | - | ||||
| 30 | - | if langValue, ok := sess.Values["lang"]; ok && langValue != nil { | |||
| 31 | - | return langValue.(string) // Return the lang value from the session storage | |||
| 32 | - | } | |||
| 33 | - | ||||
| 34 | - | // If lang key is not set, proceed with "Accept-Language" header detection | |||
| 35 | - | header := c.Request().Header.Get("Accept-Language") | |||
| 36 | - | if strings.Contains(header, "fr") { | |||
| 37 | - | return "fr" | |||
| 38 | - | } | |||
| 39 | - | return "en" // Default to English | |||
| 40 | - | } | |||
D internal/http/middlewares/logger.go +0 -40
| @@ -1,40 +0,0 @@ | |||
| 1 | - | package middlewares | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "strings" | |
| 5 | - | ||
| 6 | - | "github.com/labstack/echo/v4" | |
| 7 | - | "github.com/labstack/echo/v4/middleware" | |
| 8 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | // HTTPAccessLogger creates a new middleware to log HTTP requests using the provided HttpLogger. | |
| 12 | - | func HTTPAccessLogger(logger *utils.Logger) echo.MiddlewareFunc { | |
| 13 | - | return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ | |
| 14 | - | LogURI: true, | |
| 15 | - | LogStatus: true, | |
| 16 | - | LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { | |
| 17 | - | // Filter out any requests that contain the substring "sse" in the URI | |
| 18 | - | if strings.Contains(v.URI, "sse") { | |
| 19 | - | return nil | |
| 20 | - | } | |
| 21 | - | logger.Info(). | |
| 22 | - | Str("type", "http"). | |
| 23 | - | Str("remote_ip", c.RealIP()). | |
| 24 | - | Str("method", c.Request().Method). | |
| 25 | - | Str("URI", v.URI). | |
| 26 | - | Int("status", v.Status). | |
| 27 | - | Msg("request") | |
| 28 | - | return nil | |
| 29 | - | }, | |
| 30 | - | }) | |
| 31 | - | } | |
| 32 | - | ||
| 33 | - | func SetLogger(logger *utils.Logger) echo.MiddlewareFunc { | |
| 34 | - | return func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 35 | - | return func(c echo.Context) error { | |
| 36 | - | c.Set("logger", logger) | |
| 37 | - | return next(c) | |
| 38 | - | } | |
| 39 | - | } | |
| 40 | - | } | |
| @@ -1,40 +0,0 @@ | |||||
| 1 | - | package middlewares | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "strings" | |||
| 5 | - | ||||
| 6 | - | "github.com/labstack/echo/v4" | |||
| 7 | - | "github.com/labstack/echo/v4/middleware" | |||
| 8 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |||
| 9 | - | ) | |||
| 10 | - | ||||
| 11 | - | // HTTPAccessLogger creates a new middleware to log HTTP requests using the provided HttpLogger. | |||
| 12 | - | func HTTPAccessLogger(logger *utils.Logger) echo.MiddlewareFunc { | |||
| 13 | - | return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ | |||
| 14 | - | LogURI: true, | |||
| 15 | - | LogStatus: true, | |||
| 16 | - | LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { | |||
| 17 | - | // Filter out any requests that contain the substring "sse" in the URI | |||
| 18 | - | if strings.Contains(v.URI, "sse") { | |||
| 19 | - | return nil | |||
| 20 | - | } | |||
| 21 | - | logger.Info(). | |||
| 22 | - | Str("type", "http"). | |||
| 23 | - | Str("remote_ip", c.RealIP()). | |||
| 24 | - | Str("method", c.Request().Method). | |||
| 25 | - | Str("URI", v.URI). | |||
| 26 | - | Int("status", v.Status). | |||
| 27 | - | Msg("request") | |||
| 28 | - | return nil | |||
| 29 | - | }, | |||
| 30 | - | }) | |||
| 31 | - | } | |||
| 32 | - | ||||
| 33 | - | func SetLogger(logger *utils.Logger) echo.MiddlewareFunc { | |||
| 34 | - | return func(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 35 | - | return func(c echo.Context) error { | |||
| 36 | - | c.Set("logger", logger) | |||
| 37 | - | return next(c) | |||
| 38 | - | } | |||
| 39 | - | } | |||
| 40 | - | } | |||
D internal/http/middlewares/secure.go +0 -18
| @@ -1,18 +0,0 @@ | |||
| 1 | - | package middlewares | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "github.com/labstack/echo/v4" | |
| 5 | - | "github.com/labstack/echo/v4/middleware" | |
| 6 | - | ) | |
| 7 | - | ||
| 8 | - | func SecureRoutes() echo.MiddlewareFunc { | |
| 9 | - | return middleware.SecureWithConfig(middleware.SecureConfig{ | |
| 10 | - | XSSProtection: "1; mode=block", | |
| 11 | - | ContentTypeNosniff: "nosniff", | |
| 12 | - | XFrameOptions: "SAMEORIGIN", | |
| 13 | - | HSTSMaxAge: 3600, | |
| 14 | - | HSTSExcludeSubdomains: false, | |
| 15 | - | ContentSecurityPolicy: "default-src 'self'", | |
| 16 | - | // Skipper: YourCustomSkipperFunction, // If you have a custom skipper | |
| 17 | - | }) | |
| 18 | - | } | |
| @@ -1,18 +0,0 @@ | |||||
| 1 | - | package middlewares | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "github.com/labstack/echo/v4" | |||
| 5 | - | "github.com/labstack/echo/v4/middleware" | |||
| 6 | - | ) | |||
| 7 | - | ||||
| 8 | - | func SecureRoutes() echo.MiddlewareFunc { | |||
| 9 | - | return middleware.SecureWithConfig(middleware.SecureConfig{ | |||
| 10 | - | XSSProtection: "1; mode=block", | |||
| 11 | - | ContentTypeNosniff: "nosniff", | |||
| 12 | - | XFrameOptions: "SAMEORIGIN", | |||
| 13 | - | HSTSMaxAge: 3600, | |||
| 14 | - | HSTSExcludeSubdomains: false, | |||
| 15 | - | ContentSecurityPolicy: "default-src 'self'", | |||
| 16 | - | // Skipper: YourCustomSkipperFunction, // If you have a custom skipper | |||
| 17 | - | }) | |||
| 18 | - | } | |||
D internal/http/routes/admin-routes.go +0 -76
| @@ -1,76 +0,0 @@ | |||
| 1 | - | package routes | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "net/http" | |
| 6 | - | ||
| 7 | - | "github.com/labstack/echo/v4" | |
| 8 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |
| 9 | - | "gogs.bnema.dev/gordon-echo/internal/http/middlewares" | |
| 10 | - | "gogs.bnema.dev/gordon-echo/pkg/templating" | |
| 11 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |
| 12 | - | ) | |
| 13 | - | ||
| 14 | - | func AdminRoute(c echo.Context, ac *app.Config, a *app.App) error { | |
| 15 | - | // Check if it's the first launch of the app | |
| 16 | - | if utils.IsConfigFilePresent() { | |
| 17 | - | fmt.Println("Fresh installation detected, redirecting to /admin/install") | |
| 18 | - | return c.Redirect(http.StatusFound, "/admin/install") | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | // Retrieve the current language from the context | |
| 22 | - | lang := c.Get(middlewares.LangKey).(string) | |
| 23 | - | ||
| 24 | - | // Assuming the filename is "strings.yml" | |
| 25 | - | var data utils.StringsYamlData | |
| 26 | - | err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data) | |
| 27 | - | if err != nil { | |
| 28 | - | return err | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | renderer, err := templating.GetHTMLRenderer("index.gohtml", ac.GetPublicFS(), a) | |
| 32 | - | if err != nil { | |
| 33 | - | return err | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | html, err := renderer.Render(data.CurrentLang, a) | |
| 37 | - | if err != nil { | |
| 38 | - | // Return the error into NewLogger() to log it | |
| 39 | - | utils.NewLogger().Error() | |
| 40 | - | return err | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | return c.HTML(http.StatusOK, html) | |
| 44 | - | } | |
| 45 | - | ||
| 46 | - | func InstallRoute(c echo.Context, ac *app.Config, a *app.App) error { | |
| 47 | - | // Check if it's the first launch of the app | |
| 48 | - | if !utils.IsConfigFilePresent() { | |
| 49 | - | fmt.Println("Config file already present, redirecting to /admin") | |
| 50 | - | return c.Redirect(http.StatusFound, "/admin") | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | // Retrieve the current language from the context | |
| 54 | - | lang := c.Get(middlewares.LangKey).(string) | |
| 55 | - | ||
| 56 | - | var data utils.StringsYamlData | |
| 57 | - | err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data) | |
| 58 | - | if err != nil { | |
| 59 | - | return fmt.Errorf("failed to populate data from YAML: %w", err) | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | renderer, err := templating.GetHTMLRenderer("install.gohtml", ac.GetTemplateFS(), a) | |
| 63 | - | fmt.Println(renderer) | |
| 64 | - | if err != nil { | |
| 65 | - | return err | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | html, err := renderer.Render(data.CurrentLang, a) | |
| 69 | - | if err != nil { | |
| 70 | - | // Return the error into NewLogger() to log it | |
| 71 | - | utils.NewLogger().Error() | |
| 72 | - | return err | |
| 73 | - | } | |
| 74 | - | ||
| 75 | - | return c.HTML(http.StatusOK, html) | |
| 76 | - | } | |
| @@ -1,76 +0,0 @@ | |||||
| 1 | - | package routes | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "fmt" | |||
| 5 | - | "net/http" | |||
| 6 | - | ||||
| 7 | - | "github.com/labstack/echo/v4" | |||
| 8 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |||
| 9 | - | "gogs.bnema.dev/gordon-echo/internal/http/middlewares" | |||
| 10 | - | "gogs.bnema.dev/gordon-echo/pkg/templating" | |||
| 11 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |||
| 12 | - | ) | |||
| 13 | - | ||||
| 14 | - | func AdminRoute(c echo.Context, ac *app.Config, a *app.App) error { | |||
| 15 | - | // Check if it's the first launch of the app | |||
| 16 | - | if utils.IsConfigFilePresent() { | |||
| 17 | - | fmt.Println("Fresh installation detected, redirecting to /admin/install") | |||
| 18 | - | return c.Redirect(http.StatusFound, "/admin/install") | |||
| 19 | - | } | |||
| 20 | - | ||||
| 21 | - | // Retrieve the current language from the context | |||
| 22 | - | lang := c.Get(middlewares.LangKey).(string) | |||
| 23 | - | ||||
| 24 | - | // Assuming the filename is "strings.yml" | |||
| 25 | - | var data utils.StringsYamlData | |||
| 26 | - | err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data) | |||
| 27 | - | if err != nil { | |||
| 28 | - | return err | |||
| 29 | - | } | |||
| 30 | - | ||||
| 31 | - | renderer, err := templating.GetHTMLRenderer("index.gohtml", ac.GetPublicFS(), a) | |||
| 32 | - | if err != nil { | |||
| 33 | - | return err | |||
| 34 | - | } | |||
| 35 | - | ||||
| 36 | - | html, err := renderer.Render(data.CurrentLang, a) | |||
| 37 | - | if err != nil { | |||
| 38 | - | // Return the error into NewLogger() to log it | |||
| 39 | - | utils.NewLogger().Error() | |||
| 40 | - | return err | |||
| 41 | - | } | |||
| 42 | - | ||||
| 43 | - | return c.HTML(http.StatusOK, html) | |||
| 44 | - | } | |||
| 45 | - | ||||
| 46 | - | func InstallRoute(c echo.Context, ac *app.Config, a *app.App) error { | |||
| 47 | - | // Check if it's the first launch of the app | |||
| 48 | - | if !utils.IsConfigFilePresent() { | |||
| 49 | - | fmt.Println("Config file already present, redirecting to /admin") | |||
| 50 | - | return c.Redirect(http.StatusFound, "/admin") | |||
| 51 | - | } | |||
| 52 | - | ||||
| 53 | - | // Retrieve the current language from the context | |||
| 54 | - | lang := c.Get(middlewares.LangKey).(string) | |||
| 55 | - | ||||
| 56 | - | var data utils.StringsYamlData | |||
| 57 | - | err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data) | |||
| 58 | - | if err != nil { | |||
| 59 | - | return fmt.Errorf("failed to populate data from YAML: %w", err) | |||
| 60 | - | } | |||
| 61 | - | ||||
| 62 | - | renderer, err := templating.GetHTMLRenderer("install.gohtml", ac.GetTemplateFS(), a) | |||
| 63 | - | fmt.Println(renderer) | |||
| 64 | - | if err != nil { | |||
| 65 | - | return err | |||
| 66 | - | } | |||
| 67 | - | ||||
| 68 | - | html, err := renderer.Render(data.CurrentLang, a) | |||
| 69 | - | if err != nil { | |||
| 70 | - | // Return the error into NewLogger() to log it | |||
| 71 | - | utils.NewLogger().Error() | |||
| 72 | - | return err | |||
| 73 | - | } | |||
| 74 | - | ||||
| 75 | - | return c.HTML(http.StatusOK, html) | |||
| 76 | - | } | |||
D internal/http/routes/router.go +0 -66
| @@ -1,66 +0,0 @@ | |||
| 1 | - | package routes | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "github.com/labstack/echo/v4" | |
| 5 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |
| 6 | - | "gogs.bnema.dev/gordon-echo/internal/http/handlers" | |
| 7 | - | "gogs.bnema.dev/gordon-echo/internal/http/middlewares" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | func ConfigureRouter(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo { | |
| 11 | - | ||
| 12 | - | // SetLogger is a middleware that sets the logger in the context | |
| 13 | - | e.Use(middlewares.SetLogger(a.HttpLogger)) | |
| 14 | - | // HTTPAccessLogger is a middleware that logs HTTP requests | |
| 15 | - | e.Use(middlewares.HTTPAccessLogger(a.HttpLogger)) | |
| 16 | - | e.Use(middlewares.LanguageDetection) | |
| 17 | - | e.Use(middlewares.ColorSchemeDetection) | |
| 18 | - | //e.Use(middlewares.SecureMiddleware()) // Need testing | |
| 19 | - | ||
| 20 | - | // Register routes | |
| 21 | - | e = bindAdminRoutes(e, a, ac) | |
| 22 | - | e = bindHtmxRoutes(e, a, ac) | |
| 23 | - | e = bindStaticRoutes(e, a.Config) | |
| 24 | - | e.HTTPErrorHandler = handlers.ErrorNumberHandler | |
| 25 | - | return e | |
| 26 | - | } | |
| 27 | - | func bindAdminRoutes(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo { | |
| 28 | - | // We pass the embed config to the routes so we can parse the strings.yml file | |
| 29 | - | e.GET("/admin", func(c echo.Context) error { | |
| 30 | - | return AdminRoute(c, ac, a) | |
| 31 | - | }) | |
| 32 | - | e.GET("/admin/install", func(c echo.Context) error { | |
| 33 | - | return InstallRoute(c, ac, a) | |
| 34 | - | }) | |
| 35 | - | ||
| 36 | - | e.POST("/admin/install/traefik", func(c echo.Context) error { | |
| 37 | - | return handlers.TraefikInstallerHandler(c, a) | |
| 38 | - | }) | |
| 39 | - | e.GET("/admin/install/traefik/sse", func(c echo.Context) error { | |
| 40 | - | return handlers.TraefikInstallerSSEHandler(c, a) | |
| 41 | - | }) | |
| 42 | - | return e | |
| 43 | - | } | |
| 44 | - | ||
| 45 | - | func bindHtmxRoutes(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo { | |
| 46 | - | e.GET("/htmx", func(c echo.Context) error { | |
| 47 | - | return handlers.HTMXHandler(c, a, ac) | |
| 48 | - | }) | |
| 49 | - | e.GET("/htmx/fragment", func(c echo.Context) error { | |
| 50 | - | return handlers.HTMXHandler(c, a, ac) | |
| 51 | - | }) | |
| 52 | - | e.POST("/htmx", func(c echo.Context) error { | |
| 53 | - | return handlers.HTMXHandler(c, a, ac) | |
| 54 | - | }) | |
| 55 | - | e.POST("/htmx/fragment", func(c echo.Context) error { | |
| 56 | - | return handlers.HTMXHandler(c, a, ac) | |
| 57 | - | }) | |
| 58 | - | return e | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | func bindStaticRoutes(e *echo.Echo, config *app.Config) *echo.Echo { | |
| 62 | - | e.GET("/*", func(c echo.Context) error { | |
| 63 | - | return StaticRoute(c, config) | |
| 64 | - | }) | |
| 65 | - | return e | |
| 66 | - | } | |
| @@ -1,66 +0,0 @@ | |||||
| 1 | - | package routes | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "github.com/labstack/echo/v4" | |||
| 5 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |||
| 6 | - | "gogs.bnema.dev/gordon-echo/internal/http/handlers" | |||
| 7 | - | "gogs.bnema.dev/gordon-echo/internal/http/middlewares" | |||
| 8 | - | ) | |||
| 9 | - | ||||
| 10 | - | func ConfigureRouter(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo { | |||
| 11 | - | ||||
| 12 | - | // SetLogger is a middleware that sets the logger in the context | |||
| 13 | - | e.Use(middlewares.SetLogger(a.HttpLogger)) | |||
| 14 | - | // HTTPAccessLogger is a middleware that logs HTTP requests | |||
| 15 | - | e.Use(middlewares.HTTPAccessLogger(a.HttpLogger)) | |||
| 16 | - | e.Use(middlewares.LanguageDetection) | |||
| 17 | - | e.Use(middlewares.ColorSchemeDetection) | |||
| 18 | - | //e.Use(middlewares.SecureMiddleware()) // Need testing | |||
| 19 | - | ||||
| 20 | - | // Register routes | |||
| 21 | - | e = bindAdminRoutes(e, a, ac) | |||
| 22 | - | e = bindHtmxRoutes(e, a, ac) | |||
| 23 | - | e = bindStaticRoutes(e, a.Config) | |||
| 24 | - | e.HTTPErrorHandler = handlers.ErrorNumberHandler | |||
| 25 | - | return e | |||
| 26 | - | } | |||
| 27 | - | func bindAdminRoutes(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo { | |||
| 28 | - | // We pass the embed config to the routes so we can parse the strings.yml file | |||
| 29 | - | e.GET("/admin", func(c echo.Context) error { | |||
| 30 | - | return AdminRoute(c, ac, a) | |||
| 31 | - | }) | |||
| 32 | - | e.GET("/admin/install", func(c echo.Context) error { | |||
| 33 | - | return InstallRoute(c, ac, a) | |||
| 34 | - | }) | |||
| 35 | - | ||||
| 36 | - | e.POST("/admin/install/traefik", func(c echo.Context) error { | |||
| 37 | - | return handlers.TraefikInstallerHandler(c, a) | |||
| 38 | - | }) | |||
| 39 | - | e.GET("/admin/install/traefik/sse", func(c echo.Context) error { | |||
| 40 | - | return handlers.TraefikInstallerSSEHandler(c, a) | |||
| 41 | - | }) | |||
| 42 | - | return e | |||
| 43 | - | } | |||
| 44 | - | ||||
| 45 | - | func bindHtmxRoutes(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo { | |||
| 46 | - | e.GET("/htmx", func(c echo.Context) error { | |||
| 47 | - | return handlers.HTMXHandler(c, a, ac) | |||
| 48 | - | }) | |||
| 49 | - | e.GET("/htmx/fragment", func(c echo.Context) error { | |||
| 50 | - | return handlers.HTMXHandler(c, a, ac) | |||
| 51 | - | }) | |||
| 52 | - | e.POST("/htmx", func(c echo.Context) error { | |||
| 53 | - | return handlers.HTMXHandler(c, a, ac) | |||
| 54 | - | }) | |||
| 55 | - | e.POST("/htmx/fragment", func(c echo.Context) error { | |||
| 56 | - | return handlers.HTMXHandler(c, a, ac) | |||
| 57 | - | }) | |||
| 58 | - | return e | |||
| 59 | - | } | |||
| 60 | - | ||||
| 61 | - | func bindStaticRoutes(e *echo.Echo, config *app.Config) *echo.Echo { | |||
| 62 | - | e.GET("/*", func(c echo.Context) error { | |||
| 63 | - | return StaticRoute(c, config) | |||
| 64 | - | }) | |||
| 65 | - | return e | |||
| 66 | - | } | |||
D internal/http/routes/static-routes.go +0 -25
| @@ -1,25 +0,0 @@ | |||
| 1 | - | package routes | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "io/fs" | |
| 5 | - | "net/http" | |
| 6 | - | "os" | |
| 7 | - | ||
| 8 | - | "github.com/labstack/echo/v4" | |
| 9 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |
| 10 | - | ) | |
| 11 | - | ||
| 12 | - | func StaticRoute(c echo.Context, config *app.Config) error { | |
| 13 | - | ||
| 14 | - | // Set the cache-control header based on PROD environment variable | |
| 15 | - | if os.Getenv("PROD") == "true" { | |
| 16 | - | c.Response().Header().Set("Cache-Control", "public, max-age=86400") | |
| 17 | - | } else { | |
| 18 | - | c.Response().Header().Set("Cache-Control", "no-cache") | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | // Serve the file from the embedded filesystem | |
| 22 | - | httpFs := http.FileServer(http.FS(fs.FS(config.GetPublicFS()))) | |
| 23 | - | httpFs.ServeHTTP(c.Response(), c.Request()) | |
| 24 | - | return nil | |
| 25 | - | } | |
| @@ -1,25 +0,0 @@ | |||||
| 1 | - | package routes | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "io/fs" | |||
| 5 | - | "net/http" | |||
| 6 | - | "os" | |||
| 7 | - | ||||
| 8 | - | "github.com/labstack/echo/v4" | |||
| 9 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |||
| 10 | - | ) | |||
| 11 | - | ||||
| 12 | - | func StaticRoute(c echo.Context, config *app.Config) error { | |||
| 13 | - | ||||
| 14 | - | // Set the cache-control header based on PROD environment variable | |||
| 15 | - | if os.Getenv("PROD") == "true" { | |||
| 16 | - | c.Response().Header().Set("Cache-Control", "public, max-age=86400") | |||
| 17 | - | } else { | |||
| 18 | - | c.Response().Header().Set("Cache-Control", "no-cache") | |||
| 19 | - | } | |||
| 20 | - | ||||
| 21 | - | // Serve the file from the embedded filesystem | |||
| 22 | - | httpFs := http.FileServer(http.FS(fs.FS(config.GetPublicFS()))) | |||
| 23 | - | httpFs.ServeHTTP(c.Response(), c.Request()) | |||
| 24 | - | return nil | |||
| 25 | - | } | |||
D internal/ui/public/404.gohtml +0 -12
| @@ -1,12 +0,0 @@ | |||
| 1 | - | <!DOCTYPE html> | |
| 2 | - | <html lang="en"> | |
| 3 | - | <head> | |
| 4 | - | <meta charset="UTF-8" /> | |
| 5 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 6 | - | <title>Error 404</title> | |
| 7 | - | </head> | |
| 8 | - | <body> | |
| 9 | - | <h1>Error 404</h1> | |
| 10 | - | <p>Page not found</p> | |
| 11 | - | </body> | |
| 12 | - | </html> | |
| @@ -1,12 +0,0 @@ | |||||
| 1 | - | <!DOCTYPE html> | |||
| 2 | - | <html lang="en"> | |||
| 3 | - | <head> | |||
| 4 | - | <meta charset="UTF-8" /> | |||
| 5 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
| 6 | - | <title>Error 404</title> | |||
| 7 | - | </head> | |||
| 8 | - | <body> | |||
| 9 | - | <h1>Error 404</h1> | |||
| 10 | - | <p>Page not found</p> | |||
| 11 | - | </body> | |||
| 12 | - | </html> | |||
D internal/ui/public/500.gohtml +0 -12
| @@ -1,12 +0,0 @@ | |||
| 1 | - | <!DOCTYPE html> | |
| 2 | - | <html lang="en"> | |
| 3 | - | <head> | |
| 4 | - | <meta charset="UTF-8" /> | |
| 5 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 6 | - | <title>Error 500</title> | |
| 7 | - | </head> | |
| 8 | - | <body> | |
| 9 | - | <h1>500 Internal Server Error</h1> | |
| 10 | - | <p>Something went really wrong o/</p> | |
| 11 | - | </body> | |
| 12 | - | </html> | |
| @@ -1,12 +0,0 @@ | |||||
| 1 | - | <!DOCTYPE html> | |||
| 2 | - | <html lang="en"> | |||
| 3 | - | <head> | |||
| 4 | - | <meta charset="UTF-8" /> | |||
| 5 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
| 6 | - | <title>Error 500</title> | |||
| 7 | - | </head> | |||
| 8 | - | <body> | |||
| 9 | - | <h1>500 Internal Server Error</h1> | |||
| 10 | - | <p>Something went really wrong o/</p> | |||
| 11 | - | </body> | |||
| 12 | - | </html> | |||
D internal/ui/public/android-chrome-192x192.png +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/android-chrome-512x512.png +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/apple-touch-icon.png +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/css/custom.css +0 -116
| @@ -1,116 +0,0 @@ | |||
| 1 | - | /** | |
| 2 | - | * Path: internal/ui/public/assets/css/custom.css | |
| 3 | - | * | |
| 4 | - | * @format | |
| 5 | - | */ | |
| 6 | - | ||
| 7 | - | /* Embeded path : assets/css/cutsom.css */ | |
| 8 | - | ||
| 9 | - | @tailwind base; | |
| 10 | - | @tailwind components; | |
| 11 | - | @tailwind utilities; | |
| 12 | - | ||
| 13 | - | /* Import IBM Plex Fonts */ | |
| 14 | - | @font-face { | |
| 15 | - | font-family: "IBMPlexSans-Bold"; | |
| 16 | - | src: url("/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2") format("woff2"); | |
| 17 | - | font-weight: bold; | |
| 18 | - | font-style: normal; | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | @font-face { | |
| 22 | - | font-family: "IBMPlexSans-Regular"; | |
| 23 | - | src: url("/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2") format("woff2"); | |
| 24 | - | font-weight: normal; | |
| 25 | - | font-style: normal; | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | /* Import Jetbrains Mono Nerd Font */ | |
| 29 | - | @font-face { | |
| 30 | - | font-family: "JetBrainsMono-Nerd-Font-Regular"; | |
| 31 | - | src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFontMono-Regular.woff2") | |
| 32 | - | format("woff2"); | |
| 33 | - | font-weight: normal; | |
| 34 | - | font-style: normal; | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | @font-face { | |
| 38 | - | font-family: "LilitaOne-Regular"; | |
| 39 | - | src: url("/assets/fonts/LilitaOne/LilitaOne-Regular.ttf") format("truetype"); | |
| 40 | - | font-weight: normal; | |
| 41 | - | font-style: normal; | |
| 42 | - | } | |
| 43 | - | ||
| 44 | - | .fade-me-out.htmx-swapping { | |
| 45 | - | opacity: 0; | |
| 46 | - | transition: opacity 1s ease-out; | |
| 47 | - | } | |
| 48 | - | ||
| 49 | - | /* View transition */ | |
| 50 | - | @keyframes fade-in { | |
| 51 | - | from { | |
| 52 | - | opacity: 0; | |
| 53 | - | } | |
| 54 | - | } | |
| 55 | - | ||
| 56 | - | @keyframes fade-out { | |
| 57 | - | to { | |
| 58 | - | opacity: 0; | |
| 59 | - | } | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | @keyframes slide-from-right { | |
| 63 | - | from { | |
| 64 | - | transform: translateX(90px); | |
| 65 | - | } | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | @keyframes slide-to-left { | |
| 69 | - | to { | |
| 70 | - | transform: translateX(-90px); | |
| 71 | - | } | |
| 72 | - | } | |
| 73 | - | ||
| 74 | - | .slide-it { | |
| 75 | - | view-transition-name: slide-it; | |
| 76 | - | } | |
| 77 | - | ||
| 78 | - | ::view-transition-old(slide-it) { | |
| 79 | - | animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out, | |
| 80 | - | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; | |
| 81 | - | } | |
| 82 | - | ::view-transition-new(slide-it) { | |
| 83 | - | animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, | |
| 84 | - | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; | |
| 85 | - | } | |
| 86 | - | ||
| 87 | - | .gordon-title { | |
| 88 | - | font-family: "LilitaOne-Regular", sans-serif; | |
| 89 | - | ||
| 90 | - | } | |
| 91 | - | ||
| 92 | - | .IBMPlexSans { | |
| 93 | - | font-family: "IBMPlexSans-Regular", sans-serif; | |
| 94 | - | } | |
| 95 | - | ||
| 96 | - | .circle-shadow { | |
| 97 | - | width: 100%; | |
| 98 | - | position: relative; | |
| 99 | - | } | |
| 100 | - | ||
| 101 | - | .circle-shadow::after { | |
| 102 | - | position: absolute; | |
| 103 | - | width: 50%; | |
| 104 | - | height: 0.4rem; | |
| 105 | - | margin: auto; | |
| 106 | - | background: rgba(0, 0, 0, 0.1); | |
| 107 | - | border-radius: 50%; | |
| 108 | - | z-index: -1; | |
| 109 | - | bottom: 2%; | |
| 110 | - | content: ""; | |
| 111 | - | box-shadow: 0 0 1px 0.3rem rgba(0, 0, 0, 0.1); | |
| 112 | - | } | |
| 113 | - | ||
| 114 | - | .iconoir { | |
| 115 | - | margin-right: 8px; | |
| 116 | - | } | |
| @@ -1,116 +0,0 @@ | |||||
| 1 | - | /** | |||
| 2 | - | * Path: internal/ui/public/assets/css/custom.css | |||
| 3 | - | * | |||
| 4 | - | * @format | |||
| 5 | - | */ | |||
| 6 | - | ||||
| 7 | - | /* Embeded path : assets/css/cutsom.css */ | |||
| 8 | - | ||||
| 9 | - | @tailwind base; | |||
| 10 | - | @tailwind components; | |||
| 11 | - | @tailwind utilities; | |||
| 12 | - | ||||
| 13 | - | /* Import IBM Plex Fonts */ | |||
| 14 | - | @font-face { | |||
| 15 | - | font-family: "IBMPlexSans-Bold"; | |||
| 16 | - | src: url("/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2") format("woff2"); | |||
| 17 | - | font-weight: bold; | |||
| 18 | - | font-style: normal; | |||
| 19 | - | } | |||
| 20 | - | ||||
| 21 | - | @font-face { | |||
| 22 | - | font-family: "IBMPlexSans-Regular"; | |||
| 23 | - | src: url("/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2") format("woff2"); | |||
| 24 | - | font-weight: normal; | |||
| 25 | - | font-style: normal; | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | /* Import Jetbrains Mono Nerd Font */ | |||
| 29 | - | @font-face { | |||
| 30 | - | font-family: "JetBrainsMono-Nerd-Font-Regular"; | |||
| 31 | - | src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFontMono-Regular.woff2") | |||
| 32 | - | format("woff2"); | |||
| 33 | - | font-weight: normal; | |||
| 34 | - | font-style: normal; | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | @font-face { | |||
| 38 | - | font-family: "LilitaOne-Regular"; | |||
| 39 | - | src: url("/assets/fonts/LilitaOne/LilitaOne-Regular.ttf") format("truetype"); | |||
| 40 | - | font-weight: normal; | |||
| 41 | - | font-style: normal; | |||
| 42 | - | } | |||
| 43 | - | ||||
| 44 | - | .fade-me-out.htmx-swapping { | |||
| 45 | - | opacity: 0; | |||
| 46 | - | transition: opacity 1s ease-out; | |||
| 47 | - | } | |||
| 48 | - | ||||
| 49 | - | /* View transition */ | |||
| 50 | - | @keyframes fade-in { | |||
| 51 | - | from { | |||
| 52 | - | opacity: 0; | |||
| 53 | - | } | |||
| 54 | - | } | |||
| 55 | - | ||||
| 56 | - | @keyframes fade-out { | |||
| 57 | - | to { | |||
| 58 | - | opacity: 0; | |||
| 59 | - | } | |||
| 60 | - | } | |||
| 61 | - | ||||
| 62 | - | @keyframes slide-from-right { | |||
| 63 | - | from { | |||
| 64 | - | transform: translateX(90px); | |||
| 65 | - | } | |||
| 66 | - | } | |||
| 67 | - | ||||
| 68 | - | @keyframes slide-to-left { | |||
| 69 | - | to { | |||
| 70 | - | transform: translateX(-90px); | |||
| 71 | - | } | |||
| 72 | - | } | |||
| 73 | - | ||||
| 74 | - | .slide-it { | |||
| 75 | - | view-transition-name: slide-it; | |||
| 76 | - | } | |||
| 77 | - | ||||
| 78 | - | ::view-transition-old(slide-it) { | |||
| 79 | - | animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out, | |||
| 80 | - | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; | |||
| 81 | - | } | |||
| 82 | - | ::view-transition-new(slide-it) { | |||
| 83 | - | animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, | |||
| 84 | - | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; | |||
| 85 | - | } | |||
| 86 | - | ||||
| 87 | - | .gordon-title { | |||
| 88 | - | font-family: "LilitaOne-Regular", sans-serif; | |||
| 89 | - | ||||
| 90 | - | } | |||
| 91 | - | ||||
| 92 | - | .IBMPlexSans { | |||
| 93 | - | font-family: "IBMPlexSans-Regular", sans-serif; | |||
| 94 | - | } | |||
| 95 | - | ||||
| 96 | - | .circle-shadow { | |||
| 97 | - | width: 100%; | |||
| 98 | - | position: relative; | |||
| 99 | - | } | |||
| 100 | - | ||||
| 101 | - | .circle-shadow::after { | |||
| 102 | - | position: absolute; | |||
| 103 | - | width: 50%; | |||
| 104 | - | height: 0.4rem; | |||
| 105 | - | margin: auto; | |||
| 106 | - | background: rgba(0, 0, 0, 0.1); | |||
| 107 | - | border-radius: 50%; | |||
| 108 | - | z-index: -1; | |||
| 109 | - | bottom: 2%; | |||
| 110 | - | content: ""; | |||
| 111 | - | box-shadow: 0 0 1px 0.3rem rgba(0, 0, 0, 0.1); | |||
| 112 | - | } | |||
| 113 | - | ||||
| 114 | - | .iconoir { | |||
| 115 | - | margin-right: 8px; | |||
| 116 | - | } | |||
D internal/ui/public/assets/css/tailwind.css +0 -1046
| @@ -1,1046 +0,0 @@ | |||
| 1 | - | /** | |
| 2 | - | * Path: internal/ui/public/assets/css/custom.css | |
| 3 | - | * | |
| 4 | - | * @format | |
| 5 | - | */ | |
| 6 | - | ||
| 7 | - | /* Embeded path : assets/css/cutsom.css */ | |
| 8 | - | ||
| 9 | - | /*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com | |
| 10 | - | */ | |
| 11 | - | ||
| 12 | - | /* | |
| 13 | - | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) | |
| 14 | - | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) | |
| 15 | - | */ | |
| 16 | - | ||
| 17 | - | *, | |
| 18 | - | ::before, | |
| 19 | - | ::after { | |
| 20 | - | box-sizing: border-box; | |
| 21 | - | /* 1 */ | |
| 22 | - | border-width: 0; | |
| 23 | - | /* 2 */ | |
| 24 | - | border-style: solid; | |
| 25 | - | /* 2 */ | |
| 26 | - | border-color: #e5e7eb; | |
| 27 | - | /* 2 */ | |
| 28 | - | } | |
| 29 | - | ||
| 30 | - | ::before, | |
| 31 | - | ::after { | |
| 32 | - | --tw-content: ''; | |
| 33 | - | } | |
| 34 | - | ||
| 35 | - | /* | |
| 36 | - | 1. Use a consistent sensible line-height in all browsers. | |
| 37 | - | 2. Prevent adjustments of font size after orientation changes in iOS. | |
| 38 | - | 3. Use a more readable tab size. | |
| 39 | - | 4. Use the user's configured `sans` font-family by default. | |
| 40 | - | 5. Use the user's configured `sans` font-feature-settings by default. | |
| 41 | - | 6. Use the user's configured `sans` font-variation-settings by default. | |
| 42 | - | */ | |
| 43 | - | ||
| 44 | - | html { | |
| 45 | - | line-height: 1.5; | |
| 46 | - | /* 1 */ | |
| 47 | - | -webkit-text-size-adjust: 100%; | |
| 48 | - | /* 2 */ | |
| 49 | - | -moz-tab-size: 4; | |
| 50 | - | /* 3 */ | |
| 51 | - | -o-tab-size: 4; | |
| 52 | - | tab-size: 4; | |
| 53 | - | /* 3 */ | |
| 54 | - | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; | |
| 55 | - | /* 4 */ | |
| 56 | - | font-feature-settings: normal; | |
| 57 | - | /* 5 */ | |
| 58 | - | font-variation-settings: normal; | |
| 59 | - | /* 6 */ | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | /* | |
| 63 | - | 1. Remove the margin in all browsers. | |
| 64 | - | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. | |
| 65 | - | */ | |
| 66 | - | ||
| 67 | - | body { | |
| 68 | - | margin: 0; | |
| 69 | - | /* 1 */ | |
| 70 | - | line-height: inherit; | |
| 71 | - | /* 2 */ | |
| 72 | - | } | |
| 73 | - | ||
| 74 | - | /* | |
| 75 | - | 1. Add the correct height in Firefox. | |
| 76 | - | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) | |
| 77 | - | 3. Ensure horizontal rules are visible by default. | |
| 78 | - | */ | |
| 79 | - | ||
| 80 | - | hr { | |
| 81 | - | height: 0; | |
| 82 | - | /* 1 */ | |
| 83 | - | color: inherit; | |
| 84 | - | /* 2 */ | |
| 85 | - | border-top-width: 1px; | |
| 86 | - | /* 3 */ | |
| 87 | - | } | |
| 88 | - | ||
| 89 | - | /* | |
| 90 | - | Add the correct text decoration in Chrome, Edge, and Safari. | |
| 91 | - | */ | |
| 92 | - | ||
| 93 | - | abbr:where([title]) { | |
| 94 | - | -webkit-text-decoration: underline dotted; | |
| 95 | - | text-decoration: underline dotted; | |
| 96 | - | } | |
| 97 | - | ||
| 98 | - | /* | |
| 99 | - | Remove the default font size and weight for headings. | |
| 100 | - | */ | |
| 101 | - | ||
| 102 | - | h1, | |
| 103 | - | h2, | |
| 104 | - | h3, | |
| 105 | - | h4, | |
| 106 | - | h5, | |
| 107 | - | h6 { | |
| 108 | - | font-size: inherit; | |
| 109 | - | font-weight: inherit; | |
| 110 | - | } | |
| 111 | - | ||
| 112 | - | /* | |
| 113 | - | Reset links to optimize for opt-in styling instead of opt-out. | |
| 114 | - | */ | |
| 115 | - | ||
| 116 | - | a { | |
| 117 | - | color: inherit; | |
| 118 | - | text-decoration: inherit; | |
| 119 | - | } | |
| 120 | - | ||
| 121 | - | /* | |
| 122 | - | Add the correct font weight in Edge and Safari. | |
| 123 | - | */ | |
| 124 | - | ||
| 125 | - | b, | |
| 126 | - | strong { | |
| 127 | - | font-weight: bolder; | |
| 128 | - | } | |
| 129 | - | ||
| 130 | - | /* | |
| 131 | - | 1. Use the user's configured `mono` font family by default. | |
| 132 | - | 2. Correct the odd `em` font sizing in all browsers. | |
| 133 | - | */ | |
| 134 | - | ||
| 135 | - | code, | |
| 136 | - | kbd, | |
| 137 | - | samp, | |
| 138 | - | pre { | |
| 139 | - | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; | |
| 140 | - | /* 1 */ | |
| 141 | - | font-size: 1em; | |
| 142 | - | /* 2 */ | |
| 143 | - | } | |
| 144 | - | ||
| 145 | - | /* | |
| 146 | - | Add the correct font size in all browsers. | |
| 147 | - | */ | |
| 148 | - | ||
| 149 | - | small { | |
| 150 | - | font-size: 80%; | |
| 151 | - | } | |
| 152 | - | ||
| 153 | - | /* | |
| 154 | - | Prevent `sub` and `sup` elements from affecting the line height in all browsers. | |
| 155 | - | */ | |
| 156 | - | ||
| 157 | - | sub, | |
| 158 | - | sup { | |
| 159 | - | font-size: 75%; | |
| 160 | - | line-height: 0; | |
| 161 | - | position: relative; | |
| 162 | - | vertical-align: baseline; | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | sub { | |
| 166 | - | bottom: -0.25em; | |
| 167 | - | } | |
| 168 | - | ||
| 169 | - | sup { | |
| 170 | - | top: -0.5em; | |
| 171 | - | } | |
| 172 | - | ||
| 173 | - | /* | |
| 174 | - | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) | |
| 175 | - | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) | |
| 176 | - | 3. Remove gaps between table borders by default. | |
| 177 | - | */ | |
| 178 | - | ||
| 179 | - | table { | |
| 180 | - | text-indent: 0; | |
| 181 | - | /* 1 */ | |
| 182 | - | border-color: inherit; | |
| 183 | - | /* 2 */ | |
| 184 | - | border-collapse: collapse; | |
| 185 | - | /* 3 */ | |
| 186 | - | } | |
| 187 | - | ||
| 188 | - | /* | |
| 189 | - | 1. Change the font styles in all browsers. | |
| 190 | - | 2. Remove the margin in Firefox and Safari. | |
| 191 | - | 3. Remove default padding in all browsers. | |
| 192 | - | */ | |
| 193 | - | ||
| 194 | - | button, | |
| 195 | - | input, | |
| 196 | - | optgroup, | |
| 197 | - | select, | |
| 198 | - | textarea { | |
| 199 | - | font-family: inherit; | |
| 200 | - | /* 1 */ | |
| 201 | - | font-feature-settings: inherit; | |
| 202 | - | /* 1 */ | |
| 203 | - | font-variation-settings: inherit; | |
| 204 | - | /* 1 */ | |
| 205 | - | font-size: 100%; | |
| 206 | - | /* 1 */ | |
| 207 | - | font-weight: inherit; | |
| 208 | - | /* 1 */ | |
| 209 | - | line-height: inherit; | |
| 210 | - | /* 1 */ | |
| 211 | - | color: inherit; | |
| 212 | - | /* 1 */ | |
| 213 | - | margin: 0; | |
| 214 | - | /* 2 */ | |
| 215 | - | padding: 0; | |
| 216 | - | /* 3 */ | |
| 217 | - | } | |
| 218 | - | ||
| 219 | - | /* | |
| 220 | - | Remove the inheritance of text transform in Edge and Firefox. | |
| 221 | - | */ | |
| 222 | - | ||
| 223 | - | button, | |
| 224 | - | select { | |
| 225 | - | text-transform: none; | |
| 226 | - | } | |
| 227 | - | ||
| 228 | - | /* | |
| 229 | - | 1. Correct the inability to style clickable types in iOS and Safari. | |
| 230 | - | 2. Remove default button styles. | |
| 231 | - | */ | |
| 232 | - | ||
| 233 | - | button, | |
| 234 | - | [type='button'], | |
| 235 | - | [type='reset'], | |
| 236 | - | [type='submit'] { | |
| 237 | - | -webkit-appearance: button; | |
| 238 | - | /* 1 */ | |
| 239 | - | background-color: transparent; | |
| 240 | - | /* 2 */ | |
| 241 | - | background-image: none; | |
| 242 | - | /* 2 */ | |
| 243 | - | } | |
| 244 | - | ||
| 245 | - | /* | |
| 246 | - | Use the modern Firefox focus style for all focusable elements. | |
| 247 | - | */ | |
| 248 | - | ||
| 249 | - | :-moz-focusring { | |
| 250 | - | outline: auto; | |
| 251 | - | } | |
| 252 | - | ||
| 253 | - | /* | |
| 254 | - | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) | |
| 255 | - | */ | |
| 256 | - | ||
| 257 | - | :-moz-ui-invalid { | |
| 258 | - | box-shadow: none; | |
| 259 | - | } | |
| 260 | - | ||
| 261 | - | /* | |
| 262 | - | Add the correct vertical alignment in Chrome and Firefox. | |
| 263 | - | */ | |
| 264 | - | ||
| 265 | - | progress { | |
| 266 | - | vertical-align: baseline; | |
| 267 | - | } | |
| 268 | - | ||
| 269 | - | /* | |
| 270 | - | Correct the cursor style of increment and decrement buttons in Safari. | |
| 271 | - | */ | |
| 272 | - | ||
| 273 | - | ::-webkit-inner-spin-button, | |
| 274 | - | ::-webkit-outer-spin-button { | |
| 275 | - | height: auto; | |
| 276 | - | } | |
| 277 | - | ||
| 278 | - | /* | |
| 279 | - | 1. Correct the odd appearance in Chrome and Safari. | |
| 280 | - | 2. Correct the outline style in Safari. | |
| 281 | - | */ | |
| 282 | - | ||
| 283 | - | [type='search'] { | |
| 284 | - | -webkit-appearance: textfield; | |
| 285 | - | /* 1 */ | |
| 286 | - | outline-offset: -2px; | |
| 287 | - | /* 2 */ | |
| 288 | - | } | |
| 289 | - | ||
| 290 | - | /* | |
| 291 | - | Remove the inner padding in Chrome and Safari on macOS. | |
| 292 | - | */ | |
| 293 | - | ||
| 294 | - | ::-webkit-search-decoration { | |
| 295 | - | -webkit-appearance: none; | |
| 296 | - | } | |
| 297 | - | ||
| 298 | - | /* | |
| 299 | - | 1. Correct the inability to style clickable types in iOS and Safari. | |
| 300 | - | 2. Change font properties to `inherit` in Safari. | |
| 301 | - | */ | |
| 302 | - | ||
| 303 | - | ::-webkit-file-upload-button { | |
| 304 | - | -webkit-appearance: button; | |
| 305 | - | /* 1 */ | |
| 306 | - | font: inherit; | |
| 307 | - | /* 2 */ | |
| 308 | - | } | |
| 309 | - | ||
| 310 | - | /* | |
| 311 | - | Add the correct display in Chrome and Safari. | |
| 312 | - | */ | |
| 313 | - | ||
| 314 | - | summary { | |
| 315 | - | display: list-item; | |
| 316 | - | } | |
| 317 | - | ||
| 318 | - | /* | |
| 319 | - | Removes the default spacing and border for appropriate elements. | |
| 320 | - | */ | |
| 321 | - | ||
| 322 | - | blockquote, | |
| 323 | - | dl, | |
| 324 | - | dd, | |
| 325 | - | h1, | |
| 326 | - | h2, | |
| 327 | - | h3, | |
| 328 | - | h4, | |
| 329 | - | h5, | |
| 330 | - | h6, | |
| 331 | - | hr, | |
| 332 | - | figure, | |
| 333 | - | p, | |
| 334 | - | pre { | |
| 335 | - | margin: 0; | |
| 336 | - | } | |
| 337 | - | ||
| 338 | - | fieldset { | |
| 339 | - | margin: 0; | |
| 340 | - | padding: 0; | |
| 341 | - | } | |
| 342 | - | ||
| 343 | - | legend { | |
| 344 | - | padding: 0; | |
| 345 | - | } | |
| 346 | - | ||
| 347 | - | ol, | |
| 348 | - | ul, | |
| 349 | - | menu { | |
| 350 | - | list-style: none; | |
| 351 | - | margin: 0; | |
| 352 | - | padding: 0; | |
| 353 | - | } | |
| 354 | - | ||
| 355 | - | /* | |
| 356 | - | Reset default styling for dialogs. | |
| 357 | - | */ | |
| 358 | - | ||
| 359 | - | dialog { | |
| 360 | - | padding: 0; | |
| 361 | - | } | |
| 362 | - | ||
| 363 | - | /* | |
| 364 | - | Prevent resizing textareas horizontally by default. | |
| 365 | - | */ | |
| 366 | - | ||
| 367 | - | textarea { | |
| 368 | - | resize: vertical; | |
| 369 | - | } | |
| 370 | - | ||
| 371 | - | /* | |
| 372 | - | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) | |
| 373 | - | 2. Set the default placeholder color to the user's configured gray 400 color. | |
| 374 | - | */ | |
| 375 | - | ||
| 376 | - | input::-moz-placeholder, textarea::-moz-placeholder { | |
| 377 | - | opacity: 1; | |
| 378 | - | /* 1 */ | |
| 379 | - | color: #9ca3af; | |
| 380 | - | /* 2 */ | |
| 381 | - | } | |
| 382 | - | ||
| 383 | - | input::placeholder, | |
| 384 | - | textarea::placeholder { | |
| 385 | - | opacity: 1; | |
| 386 | - | /* 1 */ | |
| 387 | - | color: #9ca3af; | |
| 388 | - | /* 2 */ | |
| 389 | - | } | |
| 390 | - | ||
| 391 | - | /* | |
| 392 | - | Set the default cursor for buttons. | |
| 393 | - | */ | |
| 394 | - | ||
| 395 | - | button, | |
| 396 | - | [role="button"] { | |
| 397 | - | cursor: pointer; | |
| 398 | - | } | |
| 399 | - | ||
| 400 | - | /* | |
| 401 | - | Make sure disabled buttons don't get the pointer cursor. | |
| 402 | - | */ | |
| 403 | - | ||
| 404 | - | :disabled { | |
| 405 | - | cursor: default; | |
| 406 | - | } | |
| 407 | - | ||
| 408 | - | /* | |
| 409 | - | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) | |
| 410 | - | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) | |
| 411 | - | This can trigger a poorly considered lint error in some tools but is included by design. | |
| 412 | - | */ | |
| 413 | - | ||
| 414 | - | img, | |
| 415 | - | svg, | |
| 416 | - | video, | |
| 417 | - | canvas, | |
| 418 | - | audio, | |
| 419 | - | iframe, | |
| 420 | - | embed, | |
| 421 | - | object { | |
| 422 | - | display: block; | |
| 423 | - | /* 1 */ | |
| 424 | - | vertical-align: middle; | |
| 425 | - | /* 2 */ | |
| 426 | - | } | |
| 427 | - | ||
| 428 | - | /* | |
| 429 | - | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) | |
| 430 | - | */ | |
| 431 | - | ||
| 432 | - | img, | |
| 433 | - | video { | |
| 434 | - | max-width: 100%; | |
| 435 | - | height: auto; | |
| 436 | - | } | |
| 437 | - | ||
| 438 | - | /* Make elements with the HTML hidden attribute stay hidden by default */ | |
| 439 | - | ||
| 440 | - | [hidden] { | |
| 441 | - | display: none; | |
| 442 | - | } | |
| 443 | - | ||
| 444 | - | *, ::before, ::after { | |
| 445 | - | --tw-border-spacing-x: 0; | |
| 446 | - | --tw-border-spacing-y: 0; | |
| 447 | - | --tw-translate-x: 0; | |
| 448 | - | --tw-translate-y: 0; | |
| 449 | - | --tw-rotate: 0; | |
| 450 | - | --tw-skew-x: 0; | |
| 451 | - | --tw-skew-y: 0; | |
| 452 | - | --tw-scale-x: 1; | |
| 453 | - | --tw-scale-y: 1; | |
| 454 | - | --tw-pan-x: ; | |
| 455 | - | --tw-pan-y: ; | |
| 456 | - | --tw-pinch-zoom: ; | |
| 457 | - | --tw-scroll-snap-strictness: proximity; | |
| 458 | - | --tw-gradient-from-position: ; | |
| 459 | - | --tw-gradient-via-position: ; | |
| 460 | - | --tw-gradient-to-position: ; | |
| 461 | - | --tw-ordinal: ; | |
| 462 | - | --tw-slashed-zero: ; | |
| 463 | - | --tw-numeric-figure: ; | |
| 464 | - | --tw-numeric-spacing: ; | |
| 465 | - | --tw-numeric-fraction: ; | |
| 466 | - | --tw-ring-inset: ; | |
| 467 | - | --tw-ring-offset-width: 0px; | |
| 468 | - | --tw-ring-offset-color: #fff; | |
| 469 | - | --tw-ring-color: rgb(59 130 246 / 0.5); | |
| 470 | - | --tw-ring-offset-shadow: 0 0 #0000; | |
| 471 | - | --tw-ring-shadow: 0 0 #0000; | |
| 472 | - | --tw-shadow: 0 0 #0000; | |
| 473 | - | --tw-shadow-colored: 0 0 #0000; | |
| 474 | - | --tw-blur: ; | |
| 475 | - | --tw-brightness: ; | |
| 476 | - | --tw-contrast: ; | |
| 477 | - | --tw-grayscale: ; | |
| 478 | - | --tw-hue-rotate: ; | |
| 479 | - | --tw-invert: ; | |
| 480 | - | --tw-saturate: ; | |
| 481 | - | --tw-sepia: ; | |
| 482 | - | --tw-drop-shadow: ; | |
| 483 | - | --tw-backdrop-blur: ; | |
| 484 | - | --tw-backdrop-brightness: ; | |
| 485 | - | --tw-backdrop-contrast: ; | |
| 486 | - | --tw-backdrop-grayscale: ; | |
| 487 | - | --tw-backdrop-hue-rotate: ; | |
| 488 | - | --tw-backdrop-invert: ; | |
| 489 | - | --tw-backdrop-opacity: ; | |
| 490 | - | --tw-backdrop-saturate: ; | |
| 491 | - | --tw-backdrop-sepia: ; | |
| 492 | - | } | |
| 493 | - | ||
| 494 | - | ::backdrop { | |
| 495 | - | --tw-border-spacing-x: 0; | |
| 496 | - | --tw-border-spacing-y: 0; | |
| 497 | - | --tw-translate-x: 0; | |
| 498 | - | --tw-translate-y: 0; | |
| 499 | - | --tw-rotate: 0; | |
| 500 | - | --tw-skew-x: 0; | |
| 501 | - | --tw-skew-y: 0; | |
| 502 | - | --tw-scale-x: 1; | |
| 503 | - | --tw-scale-y: 1; | |
| 504 | - | --tw-pan-x: ; | |
| 505 | - | --tw-pan-y: ; | |
| 506 | - | --tw-pinch-zoom: ; | |
| 507 | - | --tw-scroll-snap-strictness: proximity; | |
| 508 | - | --tw-gradient-from-position: ; | |
| 509 | - | --tw-gradient-via-position: ; | |
| 510 | - | --tw-gradient-to-position: ; | |
| 511 | - | --tw-ordinal: ; | |
| 512 | - | --tw-slashed-zero: ; | |
| 513 | - | --tw-numeric-figure: ; | |
| 514 | - | --tw-numeric-spacing: ; | |
| 515 | - | --tw-numeric-fraction: ; | |
| 516 | - | --tw-ring-inset: ; | |
| 517 | - | --tw-ring-offset-width: 0px; | |
| 518 | - | --tw-ring-offset-color: #fff; | |
| 519 | - | --tw-ring-color: rgb(59 130 246 / 0.5); | |
| 520 | - | --tw-ring-offset-shadow: 0 0 #0000; | |
| 521 | - | --tw-ring-shadow: 0 0 #0000; | |
| 522 | - | --tw-shadow: 0 0 #0000; | |
| 523 | - | --tw-shadow-colored: 0 0 #0000; | |
| 524 | - | --tw-blur: ; | |
| 525 | - | --tw-brightness: ; | |
| 526 | - | --tw-contrast: ; | |
| 527 | - | --tw-grayscale: ; | |
| 528 | - | --tw-hue-rotate: ; | |
| 529 | - | --tw-invert: ; | |
| 530 | - | --tw-saturate: ; | |
| 531 | - | --tw-sepia: ; | |
| 532 | - | --tw-drop-shadow: ; | |
| 533 | - | --tw-backdrop-blur: ; | |
| 534 | - | --tw-backdrop-brightness: ; | |
| 535 | - | --tw-backdrop-contrast: ; | |
| 536 | - | --tw-backdrop-grayscale: ; | |
| 537 | - | --tw-backdrop-hue-rotate: ; | |
| 538 | - | --tw-backdrop-invert: ; | |
| 539 | - | --tw-backdrop-opacity: ; | |
| 540 | - | --tw-backdrop-saturate: ; | |
| 541 | - | --tw-backdrop-sepia: ; | |
| 542 | - | } | |
| 543 | - | ||
| 544 | - | .my-2 { | |
| 545 | - | margin-top: 0.5rem; | |
| 546 | - | margin-bottom: 0.5rem; | |
| 547 | - | } | |
| 548 | - | ||
| 549 | - | .mb-4 { | |
| 550 | - | margin-bottom: 1rem; | |
| 551 | - | } | |
| 552 | - | ||
| 553 | - | .ml-2 { | |
| 554 | - | margin-left: 0.5rem; | |
| 555 | - | } | |
| 556 | - | ||
| 557 | - | .ml-4 { | |
| 558 | - | margin-left: 1rem; | |
| 559 | - | } | |
| 560 | - | ||
| 561 | - | .mr-2 { | |
| 562 | - | margin-right: 0.5rem; | |
| 563 | - | } | |
| 564 | - | ||
| 565 | - | .mt-4 { | |
| 566 | - | margin-top: 1rem; | |
| 567 | - | } | |
| 568 | - | ||
| 569 | - | .flex { | |
| 570 | - | display: flex; | |
| 571 | - | } | |
| 572 | - | ||
| 573 | - | .h-10 { | |
| 574 | - | height: 2.5rem; | |
| 575 | - | } | |
| 576 | - | ||
| 577 | - | .h-24 { | |
| 578 | - | height: 6rem; | |
| 579 | - | } | |
| 580 | - | ||
| 581 | - | .h-32 { | |
| 582 | - | height: 8rem; | |
| 583 | - | } | |
| 584 | - | ||
| 585 | - | .h-44 { | |
| 586 | - | height: 11rem; | |
| 587 | - | } | |
| 588 | - | ||
| 589 | - | .h-64 { | |
| 590 | - | height: 16rem; | |
| 591 | - | } | |
| 592 | - | ||
| 593 | - | .h-full { | |
| 594 | - | height: 100%; | |
| 595 | - | } | |
| 596 | - | ||
| 597 | - | .w-1\/5 { | |
| 598 | - | width: 20%; | |
| 599 | - | } | |
| 600 | - | ||
| 601 | - | .w-2\/6 { | |
| 602 | - | width: 33.333333%; | |
| 603 | - | } | |
| 604 | - | ||
| 605 | - | .w-4\/6 { | |
| 606 | - | width: 66.666667%; | |
| 607 | - | } | |
| 608 | - | ||
| 609 | - | .w-72 { | |
| 610 | - | width: 18rem; | |
| 611 | - | } | |
| 612 | - | ||
| 613 | - | .w-full { | |
| 614 | - | width: 100%; | |
| 615 | - | } | |
| 616 | - | ||
| 617 | - | .w-2\/3 { | |
| 618 | - | width: 66.666667%; | |
| 619 | - | } | |
| 620 | - | ||
| 621 | - | .w-screen { | |
| 622 | - | width: 100vw; | |
| 623 | - | } | |
| 624 | - | ||
| 625 | - | .w-96 { | |
| 626 | - | width: 24rem; | |
| 627 | - | } | |
| 628 | - | ||
| 629 | - | .flex-1 { | |
| 630 | - | flex: 1 1 0%; | |
| 631 | - | } | |
| 632 | - | ||
| 633 | - | .flex-grow { | |
| 634 | - | flex-grow: 1; | |
| 635 | - | } | |
| 636 | - | ||
| 637 | - | .flex-row { | |
| 638 | - | flex-direction: row; | |
| 639 | - | } | |
| 640 | - | ||
| 641 | - | .flex-col { | |
| 642 | - | flex-direction: column; | |
| 643 | - | } | |
| 644 | - | ||
| 645 | - | .items-center { | |
| 646 | - | align-items: center; | |
| 647 | - | } | |
| 648 | - | ||
| 649 | - | .justify-end { | |
| 650 | - | justify-content: flex-end; | |
| 651 | - | } | |
| 652 | - | ||
| 653 | - | .justify-center { | |
| 654 | - | justify-content: center; | |
| 655 | - | } | |
| 656 | - | ||
| 657 | - | .justify-between { | |
| 658 | - | justify-content: space-between; | |
| 659 | - | } | |
| 660 | - | ||
| 661 | - | .justify-items-center { | |
| 662 | - | justify-items: center; | |
| 663 | - | } | |
| 664 | - | ||
| 665 | - | .space-x-4 > :not([hidden]) ~ :not([hidden]) { | |
| 666 | - | --tw-space-x-reverse: 0; | |
| 667 | - | margin-right: calc(1rem * var(--tw-space-x-reverse)); | |
| 668 | - | margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); | |
| 669 | - | } | |
| 670 | - | ||
| 671 | - | .space-y-2 > :not([hidden]) ~ :not([hidden]) { | |
| 672 | - | --tw-space-y-reverse: 0; | |
| 673 | - | margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); | |
| 674 | - | margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); | |
| 675 | - | } | |
| 676 | - | ||
| 677 | - | .space-y-4 > :not([hidden]) ~ :not([hidden]) { | |
| 678 | - | --tw-space-y-reverse: 0; | |
| 679 | - | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); | |
| 680 | - | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | |
| 681 | - | } | |
| 682 | - | ||
| 683 | - | .space-x-3 > :not([hidden]) ~ :not([hidden]) { | |
| 684 | - | --tw-space-x-reverse: 0; | |
| 685 | - | margin-right: calc(0.75rem * var(--tw-space-x-reverse)); | |
| 686 | - | margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))); | |
| 687 | - | } | |
| 688 | - | ||
| 689 | - | .space-y-3 > :not([hidden]) ~ :not([hidden]) { | |
| 690 | - | --tw-space-y-reverse: 0; | |
| 691 | - | margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); | |
| 692 | - | margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); | |
| 693 | - | } | |
| 694 | - | ||
| 695 | - | .overflow-y-scroll { | |
| 696 | - | overflow-y: scroll; | |
| 697 | - | } | |
| 698 | - | ||
| 699 | - | .rounded-lg { | |
| 700 | - | border-radius: 0.5rem; | |
| 701 | - | } | |
| 702 | - | ||
| 703 | - | .rounded-md { | |
| 704 | - | border-radius: 0.375rem; | |
| 705 | - | } | |
| 706 | - | ||
| 707 | - | .rounded-sm { | |
| 708 | - | border-radius: 0.125rem; | |
| 709 | - | } | |
| 710 | - | ||
| 711 | - | .border { | |
| 712 | - | border-width: 1px; | |
| 713 | - | } | |
| 714 | - | ||
| 715 | - | .border-gray-300 { | |
| 716 | - | --tw-border-opacity: 1; | |
| 717 | - | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | |
| 718 | - | } | |
| 719 | - | ||
| 720 | - | .bg-gordonmascot-lblue { | |
| 721 | - | --tw-bg-opacity: 1; | |
| 722 | - | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | |
| 723 | - | } | |
| 724 | - | ||
| 725 | - | .bg-gordonmascot-mblue { | |
| 726 | - | --tw-bg-opacity: 1; | |
| 727 | - | background-color: rgb(0 82 147 / var(--tw-bg-opacity)); | |
| 728 | - | } | |
| 729 | - | ||
| 730 | - | .bg-gray-300 { | |
| 731 | - | --tw-bg-opacity: 1; | |
| 732 | - | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | |
| 733 | - | } | |
| 734 | - | ||
| 735 | - | .bg-transparent { | |
| 736 | - | background-color: transparent; | |
| 737 | - | } | |
| 738 | - | ||
| 739 | - | .bg-white { | |
| 740 | - | --tw-bg-opacity: 1; | |
| 741 | - | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); | |
| 742 | - | } | |
| 743 | - | ||
| 744 | - | .bg-zinc-200 { | |
| 745 | - | --tw-bg-opacity: 1; | |
| 746 | - | background-color: rgb(228 228 231 / var(--tw-bg-opacity)); | |
| 747 | - | } | |
| 748 | - | ||
| 749 | - | .bg-gordonmascot-hblue { | |
| 750 | - | --tw-bg-opacity: 1; | |
| 751 | - | background-color: rgb(1 30 92 / var(--tw-bg-opacity)); | |
| 752 | - | } | |
| 753 | - | ||
| 754 | - | .bg-zinc-400 { | |
| 755 | - | --tw-bg-opacity: 1; | |
| 756 | - | background-color: rgb(161 161 170 / var(--tw-bg-opacity)); | |
| 757 | - | } | |
| 758 | - | ||
| 759 | - | .p-4 { | |
| 760 | - | padding: 1rem; | |
| 761 | - | } | |
| 762 | - | ||
| 763 | - | .p-2 { | |
| 764 | - | padding: 0.5rem; | |
| 765 | - | } | |
| 766 | - | ||
| 767 | - | .px-2 { | |
| 768 | - | padding-left: 0.5rem; | |
| 769 | - | padding-right: 0.5rem; | |
| 770 | - | } | |
| 771 | - | ||
| 772 | - | .px-6 { | |
| 773 | - | padding-left: 1.5rem; | |
| 774 | - | padding-right: 1.5rem; | |
| 775 | - | } | |
| 776 | - | ||
| 777 | - | .py-2 { | |
| 778 | - | padding-top: 0.5rem; | |
| 779 | - | padding-bottom: 0.5rem; | |
| 780 | - | } | |
| 781 | - | ||
| 782 | - | .px-4 { | |
| 783 | - | padding-left: 1rem; | |
| 784 | - | padding-right: 1rem; | |
| 785 | - | } | |
| 786 | - | ||
| 787 | - | .text-2xl { | |
| 788 | - | font-size: 1.5rem; | |
| 789 | - | line-height: 2rem; | |
| 790 | - | } | |
| 791 | - | ||
| 792 | - | .text-5xl { | |
| 793 | - | font-size: 3rem; | |
| 794 | - | line-height: 1; | |
| 795 | - | } | |
| 796 | - | ||
| 797 | - | .text-lg { | |
| 798 | - | font-size: 1.125rem; | |
| 799 | - | line-height: 1.75rem; | |
| 800 | - | } | |
| 801 | - | ||
| 802 | - | .text-sm { | |
| 803 | - | font-size: 0.875rem; | |
| 804 | - | line-height: 1.25rem; | |
| 805 | - | } | |
| 806 | - | ||
| 807 | - | .text-xl { | |
| 808 | - | font-size: 1.25rem; | |
| 809 | - | line-height: 1.75rem; | |
| 810 | - | } | |
| 811 | - | ||
| 812 | - | .font-semibold { | |
| 813 | - | font-weight: 600; | |
| 814 | - | } | |
| 815 | - | ||
| 816 | - | .tracking-tighter { | |
| 817 | - | letter-spacing: -0.05em; | |
| 818 | - | } | |
| 819 | - | ||
| 820 | - | .text-gordonmascot-hblue { | |
| 821 | - | --tw-text-opacity: 1; | |
| 822 | - | color: rgb(1 30 92 / var(--tw-text-opacity)); | |
| 823 | - | } | |
| 824 | - | ||
| 825 | - | .text-gordonmascot-mblue { | |
| 826 | - | --tw-text-opacity: 1; | |
| 827 | - | color: rgb(0 82 147 / var(--tw-text-opacity)); | |
| 828 | - | } | |
| 829 | - | ||
| 830 | - | .text-gray-100 { | |
| 831 | - | --tw-text-opacity: 1; | |
| 832 | - | color: rgb(243 244 246 / var(--tw-text-opacity)); | |
| 833 | - | } | |
| 834 | - | ||
| 835 | - | .text-gray-800 { | |
| 836 | - | --tw-text-opacity: 1; | |
| 837 | - | color: rgb(31 41 55 / var(--tw-text-opacity)); | |
| 838 | - | } | |
| 839 | - | ||
| 840 | - | .text-slate-400 { | |
| 841 | - | --tw-text-opacity: 1; | |
| 842 | - | color: rgb(148 163 184 / var(--tw-text-opacity)); | |
| 843 | - | } | |
| 844 | - | ||
| 845 | - | .text-slate-100 { | |
| 846 | - | --tw-text-opacity: 1; | |
| 847 | - | color: rgb(241 245 249 / var(--tw-text-opacity)); | |
| 848 | - | } | |
| 849 | - | ||
| 850 | - | .shadow { | |
| 851 | - | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); | |
| 852 | - | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); | |
| 853 | - | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); | |
| 854 | - | } | |
| 855 | - | ||
| 856 | - | .outline-none { | |
| 857 | - | outline: 2px solid transparent; | |
| 858 | - | outline-offset: 2px; | |
| 859 | - | } | |
| 860 | - | ||
| 861 | - | /* Import IBM Plex Fonts */ | |
| 862 | - | ||
| 863 | - | @font-face { | |
| 864 | - | font-family: "IBMPlexSans-Bold"; | |
| 865 | - | ||
| 866 | - | src: url("/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2") format("woff2"); | |
| 867 | - | ||
| 868 | - | font-weight: bold; | |
| 869 | - | ||
| 870 | - | font-style: normal; | |
| 871 | - | } | |
| 872 | - | ||
| 873 | - | @font-face { | |
| 874 | - | font-family: "IBMPlexSans-Regular"; | |
| 875 | - | ||
| 876 | - | src: url("/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2") format("woff2"); | |
| 877 | - | ||
| 878 | - | font-weight: normal; | |
| 879 | - | ||
| 880 | - | font-style: normal; | |
| 881 | - | } | |
| 882 | - | ||
| 883 | - | /* Import Jetbrains Mono Nerd Font */ | |
| 884 | - | ||
| 885 | - | @font-face { | |
| 886 | - | font-family: "JetBrainsMono-Nerd-Font-Regular"; | |
| 887 | - | ||
| 888 | - | src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFontMono-Regular.woff2") | |
| 889 | - | format("woff2"); | |
| 890 | - | ||
| 891 | - | font-weight: normal; | |
| 892 | - | ||
| 893 | - | font-style: normal; | |
| 894 | - | } | |
| 895 | - | ||
| 896 | - | @font-face { | |
| 897 | - | font-family: "LilitaOne-Regular"; | |
| 898 | - | ||
| 899 | - | src: url("/assets/fonts/LilitaOne/LilitaOne-Regular.ttf") format("truetype"); | |
| 900 | - | ||
| 901 | - | font-weight: normal; | |
| 902 | - | ||
| 903 | - | font-style: normal; | |
| 904 | - | } | |
| 905 | - | ||
| 906 | - | .fade-me-out.htmx-swapping { | |
| 907 | - | opacity: 0; | |
| 908 | - | transition: opacity 1s ease-out; | |
| 909 | - | } | |
| 910 | - | ||
| 911 | - | /* View transition */ | |
| 912 | - | ||
| 913 | - | @keyframes fade-in { | |
| 914 | - | from { | |
| 915 | - | opacity: 0; | |
| 916 | - | } | |
| 917 | - | } | |
| 918 | - | ||
| 919 | - | @keyframes fade-out { | |
| 920 | - | to { | |
| 921 | - | opacity: 0; | |
| 922 | - | } | |
| 923 | - | } | |
| 924 | - | ||
| 925 | - | @keyframes slide-from-right { | |
| 926 | - | from { | |
| 927 | - | transform: translateX(90px); | |
| 928 | - | } | |
| 929 | - | } | |
| 930 | - | ||
| 931 | - | @keyframes slide-to-left { | |
| 932 | - | to { | |
| 933 | - | transform: translateX(-90px); | |
| 934 | - | } | |
| 935 | - | } | |
| 936 | - | ||
| 937 | - | .slide-it { | |
| 938 | - | view-transition-name: slide-it; | |
| 939 | - | } | |
| 940 | - | ||
| 941 | - | ::view-transition-old(slide-it) { | |
| 942 | - | animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out, | |
| 943 | - | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; | |
| 944 | - | } | |
| 945 | - | ||
| 946 | - | ::view-transition-new(slide-it) { | |
| 947 | - | animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, | |
| 948 | - | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; | |
| 949 | - | } | |
| 950 | - | ||
| 951 | - | .gordon-title { | |
| 952 | - | font-family: "LilitaOne-Regular", sans-serif; | |
| 953 | - | } | |
| 954 | - | ||
| 955 | - | .IBMPlexSans { | |
| 956 | - | font-family: "IBMPlexSans-Regular", sans-serif; | |
| 957 | - | } | |
| 958 | - | ||
| 959 | - | .circle-shadow { | |
| 960 | - | width: 100%; | |
| 961 | - | position: relative; | |
| 962 | - | } | |
| 963 | - | ||
| 964 | - | .circle-shadow::after { | |
| 965 | - | position: absolute; | |
| 966 | - | width: 50%; | |
| 967 | - | height: 0.4rem; | |
| 968 | - | margin: auto; | |
| 969 | - | background: rgba(0, 0, 0, 0.1); | |
| 970 | - | border-radius: 50%; | |
| 971 | - | z-index: -1; | |
| 972 | - | bottom: 2%; | |
| 973 | - | content: ""; | |
| 974 | - | box-shadow: 0 0 1px 0.3rem rgba(0, 0, 0, 0.1); | |
| 975 | - | } | |
| 976 | - | ||
| 977 | - | .iconoir { | |
| 978 | - | margin-right: 8px; | |
| 979 | - | } | |
| 980 | - | ||
| 981 | - | @media (min-width: 640px) { | |
| 982 | - | .sm\:h-24 { | |
| 983 | - | height: 6rem; | |
| 984 | - | } | |
| 985 | - | } | |
| 986 | - | ||
| 987 | - | @media (min-width: 768px) { | |
| 988 | - | .md\:w-1\/2 { | |
| 989 | - | width: 50%; | |
| 990 | - | } | |
| 991 | - | ||
| 992 | - | .md\:flex-row { | |
| 993 | - | flex-direction: row; | |
| 994 | - | } | |
| 995 | - | ||
| 996 | - | .md\:space-y-6 > :not([hidden]) ~ :not([hidden]) { | |
| 997 | - | --tw-space-y-reverse: 0; | |
| 998 | - | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); | |
| 999 | - | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); | |
| 1000 | - | } | |
| 1001 | - | ||
| 1002 | - | .md\:p-8 { | |
| 1003 | - | padding: 2rem; | |
| 1004 | - | } | |
| 1005 | - | ||
| 1006 | - | .md\:py-8 { | |
| 1007 | - | padding-top: 2rem; | |
| 1008 | - | padding-bottom: 2rem; | |
| 1009 | - | } | |
| 1010 | - | ||
| 1011 | - | .md\:pl-4 { | |
| 1012 | - | padding-left: 1rem; | |
| 1013 | - | } | |
| 1014 | - | ||
| 1015 | - | .md\:pt-10 { | |
| 1016 | - | padding-top: 2.5rem; | |
| 1017 | - | } | |
| 1018 | - | ||
| 1019 | - | .md\:text-2xl { | |
| 1020 | - | font-size: 1.5rem; | |
| 1021 | - | line-height: 2rem; | |
| 1022 | - | } | |
| 1023 | - | ||
| 1024 | - | .md\:text-3xl { | |
| 1025 | - | font-size: 1.875rem; | |
| 1026 | - | line-height: 2.25rem; | |
| 1027 | - | } | |
| 1028 | - | } | |
| 1029 | - | ||
| 1030 | - | @media (min-width: 1024px) { | |
| 1031 | - | .lg\:text-4xl { | |
| 1032 | - | font-size: 2.25rem; | |
| 1033 | - | line-height: 2.5rem; | |
| 1034 | - | } | |
| 1035 | - | } | |
| 1036 | - | ||
| 1037 | - | @media (min-width: 1280px) { | |
| 1038 | - | .xl\:h-48 { | |
| 1039 | - | height: 12rem; | |
| 1040 | - | } | |
| 1041 | - | ||
| 1042 | - | .xl\:text-5xl { | |
| 1043 | - | font-size: 3rem; | |
| 1044 | - | line-height: 1; | |
| 1045 | - | } | |
| 1046 | - | } | |
| @@ -1,1046 +0,0 @@ | |||||
| 1 | - | /** | |||
| 2 | - | * Path: internal/ui/public/assets/css/custom.css | |||
| 3 | - | * | |||
| 4 | - | * @format | |||
| 5 | - | */ | |||
| 6 | - | ||||
| 7 | - | /* Embeded path : assets/css/cutsom.css */ | |||
| 8 | - | ||||
| 9 | - | /*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com | |||
| 10 | - | */ | |||
| 11 | - | ||||
| 12 | - | /* | |||
| 13 | - | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) | |||
| 14 | - | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) | |||
| 15 | - | */ | |||
| 16 | - | ||||
| 17 | - | *, | |||
| 18 | - | ::before, | |||
| 19 | - | ::after { | |||
| 20 | - | box-sizing: border-box; | |||
| 21 | - | /* 1 */ | |||
| 22 | - | border-width: 0; | |||
| 23 | - | /* 2 */ | |||
| 24 | - | border-style: solid; | |||
| 25 | - | /* 2 */ | |||
| 26 | - | border-color: #e5e7eb; | |||
| 27 | - | /* 2 */ | |||
| 28 | - | } | |||
| 29 | - | ||||
| 30 | - | ::before, | |||
| 31 | - | ::after { | |||
| 32 | - | --tw-content: ''; | |||
| 33 | - | } | |||
| 34 | - | ||||
| 35 | - | /* | |||
| 36 | - | 1. Use a consistent sensible line-height in all browsers. | |||
| 37 | - | 2. Prevent adjustments of font size after orientation changes in iOS. | |||
| 38 | - | 3. Use a more readable tab size. | |||
| 39 | - | 4. Use the user's configured `sans` font-family by default. | |||
| 40 | - | 5. Use the user's configured `sans` font-feature-settings by default. | |||
| 41 | - | 6. Use the user's configured `sans` font-variation-settings by default. | |||
| 42 | - | */ | |||
| 43 | - | ||||
| 44 | - | html { | |||
| 45 | - | line-height: 1.5; | |||
| 46 | - | /* 1 */ | |||
| 47 | - | -webkit-text-size-adjust: 100%; | |||
| 48 | - | /* 2 */ | |||
| 49 | - | -moz-tab-size: 4; | |||
| 50 | - | /* 3 */ | |||
| 51 | - | -o-tab-size: 4; | |||
| 52 | - | tab-size: 4; | |||
| 53 | - | /* 3 */ | |||
| 54 | - | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; | |||
| 55 | - | /* 4 */ | |||
| 56 | - | font-feature-settings: normal; | |||
| 57 | - | /* 5 */ | |||
| 58 | - | font-variation-settings: normal; | |||
| 59 | - | /* 6 */ | |||
| 60 | - | } | |||
| 61 | - | ||||
| 62 | - | /* | |||
| 63 | - | 1. Remove the margin in all browsers. | |||
| 64 | - | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. | |||
| 65 | - | */ | |||
| 66 | - | ||||
| 67 | - | body { | |||
| 68 | - | margin: 0; | |||
| 69 | - | /* 1 */ | |||
| 70 | - | line-height: inherit; | |||
| 71 | - | /* 2 */ | |||
| 72 | - | } | |||
| 73 | - | ||||
| 74 | - | /* | |||
| 75 | - | 1. Add the correct height in Firefox. | |||
| 76 | - | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) | |||
| 77 | - | 3. Ensure horizontal rules are visible by default. | |||
| 78 | - | */ | |||
| 79 | - | ||||
| 80 | - | hr { | |||
| 81 | - | height: 0; | |||
| 82 | - | /* 1 */ | |||
| 83 | - | color: inherit; | |||
| 84 | - | /* 2 */ | |||
| 85 | - | border-top-width: 1px; | |||
| 86 | - | /* 3 */ | |||
| 87 | - | } | |||
| 88 | - | ||||
| 89 | - | /* | |||
| 90 | - | Add the correct text decoration in Chrome, Edge, and Safari. | |||
| 91 | - | */ | |||
| 92 | - | ||||
| 93 | - | abbr:where([title]) { | |||
| 94 | - | -webkit-text-decoration: underline dotted; | |||
| 95 | - | text-decoration: underline dotted; | |||
| 96 | - | } | |||
| 97 | - | ||||
| 98 | - | /* | |||
| 99 | - | Remove the default font size and weight for headings. | |||
| 100 | - | */ | |||
| 101 | - | ||||
| 102 | - | h1, | |||
| 103 | - | h2, | |||
| 104 | - | h3, | |||
| 105 | - | h4, | |||
| 106 | - | h5, | |||
| 107 | - | h6 { | |||
| 108 | - | font-size: inherit; | |||
| 109 | - | font-weight: inherit; | |||
| 110 | - | } | |||
| 111 | - | ||||
| 112 | - | /* | |||
| 113 | - | Reset links to optimize for opt-in styling instead of opt-out. | |||
| 114 | - | */ | |||
| 115 | - | ||||
| 116 | - | a { | |||
| 117 | - | color: inherit; | |||
| 118 | - | text-decoration: inherit; | |||
| 119 | - | } | |||
| 120 | - | ||||
| 121 | - | /* | |||
| 122 | - | Add the correct font weight in Edge and Safari. | |||
| 123 | - | */ | |||
| 124 | - | ||||
| 125 | - | b, | |||
| 126 | - | strong { | |||
| 127 | - | font-weight: bolder; | |||
| 128 | - | } | |||
| 129 | - | ||||
| 130 | - | /* | |||
| 131 | - | 1. Use the user's configured `mono` font family by default. | |||
| 132 | - | 2. Correct the odd `em` font sizing in all browsers. | |||
| 133 | - | */ | |||
| 134 | - | ||||
| 135 | - | code, | |||
| 136 | - | kbd, | |||
| 137 | - | samp, | |||
| 138 | - | pre { | |||
| 139 | - | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; | |||
| 140 | - | /* 1 */ | |||
| 141 | - | font-size: 1em; | |||
| 142 | - | /* 2 */ | |||
| 143 | - | } | |||
| 144 | - | ||||
| 145 | - | /* | |||
| 146 | - | Add the correct font size in all browsers. | |||
| 147 | - | */ | |||
| 148 | - | ||||
| 149 | - | small { | |||
| 150 | - | font-size: 80%; | |||
| 151 | - | } | |||
| 152 | - | ||||
| 153 | - | /* | |||
| 154 | - | Prevent `sub` and `sup` elements from affecting the line height in all browsers. | |||
| 155 | - | */ | |||
| 156 | - | ||||
| 157 | - | sub, | |||
| 158 | - | sup { | |||
| 159 | - | font-size: 75%; | |||
| 160 | - | line-height: 0; | |||
| 161 | - | position: relative; | |||
| 162 | - | vertical-align: baseline; | |||
| 163 | - | } | |||
| 164 | - | ||||
| 165 | - | sub { | |||
| 166 | - | bottom: -0.25em; | |||
| 167 | - | } | |||
| 168 | - | ||||
| 169 | - | sup { | |||
| 170 | - | top: -0.5em; | |||
| 171 | - | } | |||
| 172 | - | ||||
| 173 | - | /* | |||
| 174 | - | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) | |||
| 175 | - | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) | |||
| 176 | - | 3. Remove gaps between table borders by default. | |||
| 177 | - | */ | |||
| 178 | - | ||||
| 179 | - | table { | |||
| 180 | - | text-indent: 0; | |||
| 181 | - | /* 1 */ | |||
| 182 | - | border-color: inherit; | |||
| 183 | - | /* 2 */ | |||
| 184 | - | border-collapse: collapse; | |||
| 185 | - | /* 3 */ | |||
| 186 | - | } | |||
| 187 | - | ||||
| 188 | - | /* | |||
| 189 | - | 1. Change the font styles in all browsers. | |||
| 190 | - | 2. Remove the margin in Firefox and Safari. | |||
| 191 | - | 3. Remove default padding in all browsers. | |||
| 192 | - | */ | |||
| 193 | - | ||||
| 194 | - | button, | |||
| 195 | - | input, | |||
| 196 | - | optgroup, | |||
| 197 | - | select, | |||
| 198 | - | textarea { | |||
| 199 | - | font-family: inherit; | |||
| 200 | - | /* 1 */ | |||
| 201 | - | font-feature-settings: inherit; | |||
| 202 | - | /* 1 */ | |||
| 203 | - | font-variation-settings: inherit; | |||
| 204 | - | /* 1 */ | |||
| 205 | - | font-size: 100%; | |||
| 206 | - | /* 1 */ | |||
| 207 | - | font-weight: inherit; | |||
| 208 | - | /* 1 */ | |||
| 209 | - | line-height: inherit; | |||
| 210 | - | /* 1 */ | |||
| 211 | - | color: inherit; | |||
| 212 | - | /* 1 */ | |||
| 213 | - | margin: 0; | |||
| 214 | - | /* 2 */ | |||
| 215 | - | padding: 0; | |||
| 216 | - | /* 3 */ | |||
| 217 | - | } | |||
| 218 | - | ||||
| 219 | - | /* | |||
| 220 | - | Remove the inheritance of text transform in Edge and Firefox. | |||
| 221 | - | */ | |||
| 222 | - | ||||
| 223 | - | button, | |||
| 224 | - | select { | |||
| 225 | - | text-transform: none; | |||
| 226 | - | } | |||
| 227 | - | ||||
| 228 | - | /* | |||
| 229 | - | 1. Correct the inability to style clickable types in iOS and Safari. | |||
| 230 | - | 2. Remove default button styles. | |||
| 231 | - | */ | |||
| 232 | - | ||||
| 233 | - | button, | |||
| 234 | - | [type='button'], | |||
| 235 | - | [type='reset'], | |||
| 236 | - | [type='submit'] { | |||
| 237 | - | -webkit-appearance: button; | |||
| 238 | - | /* 1 */ | |||
| 239 | - | background-color: transparent; | |||
| 240 | - | /* 2 */ | |||
| 241 | - | background-image: none; | |||
| 242 | - | /* 2 */ | |||
| 243 | - | } | |||
| 244 | - | ||||
| 245 | - | /* | |||
| 246 | - | Use the modern Firefox focus style for all focusable elements. | |||
| 247 | - | */ | |||
| 248 | - | ||||
| 249 | - | :-moz-focusring { | |||
| 250 | - | outline: auto; | |||
| 251 | - | } | |||
| 252 | - | ||||
| 253 | - | /* | |||
| 254 | - | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) | |||
| 255 | - | */ | |||
| 256 | - | ||||
| 257 | - | :-moz-ui-invalid { | |||
| 258 | - | box-shadow: none; | |||
| 259 | - | } | |||
| 260 | - | ||||
| 261 | - | /* | |||
| 262 | - | Add the correct vertical alignment in Chrome and Firefox. | |||
| 263 | - | */ | |||
| 264 | - | ||||
| 265 | - | progress { | |||
| 266 | - | vertical-align: baseline; | |||
| 267 | - | } | |||
| 268 | - | ||||
| 269 | - | /* | |||
| 270 | - | Correct the cursor style of increment and decrement buttons in Safari. | |||
| 271 | - | */ | |||
| 272 | - | ||||
| 273 | - | ::-webkit-inner-spin-button, | |||
| 274 | - | ::-webkit-outer-spin-button { | |||
| 275 | - | height: auto; | |||
| 276 | - | } | |||
| 277 | - | ||||
| 278 | - | /* | |||
| 279 | - | 1. Correct the odd appearance in Chrome and Safari. | |||
| 280 | - | 2. Correct the outline style in Safari. | |||
| 281 | - | */ | |||
| 282 | - | ||||
| 283 | - | [type='search'] { | |||
| 284 | - | -webkit-appearance: textfield; | |||
| 285 | - | /* 1 */ | |||
| 286 | - | outline-offset: -2px; | |||
| 287 | - | /* 2 */ | |||
| 288 | - | } | |||
| 289 | - | ||||
| 290 | - | /* | |||
| 291 | - | Remove the inner padding in Chrome and Safari on macOS. | |||
| 292 | - | */ | |||
| 293 | - | ||||
| 294 | - | ::-webkit-search-decoration { | |||
| 295 | - | -webkit-appearance: none; | |||
| 296 | - | } | |||
| 297 | - | ||||
| 298 | - | /* | |||
| 299 | - | 1. Correct the inability to style clickable types in iOS and Safari. | |||
| 300 | - | 2. Change font properties to `inherit` in Safari. | |||
| 301 | - | */ | |||
| 302 | - | ||||
| 303 | - | ::-webkit-file-upload-button { | |||
| 304 | - | -webkit-appearance: button; | |||
| 305 | - | /* 1 */ | |||
| 306 | - | font: inherit; | |||
| 307 | - | /* 2 */ | |||
| 308 | - | } | |||
| 309 | - | ||||
| 310 | - | /* | |||
| 311 | - | Add the correct display in Chrome and Safari. | |||
| 312 | - | */ | |||
| 313 | - | ||||
| 314 | - | summary { | |||
| 315 | - | display: list-item; | |||
| 316 | - | } | |||
| 317 | - | ||||
| 318 | - | /* | |||
| 319 | - | Removes the default spacing and border for appropriate elements. | |||
| 320 | - | */ | |||
| 321 | - | ||||
| 322 | - | blockquote, | |||
| 323 | - | dl, | |||
| 324 | - | dd, | |||
| 325 | - | h1, | |||
| 326 | - | h2, | |||
| 327 | - | h3, | |||
| 328 | - | h4, | |||
| 329 | - | h5, | |||
| 330 | - | h6, | |||
| 331 | - | hr, | |||
| 332 | - | figure, | |||
| 333 | - | p, | |||
| 334 | - | pre { | |||
| 335 | - | margin: 0; | |||
| 336 | - | } | |||
| 337 | - | ||||
| 338 | - | fieldset { | |||
| 339 | - | margin: 0; | |||
| 340 | - | padding: 0; | |||
| 341 | - | } | |||
| 342 | - | ||||
| 343 | - | legend { | |||
| 344 | - | padding: 0; | |||
| 345 | - | } | |||
| 346 | - | ||||
| 347 | - | ol, | |||
| 348 | - | ul, | |||
| 349 | - | menu { | |||
| 350 | - | list-style: none; | |||
| 351 | - | margin: 0; | |||
| 352 | - | padding: 0; | |||
| 353 | - | } | |||
| 354 | - | ||||
| 355 | - | /* | |||
| 356 | - | Reset default styling for dialogs. | |||
| 357 | - | */ | |||
| 358 | - | ||||
| 359 | - | dialog { | |||
| 360 | - | padding: 0; | |||
| 361 | - | } | |||
| 362 | - | ||||
| 363 | - | /* | |||
| 364 | - | Prevent resizing textareas horizontally by default. | |||
| 365 | - | */ | |||
| 366 | - | ||||
| 367 | - | textarea { | |||
| 368 | - | resize: vertical; | |||
| 369 | - | } | |||
| 370 | - | ||||
| 371 | - | /* | |||
| 372 | - | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) | |||
| 373 | - | 2. Set the default placeholder color to the user's configured gray 400 color. | |||
| 374 | - | */ | |||
| 375 | - | ||||
| 376 | - | input::-moz-placeholder, textarea::-moz-placeholder { | |||
| 377 | - | opacity: 1; | |||
| 378 | - | /* 1 */ | |||
| 379 | - | color: #9ca3af; | |||
| 380 | - | /* 2 */ | |||
| 381 | - | } | |||
| 382 | - | ||||
| 383 | - | input::placeholder, | |||
| 384 | - | textarea::placeholder { | |||
| 385 | - | opacity: 1; | |||
| 386 | - | /* 1 */ | |||
| 387 | - | color: #9ca3af; | |||
| 388 | - | /* 2 */ | |||
| 389 | - | } | |||
| 390 | - | ||||
| 391 | - | /* | |||
| 392 | - | Set the default cursor for buttons. | |||
| 393 | - | */ | |||
| 394 | - | ||||
| 395 | - | button, | |||
| 396 | - | [role="button"] { | |||
| 397 | - | cursor: pointer; | |||
| 398 | - | } | |||
| 399 | - | ||||
| 400 | - | /* | |||
| 401 | - | Make sure disabled buttons don't get the pointer cursor. | |||
| 402 | - | */ | |||
| 403 | - | ||||
| 404 | - | :disabled { | |||
| 405 | - | cursor: default; | |||
| 406 | - | } | |||
| 407 | - | ||||
| 408 | - | /* | |||
| 409 | - | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) | |||
| 410 | - | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) | |||
| 411 | - | This can trigger a poorly considered lint error in some tools but is included by design. | |||
| 412 | - | */ | |||
| 413 | - | ||||
| 414 | - | img, | |||
| 415 | - | svg, | |||
| 416 | - | video, | |||
| 417 | - | canvas, | |||
| 418 | - | audio, | |||
| 419 | - | iframe, | |||
| 420 | - | embed, | |||
| 421 | - | object { | |||
| 422 | - | display: block; | |||
| 423 | - | /* 1 */ | |||
| 424 | - | vertical-align: middle; | |||
| 425 | - | /* 2 */ | |||
| 426 | - | } | |||
| 427 | - | ||||
| 428 | - | /* | |||
| 429 | - | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) | |||
| 430 | - | */ | |||
| 431 | - | ||||
| 432 | - | img, | |||
| 433 | - | video { | |||
| 434 | - | max-width: 100%; | |||
| 435 | - | height: auto; | |||
| 436 | - | } | |||
| 437 | - | ||||
| 438 | - | /* Make elements with the HTML hidden attribute stay hidden by default */ | |||
| 439 | - | ||||
| 440 | - | [hidden] { | |||
| 441 | - | display: none; | |||
| 442 | - | } | |||
| 443 | - | ||||
| 444 | - | *, ::before, ::after { | |||
| 445 | - | --tw-border-spacing-x: 0; | |||
| 446 | - | --tw-border-spacing-y: 0; | |||
| 447 | - | --tw-translate-x: 0; | |||
| 448 | - | --tw-translate-y: 0; | |||
| 449 | - | --tw-rotate: 0; | |||
| 450 | - | --tw-skew-x: 0; | |||
| 451 | - | --tw-skew-y: 0; | |||
| 452 | - | --tw-scale-x: 1; | |||
| 453 | - | --tw-scale-y: 1; | |||
| 454 | - | --tw-pan-x: ; | |||
| 455 | - | --tw-pan-y: ; | |||
| 456 | - | --tw-pinch-zoom: ; | |||
| 457 | - | --tw-scroll-snap-strictness: proximity; | |||
| 458 | - | --tw-gradient-from-position: ; | |||
| 459 | - | --tw-gradient-via-position: ; | |||
| 460 | - | --tw-gradient-to-position: ; | |||
| 461 | - | --tw-ordinal: ; | |||
| 462 | - | --tw-slashed-zero: ; | |||
| 463 | - | --tw-numeric-figure: ; | |||
| 464 | - | --tw-numeric-spacing: ; | |||
| 465 | - | --tw-numeric-fraction: ; | |||
| 466 | - | --tw-ring-inset: ; | |||
| 467 | - | --tw-ring-offset-width: 0px; | |||
| 468 | - | --tw-ring-offset-color: #fff; | |||
| 469 | - | --tw-ring-color: rgb(59 130 246 / 0.5); | |||
| 470 | - | --tw-ring-offset-shadow: 0 0 #0000; | |||
| 471 | - | --tw-ring-shadow: 0 0 #0000; | |||
| 472 | - | --tw-shadow: 0 0 #0000; | |||
| 473 | - | --tw-shadow-colored: 0 0 #0000; | |||
| 474 | - | --tw-blur: ; | |||
| 475 | - | --tw-brightness: ; | |||
| 476 | - | --tw-contrast: ; | |||
| 477 | - | --tw-grayscale: ; | |||
| 478 | - | --tw-hue-rotate: ; | |||
| 479 | - | --tw-invert: ; | |||
| 480 | - | --tw-saturate: ; | |||
| 481 | - | --tw-sepia: ; | |||
| 482 | - | --tw-drop-shadow: ; | |||
| 483 | - | --tw-backdrop-blur: ; | |||
| 484 | - | --tw-backdrop-brightness: ; | |||
| 485 | - | --tw-backdrop-contrast: ; | |||
| 486 | - | --tw-backdrop-grayscale: ; | |||
| 487 | - | --tw-backdrop-hue-rotate: ; | |||
| 488 | - | --tw-backdrop-invert: ; | |||
| 489 | - | --tw-backdrop-opacity: ; | |||
| 490 | - | --tw-backdrop-saturate: ; | |||
| 491 | - | --tw-backdrop-sepia: ; | |||
| 492 | - | } | |||
| 493 | - | ||||
| 494 | - | ::backdrop { | |||
| 495 | - | --tw-border-spacing-x: 0; | |||
| 496 | - | --tw-border-spacing-y: 0; | |||
| 497 | - | --tw-translate-x: 0; | |||
| 498 | - | --tw-translate-y: 0; | |||
| 499 | - | --tw-rotate: 0; | |||
| 500 | - | --tw-skew-x: 0; | |||
| 501 | - | --tw-skew-y: 0; | |||
| 502 | - | --tw-scale-x: 1; | |||
| 503 | - | --tw-scale-y: 1; | |||
| 504 | - | --tw-pan-x: ; | |||
| 505 | - | --tw-pan-y: ; | |||
| 506 | - | --tw-pinch-zoom: ; | |||
| 507 | - | --tw-scroll-snap-strictness: proximity; | |||
| 508 | - | --tw-gradient-from-position: ; | |||
| 509 | - | --tw-gradient-via-position: ; | |||
| 510 | - | --tw-gradient-to-position: ; | |||
| 511 | - | --tw-ordinal: ; | |||
| 512 | - | --tw-slashed-zero: ; | |||
| 513 | - | --tw-numeric-figure: ; | |||
| 514 | - | --tw-numeric-spacing: ; | |||
| 515 | - | --tw-numeric-fraction: ; | |||
| 516 | - | --tw-ring-inset: ; | |||
| 517 | - | --tw-ring-offset-width: 0px; | |||
| 518 | - | --tw-ring-offset-color: #fff; | |||
| 519 | - | --tw-ring-color: rgb(59 130 246 / 0.5); | |||
| 520 | - | --tw-ring-offset-shadow: 0 0 #0000; | |||
| 521 | - | --tw-ring-shadow: 0 0 #0000; | |||
| 522 | - | --tw-shadow: 0 0 #0000; | |||
| 523 | - | --tw-shadow-colored: 0 0 #0000; | |||
| 524 | - | --tw-blur: ; | |||
| 525 | - | --tw-brightness: ; | |||
| 526 | - | --tw-contrast: ; | |||
| 527 | - | --tw-grayscale: ; | |||
| 528 | - | --tw-hue-rotate: ; | |||
| 529 | - | --tw-invert: ; | |||
| 530 | - | --tw-saturate: ; | |||
| 531 | - | --tw-sepia: ; | |||
| 532 | - | --tw-drop-shadow: ; | |||
| 533 | - | --tw-backdrop-blur: ; | |||
| 534 | - | --tw-backdrop-brightness: ; | |||
| 535 | - | --tw-backdrop-contrast: ; | |||
| 536 | - | --tw-backdrop-grayscale: ; | |||
| 537 | - | --tw-backdrop-hue-rotate: ; | |||
| 538 | - | --tw-backdrop-invert: ; | |||
| 539 | - | --tw-backdrop-opacity: ; | |||
| 540 | - | --tw-backdrop-saturate: ; | |||
| 541 | - | --tw-backdrop-sepia: ; | |||
| 542 | - | } | |||
| 543 | - | ||||
| 544 | - | .my-2 { | |||
| 545 | - | margin-top: 0.5rem; | |||
| 546 | - | margin-bottom: 0.5rem; | |||
| 547 | - | } | |||
| 548 | - | ||||
| 549 | - | .mb-4 { | |||
| 550 | - | margin-bottom: 1rem; | |||
| 551 | - | } | |||
| 552 | - | ||||
| 553 | - | .ml-2 { | |||
| 554 | - | margin-left: 0.5rem; | |||
| 555 | - | } | |||
| 556 | - | ||||
| 557 | - | .ml-4 { | |||
| 558 | - | margin-left: 1rem; | |||
| 559 | - | } | |||
| 560 | - | ||||
| 561 | - | .mr-2 { | |||
| 562 | - | margin-right: 0.5rem; | |||
| 563 | - | } | |||
| 564 | - | ||||
| 565 | - | .mt-4 { | |||
| 566 | - | margin-top: 1rem; | |||
| 567 | - | } | |||
| 568 | - | ||||
| 569 | - | .flex { | |||
| 570 | - | display: flex; | |||
| 571 | - | } | |||
| 572 | - | ||||
| 573 | - | .h-10 { | |||
| 574 | - | height: 2.5rem; | |||
| 575 | - | } | |||
| 576 | - | ||||
| 577 | - | .h-24 { | |||
| 578 | - | height: 6rem; | |||
| 579 | - | } | |||
| 580 | - | ||||
| 581 | - | .h-32 { | |||
| 582 | - | height: 8rem; | |||
| 583 | - | } | |||
| 584 | - | ||||
| 585 | - | .h-44 { | |||
| 586 | - | height: 11rem; | |||
| 587 | - | } | |||
| 588 | - | ||||
| 589 | - | .h-64 { | |||
| 590 | - | height: 16rem; | |||
| 591 | - | } | |||
| 592 | - | ||||
| 593 | - | .h-full { | |||
| 594 | - | height: 100%; | |||
| 595 | - | } | |||
| 596 | - | ||||
| 597 | - | .w-1\/5 { | |||
| 598 | - | width: 20%; | |||
| 599 | - | } | |||
| 600 | - | ||||
| 601 | - | .w-2\/6 { | |||
| 602 | - | width: 33.333333%; | |||
| 603 | - | } | |||
| 604 | - | ||||
| 605 | - | .w-4\/6 { | |||
| 606 | - | width: 66.666667%; | |||
| 607 | - | } | |||
| 608 | - | ||||
| 609 | - | .w-72 { | |||
| 610 | - | width: 18rem; | |||
| 611 | - | } | |||
| 612 | - | ||||
| 613 | - | .w-full { | |||
| 614 | - | width: 100%; | |||
| 615 | - | } | |||
| 616 | - | ||||
| 617 | - | .w-2\/3 { | |||
| 618 | - | width: 66.666667%; | |||
| 619 | - | } | |||
| 620 | - | ||||
| 621 | - | .w-screen { | |||
| 622 | - | width: 100vw; | |||
| 623 | - | } | |||
| 624 | - | ||||
| 625 | - | .w-96 { | |||
| 626 | - | width: 24rem; | |||
| 627 | - | } | |||
| 628 | - | ||||
| 629 | - | .flex-1 { | |||
| 630 | - | flex: 1 1 0%; | |||
| 631 | - | } | |||
| 632 | - | ||||
| 633 | - | .flex-grow { | |||
| 634 | - | flex-grow: 1; | |||
| 635 | - | } | |||
| 636 | - | ||||
| 637 | - | .flex-row { | |||
| 638 | - | flex-direction: row; | |||
| 639 | - | } | |||
| 640 | - | ||||
| 641 | - | .flex-col { | |||
| 642 | - | flex-direction: column; | |||
| 643 | - | } | |||
| 644 | - | ||||
| 645 | - | .items-center { | |||
| 646 | - | align-items: center; | |||
| 647 | - | } | |||
| 648 | - | ||||
| 649 | - | .justify-end { | |||
| 650 | - | justify-content: flex-end; | |||
| 651 | - | } | |||
| 652 | - | ||||
| 653 | - | .justify-center { | |||
| 654 | - | justify-content: center; | |||
| 655 | - | } | |||
| 656 | - | ||||
| 657 | - | .justify-between { | |||
| 658 | - | justify-content: space-between; | |||
| 659 | - | } | |||
| 660 | - | ||||
| 661 | - | .justify-items-center { | |||
| 662 | - | justify-items: center; | |||
| 663 | - | } | |||
| 664 | - | ||||
| 665 | - | .space-x-4 > :not([hidden]) ~ :not([hidden]) { | |||
| 666 | - | --tw-space-x-reverse: 0; | |||
| 667 | - | margin-right: calc(1rem * var(--tw-space-x-reverse)); | |||
| 668 | - | margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); | |||
| 669 | - | } | |||
| 670 | - | ||||
| 671 | - | .space-y-2 > :not([hidden]) ~ :not([hidden]) { | |||
| 672 | - | --tw-space-y-reverse: 0; | |||
| 673 | - | margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); | |||
| 674 | - | margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); | |||
| 675 | - | } | |||
| 676 | - | ||||
| 677 | - | .space-y-4 > :not([hidden]) ~ :not([hidden]) { | |||
| 678 | - | --tw-space-y-reverse: 0; | |||
| 679 | - | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); | |||
| 680 | - | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | |||
| 681 | - | } | |||
| 682 | - | ||||
| 683 | - | .space-x-3 > :not([hidden]) ~ :not([hidden]) { | |||
| 684 | - | --tw-space-x-reverse: 0; | |||
| 685 | - | margin-right: calc(0.75rem * var(--tw-space-x-reverse)); | |||
| 686 | - | margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))); | |||
| 687 | - | } | |||
| 688 | - | ||||
| 689 | - | .space-y-3 > :not([hidden]) ~ :not([hidden]) { | |||
| 690 | - | --tw-space-y-reverse: 0; | |||
| 691 | - | margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); | |||
| 692 | - | margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); | |||
| 693 | - | } | |||
| 694 | - | ||||
| 695 | - | .overflow-y-scroll { | |||
| 696 | - | overflow-y: scroll; | |||
| 697 | - | } | |||
| 698 | - | ||||
| 699 | - | .rounded-lg { | |||
| 700 | - | border-radius: 0.5rem; | |||
| 701 | - | } | |||
| 702 | - | ||||
| 703 | - | .rounded-md { | |||
| 704 | - | border-radius: 0.375rem; | |||
| 705 | - | } | |||
| 706 | - | ||||
| 707 | - | .rounded-sm { | |||
| 708 | - | border-radius: 0.125rem; | |||
| 709 | - | } | |||
| 710 | - | ||||
| 711 | - | .border { | |||
| 712 | - | border-width: 1px; | |||
| 713 | - | } | |||
| 714 | - | ||||
| 715 | - | .border-gray-300 { | |||
| 716 | - | --tw-border-opacity: 1; | |||
| 717 | - | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | |||
| 718 | - | } | |||
| 719 | - | ||||
| 720 | - | .bg-gordonmascot-lblue { | |||
| 721 | - | --tw-bg-opacity: 1; | |||
| 722 | - | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | |||
| 723 | - | } | |||
| 724 | - | ||||
| 725 | - | .bg-gordonmascot-mblue { | |||
| 726 | - | --tw-bg-opacity: 1; | |||
| 727 | - | background-color: rgb(0 82 147 / var(--tw-bg-opacity)); | |||
| 728 | - | } | |||
| 729 | - | ||||
| 730 | - | .bg-gray-300 { | |||
| 731 | - | --tw-bg-opacity: 1; | |||
| 732 | - | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | |||
| 733 | - | } | |||
| 734 | - | ||||
| 735 | - | .bg-transparent { | |||
| 736 | - | background-color: transparent; | |||
| 737 | - | } | |||
| 738 | - | ||||
| 739 | - | .bg-white { | |||
| 740 | - | --tw-bg-opacity: 1; | |||
| 741 | - | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); | |||
| 742 | - | } | |||
| 743 | - | ||||
| 744 | - | .bg-zinc-200 { | |||
| 745 | - | --tw-bg-opacity: 1; | |||
| 746 | - | background-color: rgb(228 228 231 / var(--tw-bg-opacity)); | |||
| 747 | - | } | |||
| 748 | - | ||||
| 749 | - | .bg-gordonmascot-hblue { | |||
| 750 | - | --tw-bg-opacity: 1; | |||
| 751 | - | background-color: rgb(1 30 92 / var(--tw-bg-opacity)); | |||
| 752 | - | } | |||
| 753 | - | ||||
| 754 | - | .bg-zinc-400 { | |||
| 755 | - | --tw-bg-opacity: 1; | |||
| 756 | - | background-color: rgb(161 161 170 / var(--tw-bg-opacity)); | |||
| 757 | - | } | |||
| 758 | - | ||||
| 759 | - | .p-4 { | |||
| 760 | - | padding: 1rem; | |||
| 761 | - | } | |||
| 762 | - | ||||
| 763 | - | .p-2 { | |||
| 764 | - | padding: 0.5rem; | |||
| 765 | - | } | |||
| 766 | - | ||||
| 767 | - | .px-2 { | |||
| 768 | - | padding-left: 0.5rem; | |||
| 769 | - | padding-right: 0.5rem; | |||
| 770 | - | } | |||
| 771 | - | ||||
| 772 | - | .px-6 { | |||
| 773 | - | padding-left: 1.5rem; | |||
| 774 | - | padding-right: 1.5rem; | |||
| 775 | - | } | |||
| 776 | - | ||||
| 777 | - | .py-2 { | |||
| 778 | - | padding-top: 0.5rem; | |||
| 779 | - | padding-bottom: 0.5rem; | |||
| 780 | - | } | |||
| 781 | - | ||||
| 782 | - | .px-4 { | |||
| 783 | - | padding-left: 1rem; | |||
| 784 | - | padding-right: 1rem; | |||
| 785 | - | } | |||
| 786 | - | ||||
| 787 | - | .text-2xl { | |||
| 788 | - | font-size: 1.5rem; | |||
| 789 | - | line-height: 2rem; | |||
| 790 | - | } | |||
| 791 | - | ||||
| 792 | - | .text-5xl { | |||
| 793 | - | font-size: 3rem; | |||
| 794 | - | line-height: 1; | |||
| 795 | - | } | |||
| 796 | - | ||||
| 797 | - | .text-lg { | |||
| 798 | - | font-size: 1.125rem; | |||
| 799 | - | line-height: 1.75rem; | |||
| 800 | - | } | |||
| 801 | - | ||||
| 802 | - | .text-sm { | |||
| 803 | - | font-size: 0.875rem; | |||
| 804 | - | line-height: 1.25rem; | |||
| 805 | - | } | |||
| 806 | - | ||||
| 807 | - | .text-xl { | |||
| 808 | - | font-size: 1.25rem; | |||
| 809 | - | line-height: 1.75rem; | |||
| 810 | - | } | |||
| 811 | - | ||||
| 812 | - | .font-semibold { | |||
| 813 | - | font-weight: 600; | |||
| 814 | - | } | |||
| 815 | - | ||||
| 816 | - | .tracking-tighter { | |||
| 817 | - | letter-spacing: -0.05em; | |||
| 818 | - | } | |||
| 819 | - | ||||
| 820 | - | .text-gordonmascot-hblue { | |||
| 821 | - | --tw-text-opacity: 1; | |||
| 822 | - | color: rgb(1 30 92 / var(--tw-text-opacity)); | |||
| 823 | - | } | |||
| 824 | - | ||||
| 825 | - | .text-gordonmascot-mblue { | |||
| 826 | - | --tw-text-opacity: 1; | |||
| 827 | - | color: rgb(0 82 147 / var(--tw-text-opacity)); | |||
| 828 | - | } | |||
| 829 | - | ||||
| 830 | - | .text-gray-100 { | |||
| 831 | - | --tw-text-opacity: 1; | |||
| 832 | - | color: rgb(243 244 246 / var(--tw-text-opacity)); | |||
| 833 | - | } | |||
| 834 | - | ||||
| 835 | - | .text-gray-800 { | |||
| 836 | - | --tw-text-opacity: 1; | |||
| 837 | - | color: rgb(31 41 55 / var(--tw-text-opacity)); | |||
| 838 | - | } | |||
| 839 | - | ||||
| 840 | - | .text-slate-400 { | |||
| 841 | - | --tw-text-opacity: 1; | |||
| 842 | - | color: rgb(148 163 184 / var(--tw-text-opacity)); | |||
| 843 | - | } | |||
| 844 | - | ||||
| 845 | - | .text-slate-100 { | |||
| 846 | - | --tw-text-opacity: 1; | |||
| 847 | - | color: rgb(241 245 249 / var(--tw-text-opacity)); | |||
| 848 | - | } | |||
| 849 | - | ||||
| 850 | - | .shadow { | |||
| 851 | - | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); | |||
| 852 | - | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); | |||
| 853 | - | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); | |||
| 854 | - | } | |||
| 855 | - | ||||
| 856 | - | .outline-none { | |||
| 857 | - | outline: 2px solid transparent; | |||
| 858 | - | outline-offset: 2px; | |||
| 859 | - | } | |||
| 860 | - | ||||
| 861 | - | /* Import IBM Plex Fonts */ | |||
| 862 | - | ||||
| 863 | - | @font-face { | |||
| 864 | - | font-family: "IBMPlexSans-Bold"; | |||
| 865 | - | ||||
| 866 | - | src: url("/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2") format("woff2"); | |||
| 867 | - | ||||
| 868 | - | font-weight: bold; | |||
| 869 | - | ||||
| 870 | - | font-style: normal; | |||
| 871 | - | } | |||
| 872 | - | ||||
| 873 | - | @font-face { | |||
| 874 | - | font-family: "IBMPlexSans-Regular"; | |||
| 875 | - | ||||
| 876 | - | src: url("/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2") format("woff2"); | |||
| 877 | - | ||||
| 878 | - | font-weight: normal; | |||
| 879 | - | ||||
| 880 | - | font-style: normal; | |||
| 881 | - | } | |||
| 882 | - | ||||
| 883 | - | /* Import Jetbrains Mono Nerd Font */ | |||
| 884 | - | ||||
| 885 | - | @font-face { | |||
| 886 | - | font-family: "JetBrainsMono-Nerd-Font-Regular"; | |||
| 887 | - | ||||
| 888 | - | src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFontMono-Regular.woff2") | |||
| 889 | - | format("woff2"); | |||
| 890 | - | ||||
| 891 | - | font-weight: normal; | |||
| 892 | - | ||||
| 893 | - | font-style: normal; | |||
| 894 | - | } | |||
| 895 | - | ||||
| 896 | - | @font-face { | |||
| 897 | - | font-family: "LilitaOne-Regular"; | |||
| 898 | - | ||||
| 899 | - | src: url("/assets/fonts/LilitaOne/LilitaOne-Regular.ttf") format("truetype"); | |||
| 900 | - | ||||
| 901 | - | font-weight: normal; | |||
| 902 | - | ||||
| 903 | - | font-style: normal; | |||
| 904 | - | } | |||
| 905 | - | ||||
| 906 | - | .fade-me-out.htmx-swapping { | |||
| 907 | - | opacity: 0; | |||
| 908 | - | transition: opacity 1s ease-out; | |||
| 909 | - | } | |||
| 910 | - | ||||
| 911 | - | /* View transition */ | |||
| 912 | - | ||||
| 913 | - | @keyframes fade-in { | |||
| 914 | - | from { | |||
| 915 | - | opacity: 0; | |||
| 916 | - | } | |||
| 917 | - | } | |||
| 918 | - | ||||
| 919 | - | @keyframes fade-out { | |||
| 920 | - | to { | |||
| 921 | - | opacity: 0; | |||
| 922 | - | } | |||
| 923 | - | } | |||
| 924 | - | ||||
| 925 | - | @keyframes slide-from-right { | |||
| 926 | - | from { | |||
| 927 | - | transform: translateX(90px); | |||
| 928 | - | } | |||
| 929 | - | } | |||
| 930 | - | ||||
| 931 | - | @keyframes slide-to-left { | |||
| 932 | - | to { | |||
| 933 | - | transform: translateX(-90px); | |||
| 934 | - | } | |||
| 935 | - | } | |||
| 936 | - | ||||
| 937 | - | .slide-it { | |||
| 938 | - | view-transition-name: slide-it; | |||
| 939 | - | } | |||
| 940 | - | ||||
| 941 | - | ::view-transition-old(slide-it) { | |||
| 942 | - | animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out, | |||
| 943 | - | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; | |||
| 944 | - | } | |||
| 945 | - | ||||
| 946 | - | ::view-transition-new(slide-it) { | |||
| 947 | - | animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, | |||
| 948 | - | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; | |||
| 949 | - | } | |||
| 950 | - | ||||
| 951 | - | .gordon-title { | |||
| 952 | - | font-family: "LilitaOne-Regular", sans-serif; | |||
| 953 | - | } | |||
| 954 | - | ||||
| 955 | - | .IBMPlexSans { | |||
| 956 | - | font-family: "IBMPlexSans-Regular", sans-serif; | |||
| 957 | - | } | |||
| 958 | - | ||||
| 959 | - | .circle-shadow { | |||
| 960 | - | width: 100%; | |||
| 961 | - | position: relative; | |||
| 962 | - | } | |||
| 963 | - | ||||
| 964 | - | .circle-shadow::after { | |||
| 965 | - | position: absolute; | |||
| 966 | - | width: 50%; | |||
| 967 | - | height: 0.4rem; | |||
| 968 | - | margin: auto; | |||
| 969 | - | background: rgba(0, 0, 0, 0.1); | |||
| 970 | - | border-radius: 50%; | |||
| 971 | - | z-index: -1; | |||
| 972 | - | bottom: 2%; | |||
| 973 | - | content: ""; | |||
| 974 | - | box-shadow: 0 0 1px 0.3rem rgba(0, 0, 0, 0.1); | |||
| 975 | - | } | |||
| 976 | - | ||||
| 977 | - | .iconoir { | |||
| 978 | - | margin-right: 8px; | |||
| 979 | - | } | |||
| 980 | - | ||||
| 981 | - | @media (min-width: 640px) { | |||
| 982 | - | .sm\:h-24 { | |||
| 983 | - | height: 6rem; | |||
| 984 | - | } | |||
| 985 | - | } | |||
| 986 | - | ||||
| 987 | - | @media (min-width: 768px) { | |||
| 988 | - | .md\:w-1\/2 { | |||
| 989 | - | width: 50%; | |||
| 990 | - | } | |||
| 991 | - | ||||
| 992 | - | .md\:flex-row { | |||
| 993 | - | flex-direction: row; | |||
| 994 | - | } | |||
| 995 | - | ||||
| 996 | - | .md\:space-y-6 > :not([hidden]) ~ :not([hidden]) { | |||
| 997 | - | --tw-space-y-reverse: 0; | |||
| 998 | - | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); | |||
| 999 | - | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); | |||
| 1000 | - | } | |||
| 1001 | - | ||||
| 1002 | - | .md\:p-8 { | |||
| 1003 | - | padding: 2rem; | |||
| 1004 | - | } | |||
| 1005 | - | ||||
| 1006 | - | .md\:py-8 { | |||
| 1007 | - | padding-top: 2rem; | |||
| 1008 | - | padding-bottom: 2rem; | |||
| 1009 | - | } | |||
| 1010 | - | ||||
| 1011 | - | .md\:pl-4 { | |||
| 1012 | - | padding-left: 1rem; | |||
| 1013 | - | } | |||
| 1014 | - | ||||
| 1015 | - | .md\:pt-10 { | |||
| 1016 | - | padding-top: 2.5rem; | |||
| 1017 | - | } | |||
| 1018 | - | ||||
| 1019 | - | .md\:text-2xl { | |||
| 1020 | - | font-size: 1.5rem; | |||
| 1021 | - | line-height: 2rem; | |||
| 1022 | - | } | |||
| 1023 | - | ||||
| 1024 | - | .md\:text-3xl { | |||
| 1025 | - | font-size: 1.875rem; | |||
| 1026 | - | line-height: 2.25rem; | |||
| 1027 | - | } | |||
| 1028 | - | } | |||
| 1029 | - | ||||
| 1030 | - | @media (min-width: 1024px) { | |||
| 1031 | - | .lg\:text-4xl { | |||
| 1032 | - | font-size: 2.25rem; | |||
| 1033 | - | line-height: 2.5rem; | |||
| 1034 | - | } | |||
| 1035 | - | } | |||
| 1036 | - | ||||
| 1037 | - | @media (min-width: 1280px) { | |||
| 1038 | - | .xl\:h-48 { | |||
| 1039 | - | height: 12rem; | |||
| 1040 | - | } | |||
| 1041 | - | ||||
| 1042 | - | .xl\:text-5xl { | |||
| 1043 | - | font-size: 3rem; | |||
| 1044 | - | line-height: 1; | |||
| 1045 | - | } | |||
| 1046 | - | } | |||
D internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2 +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Light.woff2 +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Medium.woff2 +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2 +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Thin.woff2 +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2 +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Regular.woff2 +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFontMono-Regular.ttf +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/fonts/LilitaOne/LilitaOne-Regular.ttf +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/fonts/LilitaOne/LilitaOne-Regular.woff2 +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/imgs/gordon-mascot-hq-trsp.png +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/imgs/gordon-mascot-mq-trsp.png +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/assets/imgs/gordon-mascot.png +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/favicon-16x16.png +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/favicon-32x32.png +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/favicon.ico +0 -0 binary file: no text diff
binary file: no text diff
D internal/ui/public/index.gohtml +0 -124
| @@ -1,124 +0,0 @@ | |||
| 1 | - | <!-- @format --> | |
| 2 | - | ||
| 3 | - | <!DOCTYPE html> | |
| 4 | - | <html lang="en"> | |
| 5 | - | <head> | |
| 6 | - | <meta charset="UTF-8" /> | |
| 7 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 8 | - | <title>{{ .header.meta.title }}</title> | |
| 9 | - | <meta name="description" content="{{ .Header.Meta.Description }}" /> | |
| 10 | - | <meta name="keywords" content="{{ .Header.Meta.Keywords }}" /> | |
| 11 | - | <!-- Custom CSS + Tailwind --> | |
| 12 | - | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |
| 13 | - | <!-- Iconoir --> | |
| 14 | - | <link rel="stylesheet" href="/libs/iconoir.css" /> | |
| 15 | - | <!-- Alpine --> | |
| 16 | - | <script src="/libs/alpine.min.js"></script> | |
| 17 | - | <!-- HTMX min js --> | |
| 18 | - | <script src="/libs/htmx.min.js"></script> | |
| 19 | - | <!-- HTMX SSE --> | |
| 20 | - | <script src="/libs/htmx-sse.js"></script> | |
| 21 | - | <!-- Custom JS from assets/js --> | |
| 22 | - | <script src="assets/js/custom.js"></script> | |
| 23 | - | </head> | |
| 24 | - | <body class="IBMPlexSans bg-zinc-200 md:p-8 flex"> | |
| 25 | - | <!-- Left Menu --> | |
| 26 | - | <div class="w-1/5 flex flex-col space-evenly"> | |
| 27 | - | <!-- Logo --> | |
| 28 | - | <div id="logo-mascot-leftnav" class="flex justify-center p-4"> | |
| 29 | - | <div class="circle-shadow flex justify-center w-100"> | |
| 30 | - | <img | |
| 31 | - | src="/assets/imgs/gordon-mascot-hq-trsp.png" | |
| 32 | - | alt="logo" | |
| 33 | - | class="sm:h-24 h-32 xl:h-48" | |
| 34 | - | /> | |
| 35 | - | </div> | |
| 36 | - | </div> | |
| 37 | - | <!-- Site Name + Version --> | |
| 38 | - | <div | |
| 39 | - | class="p-4 flex flex-col bg-gordonmascot-lblue rounded-lg shadow space-y-4" | |
| 40 | - | > | |
| 41 | - | <div class="w-full flex justify-center"> | |
| 42 | - | <h1 | |
| 43 | - | class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-gordonmascot-hblue tracking-tighter" | |
| 44 | - | > | |
| 45 | - | Gordon | |
| 46 | - | </h1> | |
| 47 | - | <span class="gordon-v-subtitle text-gordonmascot-mblue text-sm" | |
| 48 | - | >v0.0.1</span | |
| 49 | - | > | |
| 50 | - | </div> | |
| 51 | - | <!--(Navigation) --> | |
| 52 | - | <div id="menu-leftnav" class="w-full"> | |
| 53 | - | <ul | |
| 54 | - | class="flex flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4" | |
| 55 | - | > | |
| 56 | - | <li class="flex items-center"> | |
| 57 | - | <i class="iconoir-dashboard-speed mr-2 md:text-3xl"></i> | |
| 58 | - | <a href="index.html">Dashboard</a> | |
| 59 | - | </li> | |
| 60 | - | <li class="flex items-center"> | |
| 61 | - | <i class="iconoir-server mr-2 md:text-3xl"></i> | |
| 62 | - | <a href="#manager">Manager</a> | |
| 63 | - | </li> | |
| 64 | - | <li class="flex items-center"> | |
| 65 | - | <i class="iconoir-community mr-2 md:text-3xl"></i> | |
| 66 | - | <a href="#users">Users</a> | |
| 67 | - | </li> | |
| 68 | - | <li class="flex items-center"> | |
| 69 | - | <i class="iconoir-database-settings mr-2 md:text-3xl"></i> | |
| 70 | - | <a href="#gordon-settings">Settings</a> | |
| 71 | - | </li> | |
| 72 | - | </ul> | |
| 73 | - | </div> | |
| 74 | - | ||
| 75 | - | <div class="w-full h-24 bg-gordonmascot-mblue rounded-lg"></div> | |
| 76 | - | <!-- Mock menu item --> | |
| 77 | - | </div> | |
| 78 | - | </div> | |
| 79 | - | ||
| 80 | - | <!-- Main Content including the navbar --> | |
| 81 | - | <div class="flex-1 flex flex-col md:pt-10 space-y-4 ml-4"> | |
| 82 | - | <!-- Search Nav within the main content --> | |
| 83 | - | <div class="mb-4 flex justify-end"> | |
| 84 | - | <div class="w-72 h-10 bg-gray-300 rounded-lg"> | |
| 85 | - | <!-- Search input with iconir search --> | |
| 86 | - | <div class="flex justify-between items-center h-full px-2"> | |
| 87 | - | <input | |
| 88 | - | type="text" | |
| 89 | - | class="w-full h-full bg-transparent outline-none" | |
| 90 | - | placeholder="Search" | |
| 91 | - | /> | |
| 92 | - | <i class="iconoir-search"></i> | |
| 93 | - | </div> | |
| 94 | - | </div> | |
| 95 | - | <!-- Mock search or other element --> | |
| 96 | - | </div> | |
| 97 | - | ||
| 98 | - | <!-- Cards section --> | |
| 99 | - | <div class="flex space-x-4 flex-grow"> | |
| 100 | - | <!-- Main Card on the Left --> | |
| 101 | - | <div class="w-4/6 bg-white p-4 rounded-md shadow mb-4"> | |
| 102 | - | <div class="w-full rounded-sm bg-gray-300"></div> | |
| 103 | - | <!-- Mock main card --> | |
| 104 | - | </div> | |
| 105 | - | ||
| 106 | - | <!-- Two Cards on the Right --> | |
| 107 | - | <div class="w-2/6 flex flex-col flex-justify-start"> | |
| 108 | - | <div class="bg-white p-4 rounded-md shadow mb-4"> | |
| 109 | - | <div class="w-full h-44 rounded-sm bg-gray-300"></div> | |
| 110 | - | <!-- Mock card --> | |
| 111 | - | </div> | |
| 112 | - | <div class="bg-white p-4 rounded-md shadow mb-4"> | |
| 113 | - | <div class="w-full h-44 rounded-sm bg-gray-300"></div> | |
| 114 | - | <!-- Mock card --> | |
| 115 | - | </div> | |
| 116 | - | <div class="bg-white p-4 rounded-md shadow mb-4"> | |
| 117 | - | <div class="w-full h-44 rounded-sm bg-gray-300"></div> | |
| 118 | - | <!-- Mock card --> | |
| 119 | - | </div> | |
| 120 | - | </div> | |
| 121 | - | </div> | |
| 122 | - | </div> | |
| 123 | - | </body> | |
| 124 | - | </html> | |
| @@ -1,124 +0,0 @@ | |||||
| 1 | - | <!-- @format --> | |||
| 2 | - | ||||
| 3 | - | <!DOCTYPE html> | |||
| 4 | - | <html lang="en"> | |||
| 5 | - | <head> | |||
| 6 | - | <meta charset="UTF-8" /> | |||
| 7 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
| 8 | - | <title>{{ .header.meta.title }}</title> | |||
| 9 | - | <meta name="description" content="{{ .Header.Meta.Description }}" /> | |||
| 10 | - | <meta name="keywords" content="{{ .Header.Meta.Keywords }}" /> | |||
| 11 | - | <!-- Custom CSS + Tailwind --> | |||
| 12 | - | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |||
| 13 | - | <!-- Iconoir --> | |||
| 14 | - | <link rel="stylesheet" href="/libs/iconoir.css" /> | |||
| 15 | - | <!-- Alpine --> | |||
| 16 | - | <script src="/libs/alpine.min.js"></script> | |||
| 17 | - | <!-- HTMX min js --> | |||
| 18 | - | <script src="/libs/htmx.min.js"></script> | |||
| 19 | - | <!-- HTMX SSE --> | |||
| 20 | - | <script src="/libs/htmx-sse.js"></script> | |||
| 21 | - | <!-- Custom JS from assets/js --> | |||
| 22 | - | <script src="assets/js/custom.js"></script> | |||
| 23 | - | </head> | |||
| 24 | - | <body class="IBMPlexSans bg-zinc-200 md:p-8 flex"> | |||
| 25 | - | <!-- Left Menu --> | |||
| 26 | - | <div class="w-1/5 flex flex-col space-evenly"> | |||
| 27 | - | <!-- Logo --> | |||
| 28 | - | <div id="logo-mascot-leftnav" class="flex justify-center p-4"> | |||
| 29 | - | <div class="circle-shadow flex justify-center w-100"> | |||
| 30 | - | <img | |||
| 31 | - | src="/assets/imgs/gordon-mascot-hq-trsp.png" | |||
| 32 | - | alt="logo" | |||
| 33 | - | class="sm:h-24 h-32 xl:h-48" | |||
| 34 | - | /> | |||
| 35 | - | </div> | |||
| 36 | - | </div> | |||
| 37 | - | <!-- Site Name + Version --> | |||
| 38 | - | <div | |||
| 39 | - | class="p-4 flex flex-col bg-gordonmascot-lblue rounded-lg shadow space-y-4" | |||
| 40 | - | > | |||
| 41 | - | <div class="w-full flex justify-center"> | |||
| 42 | - | <h1 | |||
| 43 | - | class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-gordonmascot-hblue tracking-tighter" | |||
| 44 | - | > | |||
| 45 | - | Gordon | |||
| 46 | - | </h1> | |||
| 47 | - | <span class="gordon-v-subtitle text-gordonmascot-mblue text-sm" | |||
| 48 | - | >v0.0.1</span | |||
| 49 | - | > | |||
| 50 | - | </div> | |||
| 51 | - | <!--(Navigation) --> | |||
| 52 | - | <div id="menu-leftnav" class="w-full"> | |||
| 53 | - | <ul | |||
| 54 | - | class="flex flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4" | |||
| 55 | - | > | |||
| 56 | - | <li class="flex items-center"> | |||
| 57 | - | <i class="iconoir-dashboard-speed mr-2 md:text-3xl"></i> | |||
| 58 | - | <a href="index.html">Dashboard</a> | |||
| 59 | - | </li> | |||
| 60 | - | <li class="flex items-center"> | |||
| 61 | - | <i class="iconoir-server mr-2 md:text-3xl"></i> | |||
| 62 | - | <a href="#manager">Manager</a> | |||
| 63 | - | </li> | |||
| 64 | - | <li class="flex items-center"> | |||
| 65 | - | <i class="iconoir-community mr-2 md:text-3xl"></i> | |||
| 66 | - | <a href="#users">Users</a> | |||
| 67 | - | </li> | |||
| 68 | - | <li class="flex items-center"> | |||
| 69 | - | <i class="iconoir-database-settings mr-2 md:text-3xl"></i> | |||
| 70 | - | <a href="#gordon-settings">Settings</a> | |||
| 71 | - | </li> | |||
| 72 | - | </ul> | |||
| 73 | - | </div> | |||
| 74 | - | ||||
| 75 | - | <div class="w-full h-24 bg-gordonmascot-mblue rounded-lg"></div> | |||
| 76 | - | <!-- Mock menu item --> | |||
| 77 | - | </div> | |||
| 78 | - | </div> | |||
| 79 | - | ||||
| 80 | - | <!-- Main Content including the navbar --> | |||
| 81 | - | <div class="flex-1 flex flex-col md:pt-10 space-y-4 ml-4"> | |||
| 82 | - | <!-- Search Nav within the main content --> | |||
| 83 | - | <div class="mb-4 flex justify-end"> | |||
| 84 | - | <div class="w-72 h-10 bg-gray-300 rounded-lg"> | |||
| 85 | - | <!-- Search input with iconir search --> | |||
| 86 | - | <div class="flex justify-between items-center h-full px-2"> | |||
| 87 | - | <input | |||
| 88 | - | type="text" | |||
| 89 | - | class="w-full h-full bg-transparent outline-none" | |||
| 90 | - | placeholder="Search" | |||
| 91 | - | /> | |||
| 92 | - | <i class="iconoir-search"></i> | |||
| 93 | - | </div> | |||
| 94 | - | </div> | |||
| 95 | - | <!-- Mock search or other element --> | |||
| 96 | - | </div> | |||
| 97 | - | ||||
| 98 | - | <!-- Cards section --> | |||
| 99 | - | <div class="flex space-x-4 flex-grow"> | |||
| 100 | - | <!-- Main Card on the Left --> | |||
| 101 | - | <div class="w-4/6 bg-white p-4 rounded-md shadow mb-4"> | |||
| 102 | - | <div class="w-full rounded-sm bg-gray-300"></div> | |||
| 103 | - | <!-- Mock main card --> | |||
| 104 | - | </div> | |||
| 105 | - | ||||
| 106 | - | <!-- Two Cards on the Right --> | |||
| 107 | - | <div class="w-2/6 flex flex-col flex-justify-start"> | |||
| 108 | - | <div class="bg-white p-4 rounded-md shadow mb-4"> | |||
| 109 | - | <div class="w-full h-44 rounded-sm bg-gray-300"></div> | |||
| 110 | - | <!-- Mock card --> | |||
| 111 | - | </div> | |||
| 112 | - | <div class="bg-white p-4 rounded-md shadow mb-4"> | |||
| 113 | - | <div class="w-full h-44 rounded-sm bg-gray-300"></div> | |||
| 114 | - | <!-- Mock card --> | |||
| 115 | - | </div> | |||
| 116 | - | <div class="bg-white p-4 rounded-md shadow mb-4"> | |||
| 117 | - | <div class="w-full h-44 rounded-sm bg-gray-300"></div> | |||
| 118 | - | <!-- Mock card --> | |||
| 119 | - | </div> | |||
| 120 | - | </div> | |||
| 121 | - | </div> | |||
| 122 | - | </div> | |||
| 123 | - | </body> | |||
| 124 | - | </html> | |||
D internal/ui/public/libs/alpine.min.js +0 -5 generated file: collapsed
D internal/ui/public/libs/chart.js +0 -20 too large to show in the browser
too large to show in the browser
D internal/ui/public/libs/htmx-sse.js +0 -322
| @@ -1,322 +0,0 @@ | |||
| 1 | - | /* | |
| 2 | - | Server Sent Events Extension | |
| 3 | - | ============================ | |
| 4 | - | This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions. | |
| 5 | - | ||
| 6 | - | */ | |
| 7 | - | ||
| 8 | - | (function(){ | |
| 9 | - | ||
| 10 | - | /** @type {import("../htmx").HtmxInternalApi} */ | |
| 11 | - | var api; | |
| 12 | - | ||
| 13 | - | htmx.defineExtension("sse", { | |
| 14 | - | ||
| 15 | - | /** | |
| 16 | - | * Init saves the provided reference to the internal HTMX API. | |
| 17 | - | * | |
| 18 | - | * @param {import("../htmx").HtmxInternalApi} api | |
| 19 | - | * @returns void | |
| 20 | - | */ | |
| 21 | - | init: function(apiRef) { | |
| 22 | - | // store a reference to the internal API. | |
| 23 | - | api = apiRef; | |
| 24 | - | ||
| 25 | - | // set a function in the public API for creating new EventSource objects | |
| 26 | - | if (htmx.createEventSource == undefined) { | |
| 27 | - | htmx.createEventSource = createEventSource; | |
| 28 | - | } | |
| 29 | - | }, | |
| 30 | - | ||
| 31 | - | /** | |
| 32 | - | * onEvent handles all events passed to this extension. | |
| 33 | - | * | |
| 34 | - | * @param {string} name | |
| 35 | - | * @param {Event} evt | |
| 36 | - | * @returns void | |
| 37 | - | */ | |
| 38 | - | onEvent: function(name, evt) { | |
| 39 | - | ||
| 40 | - | switch (name) { | |
| 41 | - | ||
| 42 | - | // Try to remove remove an EventSource when elements are removed | |
| 43 | - | case "htmx:beforeCleanupElement": | |
| 44 | - | var internalData = api.getInternalData(evt.target) | |
| 45 | - | if (internalData.sseEventSource) { | |
| 46 | - | internalData.sseEventSource.close(); | |
| 47 | - | } | |
| 48 | - | return; | |
| 49 | - | ||
| 50 | - | // Try to create EventSources when elements are processed | |
| 51 | - | case "htmx:afterProcessNode": | |
| 52 | - | createEventSourceOnElement(evt.target); | |
| 53 | - | } | |
| 54 | - | } | |
| 55 | - | }); | |
| 56 | - | ||
| 57 | - | /////////////////////////////////////////////// | |
| 58 | - | // HELPER FUNCTIONS | |
| 59 | - | /////////////////////////////////////////////// | |
| 60 | - | ||
| 61 | - | ||
| 62 | - | /** | |
| 63 | - | * createEventSource is the default method for creating new EventSource objects. | |
| 64 | - | * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed. | |
| 65 | - | * | |
| 66 | - | * @param {string} url | |
| 67 | - | * @returns EventSource | |
| 68 | - | */ | |
| 69 | - | function createEventSource(url) { | |
| 70 | - | return new EventSource(url, {withCredentials:true}); | |
| 71 | - | } | |
| 72 | - | ||
| 73 | - | function splitOnWhitespace(trigger) { | |
| 74 | - | return trigger.trim().split(/\s+/); | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | function getLegacySSEURL(elt) { | |
| 78 | - | var legacySSEValue = api.getAttributeValue(elt, "hx-sse"); | |
| 79 | - | if (legacySSEValue) { | |
| 80 | - | var values = splitOnWhitespace(legacySSEValue); | |
| 81 | - | for (var i = 0; i < values.length; i++) { | |
| 82 | - | var value = values[i].split(/:(.+)/); | |
| 83 | - | if (value[0] === "connect") { | |
| 84 | - | return value[1]; | |
| 85 | - | } | |
| 86 | - | } | |
| 87 | - | } | |
| 88 | - | } | |
| 89 | - | ||
| 90 | - | function getLegacySSESwaps(elt) { | |
| 91 | - | var legacySSEValue = api.getAttributeValue(elt, "hx-sse"); | |
| 92 | - | var returnArr = []; | |
| 93 | - | if (legacySSEValue) { | |
| 94 | - | var values = splitOnWhitespace(legacySSEValue); | |
| 95 | - | for (var i = 0; i < values.length; i++) { | |
| 96 | - | var value = values[i].split(/:(.+)/); | |
| 97 | - | if (value[0] === "swap") { | |
| 98 | - | returnArr.push(value[1]); | |
| 99 | - | } | |
| 100 | - | } | |
| 101 | - | } | |
| 102 | - | return returnArr; | |
| 103 | - | } | |
| 104 | - | ||
| 105 | - | /** | |
| 106 | - | * createEventSourceOnElement creates a new EventSource connection on the provided element. | |
| 107 | - | * If a usable EventSource already exists, then it is returned. If not, then a new EventSource | |
| 108 | - | * is created and stored in the element's internalData. | |
| 109 | - | * @param {HTMLElement} elt | |
| 110 | - | * @param {number} retryCount | |
| 111 | - | * @returns {EventSource | null} | |
| 112 | - | */ | |
| 113 | - | function createEventSourceOnElement(elt, retryCount) { | |
| 114 | - | ||
| 115 | - | if (elt == null) { | |
| 116 | - | return null; | |
| 117 | - | } | |
| 118 | - | ||
| 119 | - | var internalData = api.getInternalData(elt); | |
| 120 | - | ||
| 121 | - | // get URL from element's attribute | |
| 122 | - | var sseURL = api.getAttributeValue(elt, "sse-connect"); | |
| 123 | - | ||
| 124 | - | ||
| 125 | - | if (sseURL == undefined) { | |
| 126 | - | var legacyURL = getLegacySSEURL(elt) | |
| 127 | - | if (legacyURL) { | |
| 128 | - | sseURL = legacyURL; | |
| 129 | - | } else { | |
| 130 | - | return null; | |
| 131 | - | } | |
| 132 | - | } | |
| 133 | - | ||
| 134 | - | // Connect to the EventSource | |
| 135 | - | var source = htmx.createEventSource(sseURL); | |
| 136 | - | internalData.sseEventSource = source; | |
| 137 | - | ||
| 138 | - | // Create event handlers | |
| 139 | - | source.onerror = function (err) { | |
| 140 | - | ||
| 141 | - | // Log an error event | |
| 142 | - | api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source}); | |
| 143 | - | ||
| 144 | - | // If parent no longer exists in the document, then clean up this EventSource | |
| 145 | - | if (maybeCloseSSESource(elt)) { | |
| 146 | - | return; | |
| 147 | - | } | |
| 148 | - | ||
| 149 | - | // Otherwise, try to reconnect the EventSource | |
| 150 | - | if (source.readyState === EventSource.CLOSED) { | |
| 151 | - | retryCount = retryCount || 0; | |
| 152 | - | var timeout = Math.random() * (2 ^ retryCount) * 500; | |
| 153 | - | window.setTimeout(function() { | |
| 154 | - | createEventSourceOnElement(elt, Math.min(7, retryCount+1)); | |
| 155 | - | }, timeout); | |
| 156 | - | } | |
| 157 | - | }; | |
| 158 | - | ||
| 159 | - | source.onopen = function (evt) { | |
| 160 | - | api.triggerEvent(elt, "htmx:sseOpen", {source: source}); | |
| 161 | - | } | |
| 162 | - | ||
| 163 | - | // Add message handlers for every `sse-swap` attribute | |
| 164 | - | queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) { | |
| 165 | - | ||
| 166 | - | var sseSwapAttr = api.getAttributeValue(child, "sse-swap"); | |
| 167 | - | if (sseSwapAttr) { | |
| 168 | - | var sseEventNames = sseSwapAttr.split(","); | |
| 169 | - | } else { | |
| 170 | - | var sseEventNames = getLegacySSESwaps(child); | |
| 171 | - | } | |
| 172 | - | ||
| 173 | - | for (var i = 0 ; i < sseEventNames.length ; i++) { | |
| 174 | - | var sseEventName = sseEventNames[i].trim(); | |
| 175 | - | var listener = function(event) { | |
| 176 | - | ||
| 177 | - | // If the parent is missing then close SSE and remove listener | |
| 178 | - | if (maybeCloseSSESource(elt)) { | |
| 179 | - | source.removeEventListener(sseEventName, listener); | |
| 180 | - | return; | |
| 181 | - | } | |
| 182 | - | ||
| 183 | - | // swap the response into the DOM and trigger a notification | |
| 184 | - | swap(child, event.data); | |
| 185 | - | api.triggerEvent(elt, "htmx:sseMessage", event); | |
| 186 | - | }; | |
| 187 | - | ||
| 188 | - | // Register the new listener | |
| 189 | - | api.getInternalData(elt).sseEventListener = listener; | |
| 190 | - | source.addEventListener(sseEventName, listener); | |
| 191 | - | } | |
| 192 | - | }); | |
| 193 | - | ||
| 194 | - | // Add message handlers for every `hx-trigger="sse:*"` attribute | |
| 195 | - | queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) { | |
| 196 | - | ||
| 197 | - | var sseEventName = api.getAttributeValue(child, "hx-trigger"); | |
| 198 | - | if (sseEventName == null) { | |
| 199 | - | return; | |
| 200 | - | } | |
| 201 | - | ||
| 202 | - | // Only process hx-triggers for events with the "sse:" prefix | |
| 203 | - | if (sseEventName.slice(0, 4) != "sse:") { | |
| 204 | - | return; | |
| 205 | - | } | |
| 206 | - | ||
| 207 | - | var listener = function(event) { | |
| 208 | - | ||
| 209 | - | // If parent is missing, then close SSE and remove listener | |
| 210 | - | if (maybeCloseSSESource(elt)) { | |
| 211 | - | source.removeEventListener(sseEventName, listener); | |
| 212 | - | return; | |
| 213 | - | } | |
| 214 | - | ||
| 215 | - | // Trigger events to be handled by the rest of htmx | |
| 216 | - | htmx.trigger(child, sseEventName, event); | |
| 217 | - | htmx.trigger(child, "htmx:sseMessage", event); | |
| 218 | - | } | |
| 219 | - | ||
| 220 | - | // Register the new listener | |
| 221 | - | api.getInternalData(elt).sseEventListener = listener; | |
| 222 | - | source.addEventListener(sseEventName.slice(4), listener); | |
| 223 | - | }); | |
| 224 | - | } | |
| 225 | - | ||
| 226 | - | /** | |
| 227 | - | * maybeCloseSSESource confirms that the parent element still exists. | |
| 228 | - | * If not, then any associated SSE source is closed and the function returns true. | |
| 229 | - | * | |
| 230 | - | * @param {HTMLElement} elt | |
| 231 | - | * @returns boolean | |
| 232 | - | */ | |
| 233 | - | function maybeCloseSSESource(elt) { | |
| 234 | - | if (!api.bodyContains(elt)) { | |
| 235 | - | var source = api.getInternalData(elt).sseEventSource; | |
| 236 | - | if (source != undefined) { | |
| 237 | - | source.close(); | |
| 238 | - | // source = null | |
| 239 | - | return true; | |
| 240 | - | } | |
| 241 | - | } | |
| 242 | - | return false; | |
| 243 | - | } | |
| 244 | - | ||
| 245 | - | /** | |
| 246 | - | * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT. | |
| 247 | - | * | |
| 248 | - | * @param {HTMLElement} elt | |
| 249 | - | * @param {string} attributeName | |
| 250 | - | */ | |
| 251 | - | function queryAttributeOnThisOrChildren(elt, attributeName) { | |
| 252 | - | ||
| 253 | - | var result = []; | |
| 254 | - | ||
| 255 | - | // If the parent element also contains the requested attribute, then add it to the results too. | |
| 256 | - | if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) { | |
| 257 | - | result.push(elt); | |
| 258 | - | } | |
| 259 | - | ||
| 260 | - | // Search all child nodes that match the requested attribute | |
| 261 | - | elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) { | |
| 262 | - | result.push(node); | |
| 263 | - | }); | |
| 264 | - | ||
| 265 | - | return result; | |
| 266 | - | } | |
| 267 | - | ||
| 268 | - | /** | |
| 269 | - | * @param {HTMLElement} elt | |
| 270 | - | * @param {string} content | |
| 271 | - | */ | |
| 272 | - | function swap(elt, content) { | |
| 273 | - | ||
| 274 | - | api.withExtensions(elt, function(extension) { | |
| 275 | - | content = extension.transformResponse(content, null, elt); | |
| 276 | - | }); | |
| 277 | - | ||
| 278 | - | var swapSpec = api.getSwapSpecification(elt); | |
| 279 | - | var target = api.getTarget(elt); | |
| 280 | - | var settleInfo = api.makeSettleInfo(elt); | |
| 281 | - | ||
| 282 | - | api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo); | |
| 283 | - | ||
| 284 | - | settleInfo.elts.forEach(function (elt) { | |
| 285 | - | if (elt.classList) { | |
| 286 | - | elt.classList.add(htmx.config.settlingClass); | |
| 287 | - | } | |
| 288 | - | api.triggerEvent(elt, 'htmx:beforeSettle'); | |
| 289 | - | }); | |
| 290 | - | ||
| 291 | - | // Handle settle tasks (with delay if requested) | |
| 292 | - | if (swapSpec.settleDelay > 0) { | |
| 293 | - | setTimeout(doSettle(settleInfo), swapSpec.settleDelay); | |
| 294 | - | } else { | |
| 295 | - | doSettle(settleInfo)(); | |
| 296 | - | } | |
| 297 | - | } | |
| 298 | - | ||
| 299 | - | /** | |
| 300 | - | * doSettle mirrors much of the functionality in htmx that | |
| 301 | - | * settles elements after their content has been swapped. | |
| 302 | - | * TODO: this should be published by htmx, and not duplicated here | |
| 303 | - | * @param {import("../htmx").HtmxSettleInfo} settleInfo | |
| 304 | - | * @returns () => void | |
| 305 | - | */ | |
| 306 | - | function doSettle(settleInfo) { | |
| 307 | - | ||
| 308 | - | return function() { | |
| 309 | - | settleInfo.tasks.forEach(function (task) { | |
| 310 | - | task.call(); | |
| 311 | - | }); | |
| 312 | - | ||
| 313 | - | settleInfo.elts.forEach(function (elt) { | |
| 314 | - | if (elt.classList) { | |
| 315 | - | elt.classList.remove(htmx.config.settlingClass); | |
| 316 | - | } | |
| 317 | - | api.triggerEvent(elt, 'htmx:afterSettle'); | |
| 318 | - | }); | |
| 319 | - | } | |
| 320 | - | } | |
| 321 | - | ||
| 322 | - | })();⏎̸ | |
| @@ -1,322 +0,0 @@ | |||||
| 1 | - | /* | |||
| 2 | - | Server Sent Events Extension | |||
| 3 | - | ============================ | |||
| 4 | - | This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions. | |||
| 5 | - | ||||
| 6 | - | */ | |||
| 7 | - | ||||
| 8 | - | (function(){ | |||
| 9 | - | ||||
| 10 | - | /** @type {import("../htmx").HtmxInternalApi} */ | |||
| 11 | - | var api; | |||
| 12 | - | ||||
| 13 | - | htmx.defineExtension("sse", { | |||
| 14 | - | ||||
| 15 | - | /** | |||
| 16 | - | * Init saves the provided reference to the internal HTMX API. | |||
| 17 | - | * | |||
| 18 | - | * @param {import("../htmx").HtmxInternalApi} api | |||
| 19 | - | * @returns void | |||
| 20 | - | */ | |||
| 21 | - | init: function(apiRef) { | |||
| 22 | - | // store a reference to the internal API. | |||
| 23 | - | api = apiRef; | |||
| 24 | - | ||||
| 25 | - | // set a function in the public API for creating new EventSource objects | |||
| 26 | - | if (htmx.createEventSource == undefined) { | |||
| 27 | - | htmx.createEventSource = createEventSource; | |||
| 28 | - | } | |||
| 29 | - | }, | |||
| 30 | - | ||||
| 31 | - | /** | |||
| 32 | - | * onEvent handles all events passed to this extension. | |||
| 33 | - | * | |||
| 34 | - | * @param {string} name | |||
| 35 | - | * @param {Event} evt | |||
| 36 | - | * @returns void | |||
| 37 | - | */ | |||
| 38 | - | onEvent: function(name, evt) { | |||
| 39 | - | ||||
| 40 | - | switch (name) { | |||
| 41 | - | ||||
| 42 | - | // Try to remove remove an EventSource when elements are removed | |||
| 43 | - | case "htmx:beforeCleanupElement": | |||
| 44 | - | var internalData = api.getInternalData(evt.target) | |||
| 45 | - | if (internalData.sseEventSource) { | |||
| 46 | - | internalData.sseEventSource.close(); | |||
| 47 | - | } | |||
| 48 | - | return; | |||
| 49 | - | ||||
| 50 | - | // Try to create EventSources when elements are processed | |||
| 51 | - | case "htmx:afterProcessNode": | |||
| 52 | - | createEventSourceOnElement(evt.target); | |||
| 53 | - | } | |||
| 54 | - | } | |||
| 55 | - | }); | |||
| 56 | - | ||||
| 57 | - | /////////////////////////////////////////////// | |||
| 58 | - | // HELPER FUNCTIONS | |||
| 59 | - | /////////////////////////////////////////////// | |||
| 60 | - | ||||
| 61 | - | ||||
| 62 | - | /** | |||
| 63 | - | * createEventSource is the default method for creating new EventSource objects. | |||
| 64 | - | * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed. | |||
| 65 | - | * | |||
| 66 | - | * @param {string} url | |||
| 67 | - | * @returns EventSource | |||
| 68 | - | */ | |||
| 69 | - | function createEventSource(url) { | |||
| 70 | - | return new EventSource(url, {withCredentials:true}); | |||
| 71 | - | } | |||
| 72 | - | ||||
| 73 | - | function splitOnWhitespace(trigger) { | |||
| 74 | - | return trigger.trim().split(/\s+/); | |||
| 75 | - | } | |||
| 76 | - | ||||
| 77 | - | function getLegacySSEURL(elt) { | |||
| 78 | - | var legacySSEValue = api.getAttributeValue(elt, "hx-sse"); | |||
| 79 | - | if (legacySSEValue) { | |||
| 80 | - | var values = splitOnWhitespace(legacySSEValue); | |||
| 81 | - | for (var i = 0; i < values.length; i++) { | |||
| 82 | - | var value = values[i].split(/:(.+)/); | |||
| 83 | - | if (value[0] === "connect") { | |||
| 84 | - | return value[1]; | |||
| 85 | - | } | |||
| 86 | - | } | |||
| 87 | - | } | |||
| 88 | - | } | |||
| 89 | - | ||||
| 90 | - | function getLegacySSESwaps(elt) { | |||
| 91 | - | var legacySSEValue = api.getAttributeValue(elt, "hx-sse"); | |||
| 92 | - | var returnArr = []; | |||
| 93 | - | if (legacySSEValue) { | |||
| 94 | - | var values = splitOnWhitespace(legacySSEValue); | |||
| 95 | - | for (var i = 0; i < values.length; i++) { | |||
| 96 | - | var value = values[i].split(/:(.+)/); | |||
| 97 | - | if (value[0] === "swap") { | |||
| 98 | - | returnArr.push(value[1]); | |||
| 99 | - | } | |||
| 100 | - | } | |||
| 101 | - | } | |||
| 102 | - | return returnArr; | |||
| 103 | - | } | |||
| 104 | - | ||||
| 105 | - | /** | |||
| 106 | - | * createEventSourceOnElement creates a new EventSource connection on the provided element. | |||
| 107 | - | * If a usable EventSource already exists, then it is returned. If not, then a new EventSource | |||
| 108 | - | * is created and stored in the element's internalData. | |||
| 109 | - | * @param {HTMLElement} elt | |||
| 110 | - | * @param {number} retryCount | |||
| 111 | - | * @returns {EventSource | null} | |||
| 112 | - | */ | |||
| 113 | - | function createEventSourceOnElement(elt, retryCount) { | |||
| 114 | - | ||||
| 115 | - | if (elt == null) { | |||
| 116 | - | return null; | |||
| 117 | - | } | |||
| 118 | - | ||||
| 119 | - | var internalData = api.getInternalData(elt); | |||
| 120 | - | ||||
| 121 | - | // get URL from element's attribute | |||
| 122 | - | var sseURL = api.getAttributeValue(elt, "sse-connect"); | |||
| 123 | - | ||||
| 124 | - | ||||
| 125 | - | if (sseURL == undefined) { | |||
| 126 | - | var legacyURL = getLegacySSEURL(elt) | |||
| 127 | - | if (legacyURL) { | |||
| 128 | - | sseURL = legacyURL; | |||
| 129 | - | } else { | |||
| 130 | - | return null; | |||
| 131 | - | } | |||
| 132 | - | } | |||
| 133 | - | ||||
| 134 | - | // Connect to the EventSource | |||
| 135 | - | var source = htmx.createEventSource(sseURL); | |||
| 136 | - | internalData.sseEventSource = source; | |||
| 137 | - | ||||
| 138 | - | // Create event handlers | |||
| 139 | - | source.onerror = function (err) { | |||
| 140 | - | ||||
| 141 | - | // Log an error event | |||
| 142 | - | api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source}); | |||
| 143 | - | ||||
| 144 | - | // If parent no longer exists in the document, then clean up this EventSource | |||
| 145 | - | if (maybeCloseSSESource(elt)) { | |||
| 146 | - | return; | |||
| 147 | - | } | |||
| 148 | - | ||||
| 149 | - | // Otherwise, try to reconnect the EventSource | |||
| 150 | - | if (source.readyState === EventSource.CLOSED) { | |||
| 151 | - | retryCount = retryCount || 0; | |||
| 152 | - | var timeout = Math.random() * (2 ^ retryCount) * 500; | |||
| 153 | - | window.setTimeout(function() { | |||
| 154 | - | createEventSourceOnElement(elt, Math.min(7, retryCount+1)); | |||
| 155 | - | }, timeout); | |||
| 156 | - | } | |||
| 157 | - | }; | |||
| 158 | - | ||||
| 159 | - | source.onopen = function (evt) { | |||
| 160 | - | api.triggerEvent(elt, "htmx:sseOpen", {source: source}); | |||
| 161 | - | } | |||
| 162 | - | ||||
| 163 | - | // Add message handlers for every `sse-swap` attribute | |||
| 164 | - | queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) { | |||
| 165 | - | ||||
| 166 | - | var sseSwapAttr = api.getAttributeValue(child, "sse-swap"); | |||
| 167 | - | if (sseSwapAttr) { | |||
| 168 | - | var sseEventNames = sseSwapAttr.split(","); | |||
| 169 | - | } else { | |||
| 170 | - | var sseEventNames = getLegacySSESwaps(child); | |||
| 171 | - | } | |||
| 172 | - | ||||
| 173 | - | for (var i = 0 ; i < sseEventNames.length ; i++) { | |||
| 174 | - | var sseEventName = sseEventNames[i].trim(); | |||
| 175 | - | var listener = function(event) { | |||
| 176 | - | ||||
| 177 | - | // If the parent is missing then close SSE and remove listener | |||
| 178 | - | if (maybeCloseSSESource(elt)) { | |||
| 179 | - | source.removeEventListener(sseEventName, listener); | |||
| 180 | - | return; | |||
| 181 | - | } | |||
| 182 | - | ||||
| 183 | - | // swap the response into the DOM and trigger a notification | |||
| 184 | - | swap(child, event.data); | |||
| 185 | - | api.triggerEvent(elt, "htmx:sseMessage", event); | |||
| 186 | - | }; | |||
| 187 | - | ||||
| 188 | - | // Register the new listener | |||
| 189 | - | api.getInternalData(elt).sseEventListener = listener; | |||
| 190 | - | source.addEventListener(sseEventName, listener); | |||
| 191 | - | } | |||
| 192 | - | }); | |||
| 193 | - | ||||
| 194 | - | // Add message handlers for every `hx-trigger="sse:*"` attribute | |||
| 195 | - | queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) { | |||
| 196 | - | ||||
| 197 | - | var sseEventName = api.getAttributeValue(child, "hx-trigger"); | |||
| 198 | - | if (sseEventName == null) { | |||
| 199 | - | return; | |||
| 200 | - | } | |||
| 201 | - | ||||
| 202 | - | // Only process hx-triggers for events with the "sse:" prefix | |||
| 203 | - | if (sseEventName.slice(0, 4) != "sse:") { | |||
| 204 | - | return; | |||
| 205 | - | } | |||
| 206 | - | ||||
| 207 | - | var listener = function(event) { | |||
| 208 | - | ||||
| 209 | - | // If parent is missing, then close SSE and remove listener | |||
| 210 | - | if (maybeCloseSSESource(elt)) { | |||
| 211 | - | source.removeEventListener(sseEventName, listener); | |||
| 212 | - | return; | |||
| 213 | - | } | |||
| 214 | - | ||||
| 215 | - | // Trigger events to be handled by the rest of htmx | |||
| 216 | - | htmx.trigger(child, sseEventName, event); | |||
| 217 | - | htmx.trigger(child, "htmx:sseMessage", event); | |||
| 218 | - | } | |||
| 219 | - | ||||
| 220 | - | // Register the new listener | |||
| 221 | - | api.getInternalData(elt).sseEventListener = listener; | |||
| 222 | - | source.addEventListener(sseEventName.slice(4), listener); | |||
| 223 | - | }); | |||
| 224 | - | } | |||
| 225 | - | ||||
| 226 | - | /** | |||
| 227 | - | * maybeCloseSSESource confirms that the parent element still exists. | |||
| 228 | - | * If not, then any associated SSE source is closed and the function returns true. | |||
| 229 | - | * | |||
| 230 | - | * @param {HTMLElement} elt | |||
| 231 | - | * @returns boolean | |||
| 232 | - | */ | |||
| 233 | - | function maybeCloseSSESource(elt) { | |||
| 234 | - | if (!api.bodyContains(elt)) { | |||
| 235 | - | var source = api.getInternalData(elt).sseEventSource; | |||
| 236 | - | if (source != undefined) { | |||
| 237 | - | source.close(); | |||
| 238 | - | // source = null | |||
| 239 | - | return true; | |||
| 240 | - | } | |||
| 241 | - | } | |||
| 242 | - | return false; | |||
| 243 | - | } | |||
| 244 | - | ||||
| 245 | - | /** | |||
| 246 | - | * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT. | |||
| 247 | - | * | |||
| 248 | - | * @param {HTMLElement} elt | |||
| 249 | - | * @param {string} attributeName | |||
| 250 | - | */ | |||
| 251 | - | function queryAttributeOnThisOrChildren(elt, attributeName) { | |||
| 252 | - | ||||
| 253 | - | var result = []; | |||
| 254 | - | ||||
| 255 | - | // If the parent element also contains the requested attribute, then add it to the results too. | |||
| 256 | - | if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) { | |||
| 257 | - | result.push(elt); | |||
| 258 | - | } | |||
| 259 | - | ||||
| 260 | - | // Search all child nodes that match the requested attribute | |||
| 261 | - | elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) { | |||
| 262 | - | result.push(node); | |||
| 263 | - | }); | |||
| 264 | - | ||||
| 265 | - | return result; | |||
| 266 | - | } | |||
| 267 | - | ||||
| 268 | - | /** | |||
| 269 | - | * @param {HTMLElement} elt | |||
| 270 | - | * @param {string} content | |||
| 271 | - | */ | |||
| 272 | - | function swap(elt, content) { | |||
| 273 | - | ||||
| 274 | - | api.withExtensions(elt, function(extension) { | |||
| 275 | - | content = extension.transformResponse(content, null, elt); | |||
| 276 | - | }); | |||
| 277 | - | ||||
| 278 | - | var swapSpec = api.getSwapSpecification(elt); | |||
| 279 | - | var target = api.getTarget(elt); | |||
| 280 | - | var settleInfo = api.makeSettleInfo(elt); | |||
| 281 | - | ||||
| 282 | - | api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo); | |||
| 283 | - | ||||
| 284 | - | settleInfo.elts.forEach(function (elt) { | |||
| 285 | - | if (elt.classList) { | |||
| 286 | - | elt.classList.add(htmx.config.settlingClass); | |||
| 287 | - | } | |||
| 288 | - | api.triggerEvent(elt, 'htmx:beforeSettle'); | |||
| 289 | - | }); | |||
| 290 | - | ||||
| 291 | - | // Handle settle tasks (with delay if requested) | |||
| 292 | - | if (swapSpec.settleDelay > 0) { | |||
| 293 | - | setTimeout(doSettle(settleInfo), swapSpec.settleDelay); | |||
| 294 | - | } else { | |||
| 295 | - | doSettle(settleInfo)(); | |||
| 296 | - | } | |||
| 297 | - | } | |||
| 298 | - | ||||
| 299 | - | /** | |||
| 300 | - | * doSettle mirrors much of the functionality in htmx that | |||
| 301 | - | * settles elements after their content has been swapped. | |||
| 302 | - | * TODO: this should be published by htmx, and not duplicated here | |||
| 303 | - | * @param {import("../htmx").HtmxSettleInfo} settleInfo | |||
| 304 | - | * @returns () => void | |||
| 305 | - | */ | |||
| 306 | - | function doSettle(settleInfo) { | |||
| 307 | - | ||||
| 308 | - | return function() { | |||
| 309 | - | settleInfo.tasks.forEach(function (task) { | |||
| 310 | - | task.call(); | |||
| 311 | - | }); | |||
| 312 | - | ||||
| 313 | - | settleInfo.elts.forEach(function (elt) { | |||
| 314 | - | if (elt.classList) { | |||
| 315 | - | elt.classList.remove(htmx.config.settlingClass); | |||
| 316 | - | } | |||
| 317 | - | api.triggerEvent(elt, 'htmx:afterSettle'); | |||
| 318 | - | }); | |||
| 319 | - | } | |||
| 320 | - | } | |||
| 321 | - | ||||
| 322 | - | })();⏎̸ | |||
D internal/ui/public/libs/htmx.min.js +0 -1 generated file: collapsed
D internal/ui/public/libs/iconoir.css +0 -22 too large to show in the browser
too large to show in the browser
D internal/ui/public/site.webmanifest +0 -1
| @@ -1,1 +0,0 @@ | |||
| 1 | - | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}⏎̸ | |
| @@ -1,1 +0,0 @@ | |||||
| 1 | - | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}⏎̸ | |||
D internal/ui/templates/components.gohtml +0 -33
| @@ -1,33 +0,0 @@ | |||
| 1 | - | <!-- @format --> | |
| 2 | - | ||
| 3 | - | <div id="header"> | |
| 4 | - | <h1 | |
| 5 | - | class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s" | |
| 6 | - | > | |
| 7 | - | {{ .body.header.helloworld }} | |
| 8 | - | </h1> | |
| 9 | - | </div> | |
| 10 | - | ||
| 11 | - | <div id="hello"> | |
| 12 | - | <h1 | |
| 13 | - | class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s" | |
| 14 | - | > | |
| 15 | - | {{ .body.div.hello }} | |
| 16 | - | </h1> | |
| 17 | - | </div> | |
| 18 | - | <div | |
| 19 | - | id="header" | |
| 20 | - | hx-get="/htmx" | |
| 21 | - | hx-headers='{"HX-Fragment": "header"}' | |
| 22 | - | hx-trigger="load" | |
| 23 | - | hx-swap="outerHTML" | |
| 24 | - | ></div> | |
| 25 | - | ||
| 26 | - | <!-- App Fragment --> | |
| 27 | - | <div | |
| 28 | - | id="app" | |
| 29 | - | hx-get="/htmx" | |
| 30 | - | hx-headers='{"HX-Fragment": "hello"}' | |
| 31 | - | hx-trigger="load" | |
| 32 | - | hx-swap="outerHTML" | |
| 33 | - | ></div> | |
| @@ -1,33 +0,0 @@ | |||||
| 1 | - | <!-- @format --> | |||
| 2 | - | ||||
| 3 | - | <div id="header"> | |||
| 4 | - | <h1 | |||
| 5 | - | class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s" | |||
| 6 | - | > | |||
| 7 | - | {{ .body.header.helloworld }} | |||
| 8 | - | </h1> | |||
| 9 | - | </div> | |||
| 10 | - | ||||
| 11 | - | <div id="hello"> | |||
| 12 | - | <h1 | |||
| 13 | - | class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s" | |||
| 14 | - | > | |||
| 15 | - | {{ .body.div.hello }} | |||
| 16 | - | </h1> | |||
| 17 | - | </div> | |||
| 18 | - | <div | |||
| 19 | - | id="header" | |||
| 20 | - | hx-get="/htmx" | |||
| 21 | - | hx-headers='{"HX-Fragment": "header"}' | |||
| 22 | - | hx-trigger="load" | |||
| 23 | - | hx-swap="outerHTML" | |||
| 24 | - | ></div> | |||
| 25 | - | ||||
| 26 | - | <!-- App Fragment --> | |||
| 27 | - | <div | |||
| 28 | - | id="app" | |||
| 29 | - | hx-get="/htmx" | |||
| 30 | - | hx-headers='{"HX-Fragment": "hello"}' | |||
| 31 | - | hx-trigger="load" | |||
| 32 | - | hx-swap="outerHTML" | |||
| 33 | - | ></div> | |||
D internal/ui/templates/dashboard.gohtml +0 -9
| @@ -1,9 +0,0 @@ | |||
| 1 | - | <div id="dashboard"> | |
| 2 | - | <h1 | |
| 3 | - | class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s" | |
| 4 | - | > | |
| 5 | - | Hello World from dashboard.html | |
| 6 | - | </h1> | |
| 7 | - | <!-- We load Chart.js here to avoid loading it in every page end when all the rest is loaded --> | |
| 8 | - | <script src="libs/chart.js"></script> | |
| 9 | - | </div> | |
| @@ -1,9 +0,0 @@ | |||||
| 1 | - | <div id="dashboard"> | |||
| 2 | - | <h1 | |||
| 3 | - | class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s" | |||
| 4 | - | > | |||
| 5 | - | Hello World from dashboard.html | |||
| 6 | - | </h1> | |||
| 7 | - | <!-- We load Chart.js here to avoid loading it in every page end when all the rest is loaded --> | |||
| 8 | - | <script src="libs/chart.js"></script> | |||
| 9 | - | </div> | |||
D internal/ui/templates/install.gohtml +0 -137
| @@ -1,137 +0,0 @@ | |||
| 1 | - | <!DOCTYPE html> | |
| 2 | - | <html lang="en"> | |
| 3 | - | <head> | |
| 4 | - | <meta charset="UTF-8" /> | |
| 5 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 6 | - | <title>{{ .header.meta.title }}</title> | |
| 7 | - | <meta name="description" content="{{ .Header.Meta.Description }}" /> | |
| 8 | - | <meta name="keywords" content="{{ .Header.Meta.Keywords }}" /> | |
| 9 | - | <!-- Custom CSS + Tailwind --> | |
| 10 | - | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |
| 11 | - | <!-- Iconoir --> | |
| 12 | - | <link rel="stylesheet" href="/libs/iconoir.css" /> | |
| 13 | - | <!-- Alpine --> | |
| 14 | - | <script src="/libs/alpine.min.js"></script> | |
| 15 | - | <!-- HTMX min js --> | |
| 16 | - | <script src="/libs/htmx.min.js"></script> | |
| 17 | - | <!-- HTMX SSE --> | |
| 18 | - | <script src="/libs/htmx-sse.js"></script> | |
| 19 | - | <!-- Custom JS from assets/js --> | |
| 20 | - | <script src="/assets/js/custom.js"></script> | |
| 21 | - | </head> | |
| 22 | - | ||
| 23 | - | <body | |
| 24 | - | class="IBMPlexSans bg-zinc-200 md:p-8 flex flex-col items-center" | |
| 25 | - | hx-boost="true" | |
| 26 | - | > | |
| 27 | - | <!-- Logo and Title --> | |
| 28 | - | <div | |
| 29 | - | id="logo-mascot" | |
| 30 | - | class="flex flex-col justify-center items-center p-4 mb-4" | |
| 31 | - | > | |
| 32 | - | <div class="circle-shadow flex justify-center w-100"> | |
| 33 | - | <img | |
| 34 | - | src="/assets/imgs/gordon-mascot-hq-trsp.png" | |
| 35 | - | alt="logo" | |
| 36 | - | class="sm:h-24 h-32 xl:h-48" | |
| 37 | - | /> | |
| 38 | - | </div> | |
| 39 | - | <div class="flex justify-center mt-4"> | |
| 40 | - | <h1 | |
| 41 | - | class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-gordonmascot-hblue tracking-tighter" | |
| 42 | - | > | |
| 43 | - | Gordon | |
| 44 | - | </h1> | |
| 45 | - | <span class="gordon-v-subtitle text-gordonmascot-mblue text-sm ml-2" | |
| 46 | - | >v0.0.1</span | |
| 47 | - | > | |
| 48 | - | </div> | |
| 49 | - | </div> | |
| 50 | - | ||
| 51 | - | <!-- Main Content --> | |
| 52 | - | <div class="bg-white p-4 rounded-md shadow mb-4 w-2/3"> | |
| 53 | - | <!-- Install Traefik --> | |
| 54 | - | <div id="install-traefik"> | |
| 55 | - | <div class="flex flex-col items-center"> | |
| 56 | - | <h2 class="text-2xl font-semibold text-gray-800"> | |
| 57 | - | 1 - Install Traefik | |
| 58 | - | </h2> | |
| 59 | - | <!-- Dual view left : install procedure / right : SSE log installation --> | |
| 60 | - | <div class="flex flex-col md:flex-row justify-items-center w-full"> | |
| 61 | - | <!-- Left --> | |
| 62 | - | <div class="w-full md:w-1/2 p-4"> | |
| 63 | - | <form | |
| 64 | - | id="install-traefik-form" | |
| 65 | - | action="/admin/install/traefik" | |
| 66 | - | method="POST" | |
| 67 | - | hx-post="/admin/install/traefik" | |
| 68 | - | hx-swap="outerHTML" | |
| 69 | - | > | |
| 70 | - | <h2 class="">1.1 - Set your top domain name</h2> | |
| 71 | - | <input | |
| 72 | - | type="text" | |
| 73 | - | name="topdomain" | |
| 74 | - | id="topdomain" | |
| 75 | - | class="w-full py-2 px-4 my-2 border border-gray-300 rounded-md" | |
| 76 | - | placeholder="example.com" | |
| 77 | - | /> | |
| 78 | - | <h2>1.2 - Set your admin email (a reachable one)</h2> | |
| 79 | - | <input | |
| 80 | - | type="text" | |
| 81 | - | name="adminemail" | |
| 82 | - | id="adminemail" | |
| 83 | - | class="w-full p-2 border border-gray-300 rounded-md" | |
| 84 | - | placeholder="admin@example.com" | |
| 85 | - | /> | |
| 86 | - | <button | |
| 87 | - | type="submit" | |
| 88 | - | class="w-full p-2 bg-gordonmascot-hblue text-slate-100 rounded-md mt-4" | |
| 89 | - | > | |
| 90 | - | Save & Install Traefik | |
| 91 | - | </button> | |
| 92 | - | </form> | |
| 93 | - | </div> | |
| 94 | - | <!-- Right --> | |
| 95 | - | <div class="w-full md:w-1/2 p-4 text-slate-100"> | |
| 96 | - | <div | |
| 97 | - | class="flex flex-col items-center bg-gordonmascot-hblue rounded-md" | |
| 98 | - | > | |
| 99 | - | <div class="w-full h-64 overflow-y-scroll-auto p-4"> | |
| 100 | - | <div | |
| 101 | - | class="flex flex-col space-y-2" | |
| 102 | - | hx-ext="sse" | |
| 103 | - | sse-connect="/admin/install/traefik/sse" | |
| 104 | - | > | |
| 105 | - | <!-- One item --> | |
| 106 | - | <div class="flex flex-row items-center"> | |
| 107 | - | <span class="">[INFO]</span> | |
| 108 | - | <span class="">Traefik installed</span> | |
| 109 | - | </div> | |
| 110 | - | <!-- One item --> | |
| 111 | - | <div class="flex flex-row items-center"> | |
| 112 | - | <span class="">[INFO]</span> | |
| 113 | - | <span class="ml-2">Traefik started</span> | |
| 114 | - | </div> | |
| 115 | - | <!-- One item --> | |
| 116 | - | <div class="flex flex-row items-center"> | |
| 117 | - | <span class="">[INFO]</span> | |
| 118 | - | <span class="ml-2" | |
| 119 | - | >Traefik configuration file created</span | |
| 120 | - | > | |
| 121 | - | </div> | |
| 122 | - | <!-- One item --> | |
| 123 | - | <div class="flex flex-row items-center"> | |
| 124 | - | <span class="">[INFO]</span> | |
| 125 | - | <span class="">Traefik configuration file updated</span> | |
| 126 | - | </div> | |
| 127 | - | </div> | |
| 128 | - | </div> | |
| 129 | - | </div> | |
| 130 | - | </div> | |
| 131 | - | </div> | |
| 132 | - | </div> | |
| 133 | - | </div> | |
| 134 | - | <!-- End Install Traefik --> | |
| 135 | - | </div> | |
| 136 | - | </body> | |
| 137 | - | </html> | |
| @@ -1,137 +0,0 @@ | |||||
| 1 | - | <!DOCTYPE html> | |||
| 2 | - | <html lang="en"> | |||
| 3 | - | <head> | |||
| 4 | - | <meta charset="UTF-8" /> | |||
| 5 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
| 6 | - | <title>{{ .header.meta.title }}</title> | |||
| 7 | - | <meta name="description" content="{{ .Header.Meta.Description }}" /> | |||
| 8 | - | <meta name="keywords" content="{{ .Header.Meta.Keywords }}" /> | |||
| 9 | - | <!-- Custom CSS + Tailwind --> | |||
| 10 | - | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |||
| 11 | - | <!-- Iconoir --> | |||
| 12 | - | <link rel="stylesheet" href="/libs/iconoir.css" /> | |||
| 13 | - | <!-- Alpine --> | |||
| 14 | - | <script src="/libs/alpine.min.js"></script> | |||
| 15 | - | <!-- HTMX min js --> | |||
| 16 | - | <script src="/libs/htmx.min.js"></script> | |||
| 17 | - | <!-- HTMX SSE --> | |||
| 18 | - | <script src="/libs/htmx-sse.js"></script> | |||
| 19 | - | <!-- Custom JS from assets/js --> | |||
| 20 | - | <script src="/assets/js/custom.js"></script> | |||
| 21 | - | </head> | |||
| 22 | - | ||||
| 23 | - | <body | |||
| 24 | - | class="IBMPlexSans bg-zinc-200 md:p-8 flex flex-col items-center" | |||
| 25 | - | hx-boost="true" | |||
| 26 | - | > | |||
| 27 | - | <!-- Logo and Title --> | |||
| 28 | - | <div | |||
| 29 | - | id="logo-mascot" | |||
| 30 | - | class="flex flex-col justify-center items-center p-4 mb-4" | |||
| 31 | - | > | |||
| 32 | - | <div class="circle-shadow flex justify-center w-100"> | |||
| 33 | - | <img | |||
| 34 | - | src="/assets/imgs/gordon-mascot-hq-trsp.png" | |||
| 35 | - | alt="logo" | |||
| 36 | - | class="sm:h-24 h-32 xl:h-48" | |||
| 37 | - | /> | |||
| 38 | - | </div> | |||
| 39 | - | <div class="flex justify-center mt-4"> | |||
| 40 | - | <h1 | |||
| 41 | - | class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-gordonmascot-hblue tracking-tighter" | |||
| 42 | - | > | |||
| 43 | - | Gordon | |||
| 44 | - | </h1> | |||
| 45 | - | <span class="gordon-v-subtitle text-gordonmascot-mblue text-sm ml-2" | |||
| 46 | - | >v0.0.1</span | |||
| 47 | - | > | |||
| 48 | - | </div> | |||
| 49 | - | </div> | |||
| 50 | - | ||||
| 51 | - | <!-- Main Content --> | |||
| 52 | - | <div class="bg-white p-4 rounded-md shadow mb-4 w-2/3"> | |||
| 53 | - | <!-- Install Traefik --> | |||
| 54 | - | <div id="install-traefik"> | |||
| 55 | - | <div class="flex flex-col items-center"> | |||
| 56 | - | <h2 class="text-2xl font-semibold text-gray-800"> | |||
| 57 | - | 1 - Install Traefik | |||
| 58 | - | </h2> | |||
| 59 | - | <!-- Dual view left : install procedure / right : SSE log installation --> | |||
| 60 | - | <div class="flex flex-col md:flex-row justify-items-center w-full"> | |||
| 61 | - | <!-- Left --> | |||
| 62 | - | <div class="w-full md:w-1/2 p-4"> | |||
| 63 | - | <form | |||
| 64 | - | id="install-traefik-form" | |||
| 65 | - | action="/admin/install/traefik" | |||
| 66 | - | method="POST" | |||
| 67 | - | hx-post="/admin/install/traefik" | |||
| 68 | - | hx-swap="outerHTML" | |||
| 69 | - | > | |||
| 70 | - | <h2 class="">1.1 - Set your top domain name</h2> | |||
| 71 | - | <input | |||
| 72 | - | type="text" | |||
| 73 | - | name="topdomain" | |||
| 74 | - | id="topdomain" | |||
| 75 | - | class="w-full py-2 px-4 my-2 border border-gray-300 rounded-md" | |||
| 76 | - | placeholder="example.com" | |||
| 77 | - | /> | |||
| 78 | - | <h2>1.2 - Set your admin email (a reachable one)</h2> | |||
| 79 | - | <input | |||
| 80 | - | type="text" | |||
| 81 | - | name="adminemail" | |||
| 82 | - | id="adminemail" | |||
| 83 | - | class="w-full p-2 border border-gray-300 rounded-md" | |||
| 84 | - | placeholder="admin@example.com" | |||
| 85 | - | /> | |||
| 86 | - | <button | |||
| 87 | - | type="submit" | |||
| 88 | - | class="w-full p-2 bg-gordonmascot-hblue text-slate-100 rounded-md mt-4" | |||
| 89 | - | > | |||
| 90 | - | Save & Install Traefik | |||
| 91 | - | </button> | |||
| 92 | - | </form> | |||
| 93 | - | </div> | |||
| 94 | - | <!-- Right --> | |||
| 95 | - | <div class="w-full md:w-1/2 p-4 text-slate-100"> | |||
| 96 | - | <div | |||
| 97 | - | class="flex flex-col items-center bg-gordonmascot-hblue rounded-md" | |||
| 98 | - | > | |||
| 99 | - | <div class="w-full h-64 overflow-y-scroll-auto p-4"> | |||
| 100 | - | <div | |||
| 101 | - | class="flex flex-col space-y-2" | |||
| 102 | - | hx-ext="sse" | |||
| 103 | - | sse-connect="/admin/install/traefik/sse" | |||
| 104 | - | > | |||
| 105 | - | <!-- One item --> | |||
| 106 | - | <div class="flex flex-row items-center"> | |||
| 107 | - | <span class="">[INFO]</span> | |||
| 108 | - | <span class="">Traefik installed</span> | |||
| 109 | - | </div> | |||
| 110 | - | <!-- One item --> | |||
| 111 | - | <div class="flex flex-row items-center"> | |||
| 112 | - | <span class="">[INFO]</span> | |||
| 113 | - | <span class="ml-2">Traefik started</span> | |||
| 114 | - | </div> | |||
| 115 | - | <!-- One item --> | |||
| 116 | - | <div class="flex flex-row items-center"> | |||
| 117 | - | <span class="">[INFO]</span> | |||
| 118 | - | <span class="ml-2" | |||
| 119 | - | >Traefik configuration file created</span | |||
| 120 | - | > | |||
| 121 | - | </div> | |||
| 122 | - | <!-- One item --> | |||
| 123 | - | <div class="flex flex-row items-center"> | |||
| 124 | - | <span class="">[INFO]</span> | |||
| 125 | - | <span class="">Traefik configuration file updated</span> | |||
| 126 | - | </div> | |||
| 127 | - | </div> | |||
| 128 | - | </div> | |||
| 129 | - | </div> | |||
| 130 | - | </div> | |||
| 131 | - | </div> | |||
| 132 | - | </div> | |||
| 133 | - | </div> | |||
| 134 | - | <!-- End Install Traefik --> | |||
| 135 | - | </div> | |||
| 136 | - | </body> | |||
| 137 | - | </html> | |||
D internal/ui/templates/manager.gohtml +0 -0
no textual change.
D internal/ui/templates/strings.yml +0 -23
| @@ -1,23 +0,0 @@ | |||
| 1 | - | en: | |
| 2 | - | header: | |
| 3 | - | meta: | |
| 4 | - | title: Gordon - Admin Dashboard | |
| 5 | - | description: Admin dashboard for Gordon | |
| 6 | - | keywords: gordon, admin, dashboard | |
| 7 | - | body: | |
| 8 | - | header: | |
| 9 | - | helloworld: Hello World! | |
| 10 | - | div: | |
| 11 | - | hello: Hello component loaded from a template! | |
| 12 | - | ||
| 13 | - | fr: | |
| 14 | - | header: | |
| 15 | - | meta: | |
| 16 | - | title: Gordon - Tableau de bord administrateur | |
| 17 | - | description: Tableau de bord administrateur pour Gordon | |
| 18 | - | keywords: gordon, admin, dashboard | |
| 19 | - | body: | |
| 20 | - | header: | |
| 21 | - | helloworld: Bonjour le monde! | |
| 22 | - | div: | |
| 23 | - | hello: Composant de salut chargé à partir d'un modèle! | |
| @@ -1,23 +0,0 @@ | |||||
| 1 | - | en: | |||
| 2 | - | header: | |||
| 3 | - | meta: | |||
| 4 | - | title: Gordon - Admin Dashboard | |||
| 5 | - | description: Admin dashboard for Gordon | |||
| 6 | - | keywords: gordon, admin, dashboard | |||
| 7 | - | body: | |||
| 8 | - | header: | |||
| 9 | - | helloworld: Hello World! | |||
| 10 | - | div: | |||
| 11 | - | hello: Hello component loaded from a template! | |||
| 12 | - | ||||
| 13 | - | fr: | |||
| 14 | - | header: | |||
| 15 | - | meta: | |||
| 16 | - | title: Gordon - Tableau de bord administrateur | |||
| 17 | - | description: Tableau de bord administrateur pour Gordon | |||
| 18 | - | keywords: gordon, admin, dashboard | |||
| 19 | - | body: | |||
| 20 | - | header: | |||
| 21 | - | helloworld: Bonjour le monde! | |||
| 22 | - | div: | |||
| 23 | - | hello: Composant de salut chargé à partir d'un modèle! | |||
D internal/ui/templates/user.gohtml +0 -0
no textual change.
D loop +0 -0
no textual change.
D pkg/docker/commands.go +0 -165
| @@ -1,165 +0,0 @@ | |||
| 1 | - | package docker | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bufio" | |
| 5 | - | "bytes" | |
| 6 | - | "context" | |
| 7 | - | "errors" | |
| 8 | - | "fmt" | |
| 9 | - | "io" | |
| 10 | - | "os" | |
| 11 | - | "os/exec" | |
| 12 | - | "path/filepath" | |
| 13 | - | ||
| 14 | - | "github.com/docker/docker/api/types" | |
| 15 | - | "github.com/docker/docker/client" | |
| 16 | - | "github.com/docker/docker/pkg/archive" | |
| 17 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |
| 18 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |
| 19 | - | ) | |
| 20 | - | ||
| 21 | - | // Use the App struct to access the logger | |
| 22 | - | ||
| 23 | - | func CreateImageFromDockerfile(a *app.App) error { | |
| 24 | - | ctx := context.Background() | |
| 25 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |
| 26 | - | if err != nil { | |
| 27 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to initialize Docker client: %v", err)) | |
| 28 | - | defer cli.Close() | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | a.AppLogger.Log(utils.INFO, "Building image...") | |
| 32 | - | buildCtx, err := archive.TarWithOptions("tmp/docker-test/", &archive.TarOptions{}) | |
| 33 | - | if err != nil { | |
| 34 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to create build context: %v", err)) | |
| 35 | - | } | |
| 36 | - | defer buildCtx.Close() | |
| 37 | - | ||
| 38 | - | a.AppLogger.Log(utils.INFO, "Building image...") | |
| 39 | - | imageBuildResponse, err := cli.ImageBuild(ctx, buildCtx, types.ImageBuildOptions{ | |
| 40 | - | Tags: []string{"gordon-echo:latest"}, | |
| 41 | - | }) | |
| 42 | - | if err != nil { | |
| 43 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to build image: %v", err)) | |
| 44 | - | } | |
| 45 | - | defer imageBuildResponse.Body.Close() | |
| 46 | - | ||
| 47 | - | a.AppLogger.Log(utils.INFO, "Saving image to file...") | |
| 48 | - | ||
| 49 | - | imageTar, err := cli.ImageSave(ctx, []string{"gordon-echo:latest"}) | |
| 50 | - | if err != nil { | |
| 51 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to save image: %v", err)) | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | outputFile, err := os.Create("tmp/docker-test/gordon-echo.tar") | |
| 55 | - | if err != nil { | |
| 56 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to create output file: %v", err)) | |
| 57 | - | } | |
| 58 | - | defer outputFile.Close() | |
| 59 | - | ||
| 60 | - | _, err = io.Copy(outputFile, imageTar) | |
| 61 | - | if err != nil { | |
| 62 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to save image to file: %v", err)) | |
| 63 | - | } | |
| 64 | - | ||
| 65 | - | return nil | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | func CreateNetwork(name string, a *app.App) error { | |
| 69 | - | a.AppLogger.Log(utils.INFO, "Entering CreateNetwork function with name: "+name) | |
| 70 | - | ||
| 71 | - | ctx := context.Background() | |
| 72 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |
| 73 | - | if err != nil { | |
| 74 | - | errMsg := fmt.Sprintf("Failed to initialize Docker client: %v", err) | |
| 75 | - | a.AppLogger.Log(utils.ERROR, errMsg) | |
| 76 | - | return errors.New(errMsg) | |
| 77 | - | } | |
| 78 | - | defer cli.Close() | |
| 79 | - | ||
| 80 | - | a.AppLogger.Log(utils.INFO, "Attempting to create network...") | |
| 81 | - | _, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{ | |
| 82 | - | CheckDuplicate: true, | |
| 83 | - | Driver: "bridge", | |
| 84 | - | }) | |
| 85 | - | if err != nil { | |
| 86 | - | errMsg := fmt.Sprintf("Failed to create network: %v", err) | |
| 87 | - | a.AppLogger.Log(utils.ERROR, errMsg) | |
| 88 | - | return errors.New(errMsg) | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | a.AppLogger.Log(utils.INFO, "Successfully created network: "+name) | |
| 92 | - | return nil | |
| 93 | - | } | |
| 94 | - | ||
| 95 | - | // Check if a container exists | |
| 96 | - | func ContainerExists(name string, a *app.App) error { | |
| 97 | - | ctx := context.Background() | |
| 98 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |
| 99 | - | if err != nil { | |
| 100 | - | return err | |
| 101 | - | } | |
| 102 | - | defer cli.Close() | |
| 103 | - | ||
| 104 | - | fmt.Println("Checking if container exists...") | |
| 105 | - | containers, err := cli.ContainerList(ctx, types.ContainerListOptions{}) | |
| 106 | - | if err != nil { | |
| 107 | - | return err | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | for _, container := range containers { | |
| 111 | - | for _, containerName := range container.Names { | |
| 112 | - | if containerName == "/"+name { | |
| 113 | - | return errors.New("container already exists") | |
| 114 | - | } | |
| 115 | - | } | |
| 116 | - | } | |
| 117 | - | ||
| 118 | - | return nil | |
| 119 | - | } | |
| 120 | - | ||
| 121 | - | func CheckIfDockerIsRunning(a *app.App) error { | |
| 122 | - | cmd := exec.Command("docker", "info") | |
| 123 | - | if err := cmd.Run(); err != nil { | |
| 124 | - | return errors.New("docker is not running") | |
| 125 | - | } | |
| 126 | - | return nil | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | // CreateContainerFromComposeFile creates containers using docker compose based on the provided compose file. | |
| 130 | - | func CreateContainerFromComposeFile(composeFilePath string, a *app.App) error { | |
| 131 | - | a.AppLogger.Log(utils.INFO, "Executing CreateContainerFromComposeFile()") | |
| 132 | - | ||
| 133 | - | // Extract directory from the composeFilePath | |
| 134 | - | dir := filepath.Dir(composeFilePath) | |
| 135 | - | ||
| 136 | - | // Change directory to the compose file directory | |
| 137 | - | if err := os.Chdir(dir); err != nil { | |
| 138 | - | return errors.New("failed to change directory: " + err.Error()) | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | // Create the command | |
| 142 | - | cmd := exec.Command("docker", "compose", "up", "-d") | |
| 143 | - | ||
| 144 | - | // Capture standard output and error | |
| 145 | - | var stdout, stderr bytes.Buffer | |
| 146 | - | cmd.Stdout = &stdout | |
| 147 | - | cmd.Stderr = &stderr | |
| 148 | - | ||
| 149 | - | // Execute the command | |
| 150 | - | if err := cmd.Run(); err != nil { | |
| 151 | - | // Log the captured standard output and error for better diagnostics | |
| 152 | - | a.AppLogger.Log(utils.ERROR, "Docker Compose Output: "+stdout.String()) | |
| 153 | - | a.AppLogger.Log(utils.ERROR, "Docker Compose Error: "+stderr.String()) | |
| 154 | - | return errors.New("failed to execute docker compose command: " + err.Error()) | |
| 155 | - | } | |
| 156 | - | ||
| 157 | - | return nil | |
| 158 | - | } | |
| 159 | - | ||
| 160 | - | func streamToLogger(r io.Reader, logger *utils.Logger, level utils.LogLevel) { | |
| 161 | - | scanner := bufio.NewScanner(r) | |
| 162 | - | for scanner.Scan() { | |
| 163 | - | logger.Log(level, scanner.Text()) | |
| 164 | - | } | |
| 165 | - | } | |
| @@ -1,165 +0,0 @@ | |||||
| 1 | - | package docker | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "bufio" | |||
| 5 | - | "bytes" | |||
| 6 | - | "context" | |||
| 7 | - | "errors" | |||
| 8 | - | "fmt" | |||
| 9 | - | "io" | |||
| 10 | - | "os" | |||
| 11 | - | "os/exec" | |||
| 12 | - | "path/filepath" | |||
| 13 | - | ||||
| 14 | - | "github.com/docker/docker/api/types" | |||
| 15 | - | "github.com/docker/docker/client" | |||
| 16 | - | "github.com/docker/docker/pkg/archive" | |||
| 17 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |||
| 18 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |||
| 19 | - | ) | |||
| 20 | - | ||||
| 21 | - | // Use the App struct to access the logger | |||
| 22 | - | ||||
| 23 | - | func CreateImageFromDockerfile(a *app.App) error { | |||
| 24 | - | ctx := context.Background() | |||
| 25 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |||
| 26 | - | if err != nil { | |||
| 27 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to initialize Docker client: %v", err)) | |||
| 28 | - | defer cli.Close() | |||
| 29 | - | } | |||
| 30 | - | ||||
| 31 | - | a.AppLogger.Log(utils.INFO, "Building image...") | |||
| 32 | - | buildCtx, err := archive.TarWithOptions("tmp/docker-test/", &archive.TarOptions{}) | |||
| 33 | - | if err != nil { | |||
| 34 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to create build context: %v", err)) | |||
| 35 | - | } | |||
| 36 | - | defer buildCtx.Close() | |||
| 37 | - | ||||
| 38 | - | a.AppLogger.Log(utils.INFO, "Building image...") | |||
| 39 | - | imageBuildResponse, err := cli.ImageBuild(ctx, buildCtx, types.ImageBuildOptions{ | |||
| 40 | - | Tags: []string{"gordon-echo:latest"}, | |||
| 41 | - | }) | |||
| 42 | - | if err != nil { | |||
| 43 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to build image: %v", err)) | |||
| 44 | - | } | |||
| 45 | - | defer imageBuildResponse.Body.Close() | |||
| 46 | - | ||||
| 47 | - | a.AppLogger.Log(utils.INFO, "Saving image to file...") | |||
| 48 | - | ||||
| 49 | - | imageTar, err := cli.ImageSave(ctx, []string{"gordon-echo:latest"}) | |||
| 50 | - | if err != nil { | |||
| 51 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to save image: %v", err)) | |||
| 52 | - | } | |||
| 53 | - | ||||
| 54 | - | outputFile, err := os.Create("tmp/docker-test/gordon-echo.tar") | |||
| 55 | - | if err != nil { | |||
| 56 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to create output file: %v", err)) | |||
| 57 | - | } | |||
| 58 | - | defer outputFile.Close() | |||
| 59 | - | ||||
| 60 | - | _, err = io.Copy(outputFile, imageTar) | |||
| 61 | - | if err != nil { | |||
| 62 | - | a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to save image to file: %v", err)) | |||
| 63 | - | } | |||
| 64 | - | ||||
| 65 | - | return nil | |||
| 66 | - | } | |||
| 67 | - | ||||
| 68 | - | func CreateNetwork(name string, a *app.App) error { | |||
| 69 | - | a.AppLogger.Log(utils.INFO, "Entering CreateNetwork function with name: "+name) | |||
| 70 | - | ||||
| 71 | - | ctx := context.Background() | |||
| 72 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |||
| 73 | - | if err != nil { | |||
| 74 | - | errMsg := fmt.Sprintf("Failed to initialize Docker client: %v", err) | |||
| 75 | - | a.AppLogger.Log(utils.ERROR, errMsg) | |||
| 76 | - | return errors.New(errMsg) | |||
| 77 | - | } | |||
| 78 | - | defer cli.Close() | |||
| 79 | - | ||||
| 80 | - | a.AppLogger.Log(utils.INFO, "Attempting to create network...") | |||
| 81 | - | _, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{ | |||
| 82 | - | CheckDuplicate: true, | |||
| 83 | - | Driver: "bridge", | |||
| 84 | - | }) | |||
| 85 | - | if err != nil { | |||
| 86 | - | errMsg := fmt.Sprintf("Failed to create network: %v", err) | |||
| 87 | - | a.AppLogger.Log(utils.ERROR, errMsg) | |||
| 88 | - | return errors.New(errMsg) | |||
| 89 | - | } | |||
| 90 | - | ||||
| 91 | - | a.AppLogger.Log(utils.INFO, "Successfully created network: "+name) | |||
| 92 | - | return nil | |||
| 93 | - | } | |||
| 94 | - | ||||
| 95 | - | // Check if a container exists | |||
| 96 | - | func ContainerExists(name string, a *app.App) error { | |||
| 97 | - | ctx := context.Background() | |||
| 98 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |||
| 99 | - | if err != nil { | |||
| 100 | - | return err | |||
| 101 | - | } | |||
| 102 | - | defer cli.Close() | |||
| 103 | - | ||||
| 104 | - | fmt.Println("Checking if container exists...") | |||
| 105 | - | containers, err := cli.ContainerList(ctx, types.ContainerListOptions{}) | |||
| 106 | - | if err != nil { | |||
| 107 | - | return err | |||
| 108 | - | } | |||
| 109 | - | ||||
| 110 | - | for _, container := range containers { | |||
| 111 | - | for _, containerName := range container.Names { | |||
| 112 | - | if containerName == "/"+name { | |||
| 113 | - | return errors.New("container already exists") | |||
| 114 | - | } | |||
| 115 | - | } | |||
| 116 | - | } | |||
| 117 | - | ||||
| 118 | - | return nil | |||
| 119 | - | } | |||
| 120 | - | ||||
| 121 | - | func CheckIfDockerIsRunning(a *app.App) error { | |||
| 122 | - | cmd := exec.Command("docker", "info") | |||
| 123 | - | if err := cmd.Run(); err != nil { | |||
| 124 | - | return errors.New("docker is not running") | |||
| 125 | - | } | |||
| 126 | - | return nil | |||
| 127 | - | } | |||
| 128 | - | ||||
| 129 | - | // CreateContainerFromComposeFile creates containers using docker compose based on the provided compose file. | |||
| 130 | - | func CreateContainerFromComposeFile(composeFilePath string, a *app.App) error { | |||
| 131 | - | a.AppLogger.Log(utils.INFO, "Executing CreateContainerFromComposeFile()") | |||
| 132 | - | ||||
| 133 | - | // Extract directory from the composeFilePath | |||
| 134 | - | dir := filepath.Dir(composeFilePath) | |||
| 135 | - | ||||
| 136 | - | // Change directory to the compose file directory | |||
| 137 | - | if err := os.Chdir(dir); err != nil { | |||
| 138 | - | return errors.New("failed to change directory: " + err.Error()) | |||
| 139 | - | } | |||
| 140 | - | ||||
| 141 | - | // Create the command | |||
| 142 | - | cmd := exec.Command("docker", "compose", "up", "-d") | |||
| 143 | - | ||||
| 144 | - | // Capture standard output and error | |||
| 145 | - | var stdout, stderr bytes.Buffer | |||
| 146 | - | cmd.Stdout = &stdout | |||
| 147 | - | cmd.Stderr = &stderr | |||
| 148 | - | ||||
| 149 | - | // Execute the command | |||
| 150 | - | if err := cmd.Run(); err != nil { | |||
| 151 | - | // Log the captured standard output and error for better diagnostics | |||
| 152 | - | a.AppLogger.Log(utils.ERROR, "Docker Compose Output: "+stdout.String()) | |||
| 153 | - | a.AppLogger.Log(utils.ERROR, "Docker Compose Error: "+stderr.String()) | |||
| 154 | - | return errors.New("failed to execute docker compose command: " + err.Error()) | |||
| 155 | - | } | |||
| 156 | - | ||||
| 157 | - | return nil | |||
| 158 | - | } | |||
| 159 | - | ||||
| 160 | - | func streamToLogger(r io.Reader, logger *utils.Logger, level utils.LogLevel) { | |||
| 161 | - | scanner := bufio.NewScanner(r) | |||
| 162 | - | for scanner.Scan() { | |||
| 163 | - | logger.Log(level, scanner.Text()) | |||
| 164 | - | } | |||
| 165 | - | } | |||
D pkg/docker/daemon.json +0 -11
| @@ -1,11 +0,0 @@ | |||
| 1 | - | { | |
| 2 | - | "log-driver": "json-file", | |
| 3 | - | "log-opts": { | |
| 4 | - | "max-size": "100m", | |
| 5 | - | "max-file": "5" | |
| 6 | - | }, | |
| 7 | - | "features": { | |
| 8 | - | "buildkit": true | |
| 9 | - | }, | |
| 10 | - | "live-restore": true | |
| 11 | - | } | |
| @@ -1,11 +0,0 @@ | |||||
| 1 | - | { | |||
| 2 | - | "log-driver": "json-file", | |||
| 3 | - | "log-opts": { | |||
| 4 | - | "max-size": "100m", | |||
| 5 | - | "max-file": "5" | |||
| 6 | - | }, | |||
| 7 | - | "features": { | |||
| 8 | - | "buildkit": true | |||
| 9 | - | }, | |||
| 10 | - | "live-restore": true | |||
| 11 | - | } | |||
D pkg/htmx/html-fragments.go +0 -41
| @@ -1,41 +0,0 @@ | |||
| 1 | - | package htmx | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bytes" | |
| 5 | - | ||
| 6 | - | "github.com/PuerkitoBio/goquery" | |
| 7 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |
| 8 | - | "gogs.bnema.dev/gordon-echo/pkg/templating" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | // GetHTMLFragmentByID returns the HTML fragment with the specified id | |
| 12 | - | func GetHTMLFragmentByID(id string, data interface{}, a *app.App, ac *app.Config) (string, error) { | |
| 13 | - | // 1. Render the template | |
| 14 | - | renderer, err := templating.GetHTMLRenderer("components.gohtml", ac.GetTemplateFS(), a) | |
| 15 | - | if err != nil { | |
| 16 | - | return "", err | |
| 17 | - | } | |
| 18 | - | ||
| 19 | - | renderedHTML, err := renderer.Render(data, a) | |
| 20 | - | if err != nil { | |
| 21 | - | return "", err | |
| 22 | - | } | |
| 23 | - | ||
| 24 | - | // 2. Parse the rendered HTML with goquery | |
| 25 | - | doc, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(renderedHTML))) | |
| 26 | - | if err != nil { | |
| 27 | - | return "", err | |
| 28 | - | } | |
| 29 | - | ||
| 30 | - | // 3. Find the div with the specified id | |
| 31 | - | fragmentContent, err := doc.Find("#" + id).Html() | |
| 32 | - | if err != nil { | |
| 33 | - | return "", err | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | if fragmentContent == "" { | |
| 37 | - | return "", err | |
| 38 | - | } | |
| 39 | - | ||
| 40 | - | return fragmentContent, nil | |
| 41 | - | } | |
| @@ -1,41 +0,0 @@ | |||||
| 1 | - | package htmx | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "bytes" | |||
| 5 | - | ||||
| 6 | - | "github.com/PuerkitoBio/goquery" | |||
| 7 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |||
| 8 | - | "gogs.bnema.dev/gordon-echo/pkg/templating" | |||
| 9 | - | ) | |||
| 10 | - | ||||
| 11 | - | // GetHTMLFragmentByID returns the HTML fragment with the specified id | |||
| 12 | - | func GetHTMLFragmentByID(id string, data interface{}, a *app.App, ac *app.Config) (string, error) { | |||
| 13 | - | // 1. Render the template | |||
| 14 | - | renderer, err := templating.GetHTMLRenderer("components.gohtml", ac.GetTemplateFS(), a) | |||
| 15 | - | if err != nil { | |||
| 16 | - | return "", err | |||
| 17 | - | } | |||
| 18 | - | ||||
| 19 | - | renderedHTML, err := renderer.Render(data, a) | |||
| 20 | - | if err != nil { | |||
| 21 | - | return "", err | |||
| 22 | - | } | |||
| 23 | - | ||||
| 24 | - | // 2. Parse the rendered HTML with goquery | |||
| 25 | - | doc, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(renderedHTML))) | |||
| 26 | - | if err != nil { | |||
| 27 | - | return "", err | |||
| 28 | - | } | |||
| 29 | - | ||||
| 30 | - | // 3. Find the div with the specified id | |||
| 31 | - | fragmentContent, err := doc.Find("#" + id).Html() | |||
| 32 | - | if err != nil { | |||
| 33 | - | return "", err | |||
| 34 | - | } | |||
| 35 | - | ||||
| 36 | - | if fragmentContent == "" { | |||
| 37 | - | return "", err | |||
| 38 | - | } | |||
| 39 | - | ||||
| 40 | - | return fragmentContent, nil | |||
| 41 | - | } | |||
D pkg/htmx/htmx.go +0 -88
| @@ -1,88 +0,0 @@ | |||
| 1 | - | package htmx | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "net/http" | |
| 5 | - | ||
| 6 | - | "github.com/labstack/echo/v4" | |
| 7 | - | ) | |
| 8 | - | ||
| 9 | - | // Headers (https://htmx.org/docs/#requests) | |
| 10 | - | const ( | |
| 11 | - | HeaderRequest = "HX-Request" | |
| 12 | - | HeaderBoosted = "HX-Boosted" | |
| 13 | - | HeaderTrigger = "HX-Trigger" | |
| 14 | - | HeaderTriggerName = "HX-Trigger-Name" | |
| 15 | - | HeaderTriggerAfterSwap = "HX-Trigger-After-Swap" | |
| 16 | - | HeaderTriggerAfterSettle = "HX-Trigger-After-Settle" | |
| 17 | - | HeaderTarget = "HX-Target" | |
| 18 | - | HeaderPrompt = "HX-Prompt" | |
| 19 | - | HeaderPush = "HX-Push" | |
| 20 | - | HeaderRedirect = "HX-Redirect" | |
| 21 | - | HeaderRefresh = "HX-Refresh" | |
| 22 | - | // Custom header | |
| 23 | - | HeaderFragment = "HX-Fragment" | |
| 24 | - | ) | |
| 25 | - | ||
| 26 | - | type ( | |
| 27 | - | // Request contains data that HTMX provides during requests | |
| 28 | - | Request struct { | |
| 29 | - | Enabled bool | |
| 30 | - | Boosted bool | |
| 31 | - | Trigger string | |
| 32 | - | TriggerName string | |
| 33 | - | Target string | |
| 34 | - | Prompt string | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | // Response contain data that the server can communicate back to HTMX | |
| 38 | - | Response struct { | |
| 39 | - | Push string | |
| 40 | - | Redirect string | |
| 41 | - | Refresh bool | |
| 42 | - | Trigger string | |
| 43 | - | TriggerAfterSwap string | |
| 44 | - | TriggerAfterSettle string | |
| 45 | - | NoContent bool | |
| 46 | - | } | |
| 47 | - | ) | |
| 48 | - | ||
| 49 | - | // GetRequest extracts HTMX data from the request | |
| 50 | - | func GetRequest(ctx echo.Context) (Request, error) { | |
| 51 | - | request := Request{ | |
| 52 | - | Enabled: ctx.Request().Header.Get(HeaderRequest) == "true", | |
| 53 | - | Boosted: ctx.Request().Header.Get(HeaderBoosted) == "true", | |
| 54 | - | Trigger: ctx.Request().Header.Get(HeaderTrigger), | |
| 55 | - | TriggerName: ctx.Request().Header.Get(HeaderTriggerName), | |
| 56 | - | Target: ctx.Request().Header.Get(HeaderTarget), | |
| 57 | - | Prompt: ctx.Request().Header.Get(HeaderPrompt), | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | return request, nil | |
| 61 | - | } | |
| 62 | - | ||
| 63 | - | // Apply applies data from a Response to a server response | |
| 64 | - | func (r Response) Apply(ctx echo.Context) error { | |
| 65 | - | if r.Push != "" { | |
| 66 | - | ctx.Response().Header().Set(HeaderPush, r.Push) | |
| 67 | - | } | |
| 68 | - | if r.Redirect != "" { | |
| 69 | - | ctx.Response().Header().Set(HeaderRedirect, r.Redirect) | |
| 70 | - | } | |
| 71 | - | if r.Refresh { | |
| 72 | - | ctx.Response().Header().Set(HeaderRefresh, "true") | |
| 73 | - | } | |
| 74 | - | if r.Trigger != "" { | |
| 75 | - | ctx.Response().Header().Set(HeaderTrigger, r.Trigger) | |
| 76 | - | } | |
| 77 | - | if r.TriggerAfterSwap != "" { | |
| 78 | - | ctx.Response().Header().Set(HeaderTriggerAfterSwap, r.TriggerAfterSwap) | |
| 79 | - | } | |
| 80 | - | if r.TriggerAfterSettle != "" { | |
| 81 | - | ctx.Response().Header().Set(HeaderTriggerAfterSettle, r.TriggerAfterSettle) | |
| 82 | - | } | |
| 83 | - | if r.NoContent { | |
| 84 | - | ctx.Response().Status = http.StatusNoContent | |
| 85 | - | } | |
| 86 | - | ||
| 87 | - | return nil | |
| 88 | - | } | |
| @@ -1,88 +0,0 @@ | |||||
| 1 | - | package htmx | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "net/http" | |||
| 5 | - | ||||
| 6 | - | "github.com/labstack/echo/v4" | |||
| 7 | - | ) | |||
| 8 | - | ||||
| 9 | - | // Headers (https://htmx.org/docs/#requests) | |||
| 10 | - | const ( | |||
| 11 | - | HeaderRequest = "HX-Request" | |||
| 12 | - | HeaderBoosted = "HX-Boosted" | |||
| 13 | - | HeaderTrigger = "HX-Trigger" | |||
| 14 | - | HeaderTriggerName = "HX-Trigger-Name" | |||
| 15 | - | HeaderTriggerAfterSwap = "HX-Trigger-After-Swap" | |||
| 16 | - | HeaderTriggerAfterSettle = "HX-Trigger-After-Settle" | |||
| 17 | - | HeaderTarget = "HX-Target" | |||
| 18 | - | HeaderPrompt = "HX-Prompt" | |||
| 19 | - | HeaderPush = "HX-Push" | |||
| 20 | - | HeaderRedirect = "HX-Redirect" | |||
| 21 | - | HeaderRefresh = "HX-Refresh" | |||
| 22 | - | // Custom header | |||
| 23 | - | HeaderFragment = "HX-Fragment" | |||
| 24 | - | ) | |||
| 25 | - | ||||
| 26 | - | type ( | |||
| 27 | - | // Request contains data that HTMX provides during requests | |||
| 28 | - | Request struct { | |||
| 29 | - | Enabled bool | |||
| 30 | - | Boosted bool | |||
| 31 | - | Trigger string | |||
| 32 | - | TriggerName string | |||
| 33 | - | Target string | |||
| 34 | - | Prompt string | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | // Response contain data that the server can communicate back to HTMX | |||
| 38 | - | Response struct { | |||
| 39 | - | Push string | |||
| 40 | - | Redirect string | |||
| 41 | - | Refresh bool | |||
| 42 | - | Trigger string | |||
| 43 | - | TriggerAfterSwap string | |||
| 44 | - | TriggerAfterSettle string | |||
| 45 | - | NoContent bool | |||
| 46 | - | } | |||
| 47 | - | ) | |||
| 48 | - | ||||
| 49 | - | // GetRequest extracts HTMX data from the request | |||
| 50 | - | func GetRequest(ctx echo.Context) (Request, error) { | |||
| 51 | - | request := Request{ | |||
| 52 | - | Enabled: ctx.Request().Header.Get(HeaderRequest) == "true", | |||
| 53 | - | Boosted: ctx.Request().Header.Get(HeaderBoosted) == "true", | |||
| 54 | - | Trigger: ctx.Request().Header.Get(HeaderTrigger), | |||
| 55 | - | TriggerName: ctx.Request().Header.Get(HeaderTriggerName), | |||
| 56 | - | Target: ctx.Request().Header.Get(HeaderTarget), | |||
| 57 | - | Prompt: ctx.Request().Header.Get(HeaderPrompt), | |||
| 58 | - | } | |||
| 59 | - | ||||
| 60 | - | return request, nil | |||
| 61 | - | } | |||
| 62 | - | ||||
| 63 | - | // Apply applies data from a Response to a server response | |||
| 64 | - | func (r Response) Apply(ctx echo.Context) error { | |||
| 65 | - | if r.Push != "" { | |||
| 66 | - | ctx.Response().Header().Set(HeaderPush, r.Push) | |||
| 67 | - | } | |||
| 68 | - | if r.Redirect != "" { | |||
| 69 | - | ctx.Response().Header().Set(HeaderRedirect, r.Redirect) | |||
| 70 | - | } | |||
| 71 | - | if r.Refresh { | |||
| 72 | - | ctx.Response().Header().Set(HeaderRefresh, "true") | |||
| 73 | - | } | |||
| 74 | - | if r.Trigger != "" { | |||
| 75 | - | ctx.Response().Header().Set(HeaderTrigger, r.Trigger) | |||
| 76 | - | } | |||
| 77 | - | if r.TriggerAfterSwap != "" { | |||
| 78 | - | ctx.Response().Header().Set(HeaderTriggerAfterSwap, r.TriggerAfterSwap) | |||
| 79 | - | } | |||
| 80 | - | if r.TriggerAfterSettle != "" { | |||
| 81 | - | ctx.Response().Header().Set(HeaderTriggerAfterSettle, r.TriggerAfterSettle) | |||
| 82 | - | } | |||
| 83 | - | if r.NoContent { | |||
| 84 | - | ctx.Response().Status = http.StatusNoContent | |||
| 85 | - | } | |||
| 86 | - | ||||
| 87 | - | return nil | |||
| 88 | - | } | |||
D pkg/scripts/install-traefik.go +0 -226
| @@ -1,226 +0,0 @@ | |||
| 1 | - | package scripts | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "errors" | |
| 5 | - | "fmt" | |
| 6 | - | "os" | |
| 7 | - | ||
| 8 | - | root "gogs.bnema.dev/gordon-echo" | |
| 9 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |
| 10 | - | "gogs.bnema.dev/gordon-echo/pkg/docker" | |
| 11 | - | "gogs.bnema.dev/gordon-echo/pkg/templating" | |
| 12 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |
| 13 | - | ) | |
| 14 | - | ||
| 15 | - | var path string | |
| 16 | - | ||
| 17 | - | const ( | |
| 18 | - | traefikPath = "traefik" | |
| 19 | - | traefikVolumePath = "traefik/volume" | |
| 20 | - | traefikLog = "logs/traefik.log" | |
| 21 | - | acmeJSON = "traefik/volume/acme.json" | |
| 22 | - | networkName = "totototo" | |
| 23 | - | ) | |
| 24 | - | ||
| 25 | - | func init() { | |
| 26 | - | path = app.GetBuildDir() + "/" | |
| 27 | - | } | |
| 28 | - | ||
| 29 | - | type InstallationResult struct { | |
| 30 | - | Success bool | |
| 31 | - | Message string | |
| 32 | - | Err error | |
| 33 | - | } | |
| 34 | - | ||
| 35 | - | func InstallTraefik(topDomain, adminEmail string, a *app.App) (bool, error) { | |
| 36 | - | // 1 - Files/directories creation | |
| 37 | - | // 1.1 - Check if directory traefik exists, if not create it | |
| 38 | - | if err := createDirectory(path + traefikPath); err != nil { | |
| 39 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 40 | - | return false, err | |
| 41 | - | } else { | |
| 42 | - | fmt.Println("traefik directory created") | |
| 43 | - | } | |
| 44 | - | ||
| 45 | - | // 1.2 - Check if directory traefik/volume exists, if not create it | |
| 46 | - | if err := createDirectory(path + traefikVolumePath); err != nil { | |
| 47 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 48 | - | return false, err | |
| 49 | - | } else { | |
| 50 | - | fmt.Println("traefik/volume directory created") | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | // 1.3 - touch traefik/traefik.log | |
| 54 | - | if err := createTraefikLogFile(path + traefikLog); err != nil { | |
| 55 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 56 | - | return false, err | |
| 57 | - | } else { | |
| 58 | - | a.AppLogger.Log(utils.INFO, "Traefik log file created") | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | // 1.4 - touch traefik/volume/acme.json | |
| 62 | - | if err := createACMEfile(path + acmeJSON); err != nil { | |
| 63 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 64 | - | return false, err | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | a.AppLogger.Log(utils.INFO, "Traefik acme.json file created") | |
| 68 | - | ||
| 69 | - | // 2 - Render the yml files | |
| 70 | - | // 2.1 - Render traefik-compose.yml | |
| 71 | - | traefikComposeContent, err := renderTemplate("traefik-compose.goyml", topDomain, adminEmail, a.AppLogger) | |
| 72 | - | if err != nil { | |
| 73 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 74 | - | return false, err | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | a.AppLogger.Log(utils.INFO, "Traefik compose file rendered") | |
| 78 | - | ||
| 79 | - | // 2.2 - Save traefik-compose.yml | |
| 80 | - | if err := saveFile(path+"docker-compose.yml", traefikComposeContent); err != nil { | |
| 81 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 82 | - | return false, err | |
| 83 | - | } else { | |
| 84 | - | fmt.Println("docker-compose.yml saved") | |
| 85 | - | } | |
| 86 | - | ||
| 87 | - | // 2.3 - Render traefik.yml | |
| 88 | - | traefikContent, err := renderTemplate("traefik.goyml", topDomain, adminEmail, a.AppLogger) | |
| 89 | - | if err != nil { | |
| 90 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 91 | - | return false, err | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | a.AppLogger.Log(utils.INFO, "Traefik file rendered") | |
| 95 | - | ||
| 96 | - | // 2.4 - Save traefik.yml | |
| 97 | - | if err := saveFile(path+"traefik.yml", traefikContent); err != nil { | |
| 98 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 99 | - | return false, err | |
| 100 | - | } | |
| 101 | - | ||
| 102 | - | a.AppLogger.Log(utils.INFO, "Traefik file saved") | |
| 103 | - | ||
| 104 | - | // 3 - Check if traefik is running | |
| 105 | - | if err := checkIfTraefikIsRunning(a); err != nil { | |
| 106 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 107 | - | return false, err | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | // // 4 - Create network | |
| 111 | - | // if err := docker.CreateNetwork(networkName, a); err != nil { | |
| 112 | - | // a.AppLogger.Log(utils.ERROR, err) | |
| 113 | - | // return false, err | |
| 114 | - | // } | |
| 115 | - | ||
| 116 | - | // 5 - Create traefik container | |
| 117 | - | if err := createTraefikContainer(path+"docker-compose.yml", a); err != nil { | |
| 118 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 119 | - | return false, err | |
| 120 | - | } | |
| 121 | - | ||
| 122 | - | // 6 - Ping traefik | |
| 123 | - | if err := pingWithCURL(topDomain); err != nil { | |
| 124 | - | a.AppLogger.Log(utils.ERROR, err) | |
| 125 | - | return false, err | |
| 126 | - | } | |
| 127 | - | ||
| 128 | - | return true, nil | |
| 129 | - | } | |
| 130 | - | ||
| 131 | - | func renderTemplate(filename, topDomain, adminEmail string, logger *utils.Logger) (string, error) { | |
| 132 | - | modelFS := root.ModelFS | |
| 133 | - | if modelFS == nil { | |
| 134 | - | return "", errors.New("modelFS is nil") | |
| 135 | - | } | |
| 136 | - | ||
| 137 | - | renderer, err := templating.GetTXTRenderer(filename, modelFS, logger) | |
| 138 | - | if err != nil { | |
| 139 | - | return "", err | |
| 140 | - | } | |
| 141 | - | ||
| 142 | - | var contentData map[string]interface{} | |
| 143 | - | switch filename { | |
| 144 | - | case "traefik-compose.goyml": | |
| 145 | - | contentData = map[string]interface{}{"topdomain": topDomain} | |
| 146 | - | case "traefik.goyml": | |
| 147 | - | contentData = map[string]interface{}{ | |
| 148 | - | "admin": map[string]string{ | |
| 149 | - | "email": adminEmail, | |
| 150 | - | }, | |
| 151 | - | } | |
| 152 | - | default: | |
| 153 | - | return "", errors.New("invalid template filename") | |
| 154 | - | } | |
| 155 | - | ||
| 156 | - | return renderer.TXTRender(contentData) | |
| 157 | - | } | |
| 158 | - | ||
| 159 | - | func createDirectory(dir string) error { | |
| 160 | - | if _, err := os.Stat(dir); os.IsNotExist(err) { | |
| 161 | - | return os.Mkdir(dir, 0755) | |
| 162 | - | } | |
| 163 | - | return nil | |
| 164 | - | } | |
| 165 | - | func saveFile(filePath, content string) error { | |
| 166 | - | f, err := os.Create(filePath) | |
| 167 | - | if err != nil { | |
| 168 | - | return err | |
| 169 | - | } | |
| 170 | - | defer f.Close() | |
| 171 | - | _, err = f.WriteString(content) | |
| 172 | - | if err != nil { | |
| 173 | - | fmt.Println("Error while writing to file: ", err) | |
| 174 | - | return err | |
| 175 | - | } | |
| 176 | - | return err | |
| 177 | - | } | |
| 178 | - | ||
| 179 | - | func closeFile(f *os.File) error { | |
| 180 | - | // defer f.Close() | |
| 181 | - | return f.Close() | |
| 182 | - | } | |
| 183 | - | ||
| 184 | - | func createACMEfile(filePath string) error { | |
| 185 | - | f, err := os.Create(filePath) | |
| 186 | - | if err != nil { | |
| 187 | - | return err | |
| 188 | - | } | |
| 189 | - | defer f.Close() | |
| 190 | - | return f.Chmod(0600) | |
| 191 | - | } | |
| 192 | - | ||
| 193 | - | func createTraefikLogFile(filePath string) error { | |
| 194 | - | f, err := os.Create(filePath) | |
| 195 | - | if err != nil { | |
| 196 | - | return err | |
| 197 | - | } | |
| 198 | - | defer f.Close() | |
| 199 | - | return f.Chmod(0600) | |
| 200 | - | } | |
| 201 | - | ||
| 202 | - | func checkIfTraefikIsRunning(a *app.App) error { | |
| 203 | - | // Check if docker is running | |
| 204 | - | if err := docker.CheckIfDockerIsRunning(a); err != nil { | |
| 205 | - | return err | |
| 206 | - | } | |
| 207 | - | // Check if a container named traefik already exists | |
| 208 | - | if err := docker.ContainerExists("traefik", a); err != nil { | |
| 209 | - | return err | |
| 210 | - | } | |
| 211 | - | ||
| 212 | - | return nil | |
| 213 | - | } | |
| 214 | - | ||
| 215 | - | func createTraefikContainer(filePath string, a *app.App) error { | |
| 216 | - | err := docker.CreateContainerFromComposeFile(filePath, a) | |
| 217 | - | if err != nil { | |
| 218 | - | return err | |
| 219 | - | } | |
| 220 | - | ||
| 221 | - | return nil | |
| 222 | - | } | |
| 223 | - | ||
| 224 | - | func pingWithCURL(topDomain string) error { | |
| 225 | - | return nil | |
| 226 | - | } | |
| @@ -1,226 +0,0 @@ | |||||
| 1 | - | package scripts | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "errors" | |||
| 5 | - | "fmt" | |||
| 6 | - | "os" | |||
| 7 | - | ||||
| 8 | - | root "gogs.bnema.dev/gordon-echo" | |||
| 9 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |||
| 10 | - | "gogs.bnema.dev/gordon-echo/pkg/docker" | |||
| 11 | - | "gogs.bnema.dev/gordon-echo/pkg/templating" | |||
| 12 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |||
| 13 | - | ) | |||
| 14 | - | ||||
| 15 | - | var path string | |||
| 16 | - | ||||
| 17 | - | const ( | |||
| 18 | - | traefikPath = "traefik" | |||
| 19 | - | traefikVolumePath = "traefik/volume" | |||
| 20 | - | traefikLog = "logs/traefik.log" | |||
| 21 | - | acmeJSON = "traefik/volume/acme.json" | |||
| 22 | - | networkName = "totototo" | |||
| 23 | - | ) | |||
| 24 | - | ||||
| 25 | - | func init() { | |||
| 26 | - | path = app.GetBuildDir() + "/" | |||
| 27 | - | } | |||
| 28 | - | ||||
| 29 | - | type InstallationResult struct { | |||
| 30 | - | Success bool | |||
| 31 | - | Message string | |||
| 32 | - | Err error | |||
| 33 | - | } | |||
| 34 | - | ||||
| 35 | - | func InstallTraefik(topDomain, adminEmail string, a *app.App) (bool, error) { | |||
| 36 | - | // 1 - Files/directories creation | |||
| 37 | - | // 1.1 - Check if directory traefik exists, if not create it | |||
| 38 | - | if err := createDirectory(path + traefikPath); err != nil { | |||
| 39 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 40 | - | return false, err | |||
| 41 | - | } else { | |||
| 42 | - | fmt.Println("traefik directory created") | |||
| 43 | - | } | |||
| 44 | - | ||||
| 45 | - | // 1.2 - Check if directory traefik/volume exists, if not create it | |||
| 46 | - | if err := createDirectory(path + traefikVolumePath); err != nil { | |||
| 47 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 48 | - | return false, err | |||
| 49 | - | } else { | |||
| 50 | - | fmt.Println("traefik/volume directory created") | |||
| 51 | - | } | |||
| 52 | - | ||||
| 53 | - | // 1.3 - touch traefik/traefik.log | |||
| 54 | - | if err := createTraefikLogFile(path + traefikLog); err != nil { | |||
| 55 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 56 | - | return false, err | |||
| 57 | - | } else { | |||
| 58 | - | a.AppLogger.Log(utils.INFO, "Traefik log file created") | |||
| 59 | - | } | |||
| 60 | - | ||||
| 61 | - | // 1.4 - touch traefik/volume/acme.json | |||
| 62 | - | if err := createACMEfile(path + acmeJSON); err != nil { | |||
| 63 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 64 | - | return false, err | |||
| 65 | - | } | |||
| 66 | - | ||||
| 67 | - | a.AppLogger.Log(utils.INFO, "Traefik acme.json file created") | |||
| 68 | - | ||||
| 69 | - | // 2 - Render the yml files | |||
| 70 | - | // 2.1 - Render traefik-compose.yml | |||
| 71 | - | traefikComposeContent, err := renderTemplate("traefik-compose.goyml", topDomain, adminEmail, a.AppLogger) | |||
| 72 | - | if err != nil { | |||
| 73 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 74 | - | return false, err | |||
| 75 | - | } | |||
| 76 | - | ||||
| 77 | - | a.AppLogger.Log(utils.INFO, "Traefik compose file rendered") | |||
| 78 | - | ||||
| 79 | - | // 2.2 - Save traefik-compose.yml | |||
| 80 | - | if err := saveFile(path+"docker-compose.yml", traefikComposeContent); err != nil { | |||
| 81 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 82 | - | return false, err | |||
| 83 | - | } else { | |||
| 84 | - | fmt.Println("docker-compose.yml saved") | |||
| 85 | - | } | |||
| 86 | - | ||||
| 87 | - | // 2.3 - Render traefik.yml | |||
| 88 | - | traefikContent, err := renderTemplate("traefik.goyml", topDomain, adminEmail, a.AppLogger) | |||
| 89 | - | if err != nil { | |||
| 90 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 91 | - | return false, err | |||
| 92 | - | } | |||
| 93 | - | ||||
| 94 | - | a.AppLogger.Log(utils.INFO, "Traefik file rendered") | |||
| 95 | - | ||||
| 96 | - | // 2.4 - Save traefik.yml | |||
| 97 | - | if err := saveFile(path+"traefik.yml", traefikContent); err != nil { | |||
| 98 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 99 | - | return false, err | |||
| 100 | - | } | |||
| 101 | - | ||||
| 102 | - | a.AppLogger.Log(utils.INFO, "Traefik file saved") | |||
| 103 | - | ||||
| 104 | - | // 3 - Check if traefik is running | |||
| 105 | - | if err := checkIfTraefikIsRunning(a); err != nil { | |||
| 106 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 107 | - | return false, err | |||
| 108 | - | } | |||
| 109 | - | ||||
| 110 | - | // // 4 - Create network | |||
| 111 | - | // if err := docker.CreateNetwork(networkName, a); err != nil { | |||
| 112 | - | // a.AppLogger.Log(utils.ERROR, err) | |||
| 113 | - | // return false, err | |||
| 114 | - | // } | |||
| 115 | - | ||||
| 116 | - | // 5 - Create traefik container | |||
| 117 | - | if err := createTraefikContainer(path+"docker-compose.yml", a); err != nil { | |||
| 118 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 119 | - | return false, err | |||
| 120 | - | } | |||
| 121 | - | ||||
| 122 | - | // 6 - Ping traefik | |||
| 123 | - | if err := pingWithCURL(topDomain); err != nil { | |||
| 124 | - | a.AppLogger.Log(utils.ERROR, err) | |||
| 125 | - | return false, err | |||
| 126 | - | } | |||
| 127 | - | ||||
| 128 | - | return true, nil | |||
| 129 | - | } | |||
| 130 | - | ||||
| 131 | - | func renderTemplate(filename, topDomain, adminEmail string, logger *utils.Logger) (string, error) { | |||
| 132 | - | modelFS := root.ModelFS | |||
| 133 | - | if modelFS == nil { | |||
| 134 | - | return "", errors.New("modelFS is nil") | |||
| 135 | - | } | |||
| 136 | - | ||||
| 137 | - | renderer, err := templating.GetTXTRenderer(filename, modelFS, logger) | |||
| 138 | - | if err != nil { | |||
| 139 | - | return "", err | |||
| 140 | - | } | |||
| 141 | - | ||||
| 142 | - | var contentData map[string]interface{} | |||
| 143 | - | switch filename { | |||
| 144 | - | case "traefik-compose.goyml": | |||
| 145 | - | contentData = map[string]interface{}{"topdomain": topDomain} | |||
| 146 | - | case "traefik.goyml": | |||
| 147 | - | contentData = map[string]interface{}{ | |||
| 148 | - | "admin": map[string]string{ | |||
| 149 | - | "email": adminEmail, | |||
| 150 | - | }, | |||
| 151 | - | } | |||
| 152 | - | default: | |||
| 153 | - | return "", errors.New("invalid template filename") | |||
| 154 | - | } | |||
| 155 | - | ||||
| 156 | - | return renderer.TXTRender(contentData) | |||
| 157 | - | } | |||
| 158 | - | ||||
| 159 | - | func createDirectory(dir string) error { | |||
| 160 | - | if _, err := os.Stat(dir); os.IsNotExist(err) { | |||
| 161 | - | return os.Mkdir(dir, 0755) | |||
| 162 | - | } | |||
| 163 | - | return nil | |||
| 164 | - | } | |||
| 165 | - | func saveFile(filePath, content string) error { | |||
| 166 | - | f, err := os.Create(filePath) | |||
| 167 | - | if err != nil { | |||
| 168 | - | return err | |||
| 169 | - | } | |||
| 170 | - | defer f.Close() | |||
| 171 | - | _, err = f.WriteString(content) | |||
| 172 | - | if err != nil { | |||
| 173 | - | fmt.Println("Error while writing to file: ", err) | |||
| 174 | - | return err | |||
| 175 | - | } | |||
| 176 | - | return err | |||
| 177 | - | } | |||
| 178 | - | ||||
| 179 | - | func closeFile(f *os.File) error { | |||
| 180 | - | // defer f.Close() | |||
| 181 | - | return f.Close() | |||
| 182 | - | } | |||
| 183 | - | ||||
| 184 | - | func createACMEfile(filePath string) error { | |||
| 185 | - | f, err := os.Create(filePath) | |||
| 186 | - | if err != nil { | |||
| 187 | - | return err | |||
| 188 | - | } | |||
| 189 | - | defer f.Close() | |||
| 190 | - | return f.Chmod(0600) | |||
| 191 | - | } | |||
| 192 | - | ||||
| 193 | - | func createTraefikLogFile(filePath string) error { | |||
| 194 | - | f, err := os.Create(filePath) | |||
| 195 | - | if err != nil { | |||
| 196 | - | return err | |||
| 197 | - | } | |||
| 198 | - | defer f.Close() | |||
| 199 | - | return f.Chmod(0600) | |||
| 200 | - | } | |||
| 201 | - | ||||
| 202 | - | func checkIfTraefikIsRunning(a *app.App) error { | |||
| 203 | - | // Check if docker is running | |||
| 204 | - | if err := docker.CheckIfDockerIsRunning(a); err != nil { | |||
| 205 | - | return err | |||
| 206 | - | } | |||
| 207 | - | // Check if a container named traefik already exists | |||
| 208 | - | if err := docker.ContainerExists("traefik", a); err != nil { | |||
| 209 | - | return err | |||
| 210 | - | } | |||
| 211 | - | ||||
| 212 | - | return nil | |||
| 213 | - | } | |||
| 214 | - | ||||
| 215 | - | func createTraefikContainer(filePath string, a *app.App) error { | |||
| 216 | - | err := docker.CreateContainerFromComposeFile(filePath, a) | |||
| 217 | - | if err != nil { | |||
| 218 | - | return err | |||
| 219 | - | } | |||
| 220 | - | ||||
| 221 | - | return nil | |||
| 222 | - | } | |||
| 223 | - | ||||
| 224 | - | func pingWithCURL(topDomain string) error { | |||
| 225 | - | return nil | |||
| 226 | - | } | |||
D pkg/services/sqlite.go +0 -1
| @@ -1,1 +0,0 @@ | |||
| 1 | - | package services | |
| @@ -1,1 +0,0 @@ | |||||
| 1 | - | package services | |||
D pkg/templating/html-render.go +0 -54
| @@ -1,54 +0,0 @@ | |||
| 1 | - | package templating | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bytes" | |
| 5 | - | "html/template" | |
| 6 | - | "io/fs" | |
| 7 | - | ||
| 8 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | type Renderer struct { | |
| 12 | - | Template *template.Template | |
| 13 | - | ParseError error | |
| 14 | - | } | |
| 15 | - | ||
| 16 | - | // Render function renders the template with the given data | |
| 17 | - | func (r *Renderer) Render(data interface{}, a *app.App) (string, error) { | |
| 18 | - | if r.ParseError != nil { | |
| 19 | - | a.AppLogger.Error().Err(r.ParseError).Msg("Failed to parse template") | |
| 20 | - | return "", r.ParseError | |
| 21 | - | } | |
| 22 | - | if r.Template == nil { | |
| 23 | - | a.AppLogger.Error().Msg("Template is nil") | |
| 24 | - | return "", r.ParseError | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | buf := new(bytes.Buffer) | |
| 28 | - | err := r.Template.Execute(buf, data) | |
| 29 | - | if err != nil { | |
| 30 | - | a.AppLogger.Error().Err(err).Msg("Failed to execute template") | |
| 31 | - | return "", err | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | return buf.String(), nil | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | // GetRenderer function returns a new Renderer instance | |
| 38 | - | func GetHTMLRenderer(filename string, fs fs.FS, a *app.App) (*Renderer, error) { | |
| 39 | - | // Check if the file exists in the provided fs.FS using fs.Open | |
| 40 | - | file, err := fs.Open(filename) | |
| 41 | - | if err != nil { | |
| 42 | - | a.AppLogger.Error().Err(err).Msg("Failed to open file") | |
| 43 | - | return nil, err | |
| 44 | - | } | |
| 45 | - | file.Close() // Close the file after checking | |
| 46 | - | tmpl, err := template.New(filename).ParseFS(fs, filename) | |
| 47 | - | if err != nil { | |
| 48 | - | a.AppLogger.Error().Err(err).Msg("Failed to parse template") | |
| 49 | - | return nil, err | |
| 50 | - | } | |
| 51 | - | return &Renderer{ | |
| 52 | - | Template: tmpl, | |
| 53 | - | }, nil | |
| 54 | - | } | |
| @@ -1,54 +0,0 @@ | |||||
| 1 | - | package templating | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "bytes" | |||
| 5 | - | "html/template" | |||
| 6 | - | "io/fs" | |||
| 7 | - | ||||
| 8 | - | "gogs.bnema.dev/gordon-echo/internal/app" | |||
| 9 | - | ) | |||
| 10 | - | ||||
| 11 | - | type Renderer struct { | |||
| 12 | - | Template *template.Template | |||
| 13 | - | ParseError error | |||
| 14 | - | } | |||
| 15 | - | ||||
| 16 | - | // Render function renders the template with the given data | |||
| 17 | - | func (r *Renderer) Render(data interface{}, a *app.App) (string, error) { | |||
| 18 | - | if r.ParseError != nil { | |||
| 19 | - | a.AppLogger.Error().Err(r.ParseError).Msg("Failed to parse template") | |||
| 20 | - | return "", r.ParseError | |||
| 21 | - | } | |||
| 22 | - | if r.Template == nil { | |||
| 23 | - | a.AppLogger.Error().Msg("Template is nil") | |||
| 24 | - | return "", r.ParseError | |||
| 25 | - | } | |||
| 26 | - | ||||
| 27 | - | buf := new(bytes.Buffer) | |||
| 28 | - | err := r.Template.Execute(buf, data) | |||
| 29 | - | if err != nil { | |||
| 30 | - | a.AppLogger.Error().Err(err).Msg("Failed to execute template") | |||
| 31 | - | return "", err | |||
| 32 | - | } | |||
| 33 | - | ||||
| 34 | - | return buf.String(), nil | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | // GetRenderer function returns a new Renderer instance | |||
| 38 | - | func GetHTMLRenderer(filename string, fs fs.FS, a *app.App) (*Renderer, error) { | |||
| 39 | - | // Check if the file exists in the provided fs.FS using fs.Open | |||
| 40 | - | file, err := fs.Open(filename) | |||
| 41 | - | if err != nil { | |||
| 42 | - | a.AppLogger.Error().Err(err).Msg("Failed to open file") | |||
| 43 | - | return nil, err | |||
| 44 | - | } | |||
| 45 | - | file.Close() // Close the file after checking | |||
| 46 | - | tmpl, err := template.New(filename).ParseFS(fs, filename) | |||
| 47 | - | if err != nil { | |||
| 48 | - | a.AppLogger.Error().Err(err).Msg("Failed to parse template") | |||
| 49 | - | return nil, err | |||
| 50 | - | } | |||
| 51 | - | return &Renderer{ | |||
| 52 | - | Template: tmpl, | |||
| 53 | - | }, nil | |||
| 54 | - | } | |||
D pkg/templating/models/registry-compose.goyml +0 -15
| @@ -1,15 +0,0 @@ | |||
| 1 | - | registry: | |
| 2 | - | restart: always | |
| 3 | - | image: registry:2 | |
| 4 | - | ports: | |
| 5 | - | - 5000:5000 | |
| 6 | - | environment: | |
| 7 | - | REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt | |
| 8 | - | REGISTRY_HTTP_TLS_KEY: /certs/domain.key | |
| 9 | - | REGISTRY_AUTH: htpasswd | |
| 10 | - | REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd | |
| 11 | - | REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm | |
| 12 | - | volumes: | |
| 13 | - | - /path/data:/var/lib/registry | |
| 14 | - | - /path/certs:/certs | |
| 15 | - | - /path/auth:/auth⏎̸ | |
| @@ -1,15 +0,0 @@ | |||||
| 1 | - | registry: | |||
| 2 | - | restart: always | |||
| 3 | - | image: registry:2 | |||
| 4 | - | ports: | |||
| 5 | - | - 5000:5000 | |||
| 6 | - | environment: | |||
| 7 | - | REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt | |||
| 8 | - | REGISTRY_HTTP_TLS_KEY: /certs/domain.key | |||
| 9 | - | REGISTRY_AUTH: htpasswd | |||
| 10 | - | REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd | |||
| 11 | - | REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm | |||
| 12 | - | volumes: | |||
| 13 | - | - /path/data:/var/lib/registry | |||
| 14 | - | - /path/certs:/certs | |||
| 15 | - | - /path/auth:/auth⏎̸ | |||
D pkg/templating/models/traefik-compose.goyml +0 -29
| @@ -1,29 +0,0 @@ | |||
| 1 | - | version: "3" | |
| 2 | - | ||
| 3 | - | services: | |
| 4 | - | traefik: | |
| 5 | - | image: traefik:v2.10 | |
| 6 | - | container_name: traefik | |
| 7 | - | environment: | |
| 8 | - | - COMPOSE_PROJECT_NAME=traefik | |
| 9 | - | restart: unless-stopped | |
| 10 | - | ports: | |
| 11 | - | - "80:80" | |
| 12 | - | - "443:443" | |
| 13 | - | volumes: | |
| 14 | - | - /var/run/docker.sock:/var/run/docker.sock:ro | |
| 15 | - | - ./traefik.yml:/etc/traefik/traefik.yml | |
| 16 | - | - ./volume/acme.json:/acme.json | |
| 17 | - | - ./logs/traefik.log:/var/log/traefik/traefik.log | |
| 18 | - | labels: | |
| 19 | - | - "traefik.enable=true" | |
| 20 | - | - "traefik.http.routers.api.rule=Host(`traefik.{{ .topdomain }}`)" | |
| 21 | - | - "traefik.http.routers.api.service=api@internal" | |
| 22 | - | - "traefik.http.routers.api.entrypoints=websecure" | |
| 23 | - | - "traefik.http.routers.api.tls.certresolver=myresolver" | |
| 24 | - | - "traefik.http.services.api.loadbalancer.server.port=8080" | |
| 25 | - | networks: | |
| 26 | - | - exposed | |
| 27 | - | ||
| 28 | - | networks: | |
| 29 | - | exposed:⏎̸ | |
| @@ -1,29 +0,0 @@ | |||||
| 1 | - | version: "3" | |||
| 2 | - | ||||
| 3 | - | services: | |||
| 4 | - | traefik: | |||
| 5 | - | image: traefik:v2.10 | |||
| 6 | - | container_name: traefik | |||
| 7 | - | environment: | |||
| 8 | - | - COMPOSE_PROJECT_NAME=traefik | |||
| 9 | - | restart: unless-stopped | |||
| 10 | - | ports: | |||
| 11 | - | - "80:80" | |||
| 12 | - | - "443:443" | |||
| 13 | - | volumes: | |||
| 14 | - | - /var/run/docker.sock:/var/run/docker.sock:ro | |||
| 15 | - | - ./traefik.yml:/etc/traefik/traefik.yml | |||
| 16 | - | - ./volume/acme.json:/acme.json | |||
| 17 | - | - ./logs/traefik.log:/var/log/traefik/traefik.log | |||
| 18 | - | labels: | |||
| 19 | - | - "traefik.enable=true" | |||
| 20 | - | - "traefik.http.routers.api.rule=Host(`traefik.{{ .topdomain }}`)" | |||
| 21 | - | - "traefik.http.routers.api.service=api@internal" | |||
| 22 | - | - "traefik.http.routers.api.entrypoints=websecure" | |||
| 23 | - | - "traefik.http.routers.api.tls.certresolver=myresolver" | |||
| 24 | - | - "traefik.http.services.api.loadbalancer.server.port=8080" | |||
| 25 | - | networks: | |||
| 26 | - | - exposed | |||
| 27 | - | ||||
| 28 | - | networks: | |||
| 29 | - | exposed:⏎̸ | |||
D pkg/templating/models/traefik.goyml +0 -30
| @@ -1,30 +0,0 @@ | |||
| 1 | - | api: | |
| 2 | - | dashboard: false | |
| 3 | - | ||
| 4 | - | entryPoints: | |
| 5 | - | web: | |
| 6 | - | address: ":80" | |
| 7 | - | websecure: | |
| 8 | - | address: ":443" | |
| 9 | - | ||
| 10 | - | providers: | |
| 11 | - | docker: | |
| 12 | - | endpoint: "unix:///var/run/docker.sock" | |
| 13 | - | exposedByDefault: false | |
| 14 | - | ||
| 15 | - | certificatesResolvers: | |
| 16 | - | myresolver: | |
| 17 | - | acme: | |
| 18 | - | email: {{ .admin.email }} | |
| 19 | - | storage: acme.json | |
| 20 | - | httpChallenge: | |
| 21 | - | entryPoint: web | |
| 22 | - | ||
| 23 | - | log: | |
| 24 | - | level: "DEBUG" # Set the desired log level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC | |
| 25 | - | filePath: "/var/log/traefik/traefik.log" | |
| 26 | - | ||
| 27 | - | accessLog: {} | |
| 28 | - | ||
| 29 | - | ping: | |
| 30 | - | entryPoint: web ⏎̸ | |
| @@ -1,30 +0,0 @@ | |||||
| 1 | - | api: | |||
| 2 | - | dashboard: false | |||
| 3 | - | ||||
| 4 | - | entryPoints: | |||
| 5 | - | web: | |||
| 6 | - | address: ":80" | |||
| 7 | - | websecure: | |||
| 8 | - | address: ":443" | |||
| 9 | - | ||||
| 10 | - | providers: | |||
| 11 | - | docker: | |||
| 12 | - | endpoint: "unix:///var/run/docker.sock" | |||
| 13 | - | exposedByDefault: false | |||
| 14 | - | ||||
| 15 | - | certificatesResolvers: | |||
| 16 | - | myresolver: | |||
| 17 | - | acme: | |||
| 18 | - | email: {{ .admin.email }} | |||
| 19 | - | storage: acme.json | |||
| 20 | - | httpChallenge: | |||
| 21 | - | entryPoint: web | |||
| 22 | - | ||||
| 23 | - | log: | |||
| 24 | - | level: "DEBUG" # Set the desired log level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC | |||
| 25 | - | filePath: "/var/log/traefik/traefik.log" | |||
| 26 | - | ||||
| 27 | - | accessLog: {} | |||
| 28 | - | ||||
| 29 | - | ping: | |||
| 30 | - | entryPoint: web ⏎̸ | |||
D pkg/templating/sanitizer.go +0 -22
| @@ -1,22 +0,0 @@ | |||
| 1 | - | package templating | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | ||
| 6 | - | "github.com/microcosm-cc/bluemonday" | |
| 7 | - | ) | |
| 8 | - | ||
| 9 | - | // SanitizeHTML sanitizes HTML input using bluemonday package and returns an error if sanitization altered the input. | |
| 10 | - | func SanitizeHTML(htmlInput string) (string, error) { | |
| 11 | - | // Create a new UGC policy | |
| 12 | - | p := bluemonday.UGCPolicy() | |
| 13 | - | ||
| 14 | - | // Sanitize the input HTML | |
| 15 | - | sanitized := p.Sanitize(htmlInput) | |
| 16 | - | ||
| 17 | - | if sanitized != htmlInput { | |
| 18 | - | return sanitized, fmt.Errorf("HTML input was altered during sanitization") | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | return sanitized, nil | |
| 22 | - | } | |
| @@ -1,22 +0,0 @@ | |||||
| 1 | - | package templating | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "fmt" | |||
| 5 | - | ||||
| 6 | - | "github.com/microcosm-cc/bluemonday" | |||
| 7 | - | ) | |||
| 8 | - | ||||
| 9 | - | // SanitizeHTML sanitizes HTML input using bluemonday package and returns an error if sanitization altered the input. | |||
| 10 | - | func SanitizeHTML(htmlInput string) (string, error) { | |||
| 11 | - | // Create a new UGC policy | |||
| 12 | - | p := bluemonday.UGCPolicy() | |||
| 13 | - | ||||
| 14 | - | // Sanitize the input HTML | |||
| 15 | - | sanitized := p.Sanitize(htmlInput) | |||
| 16 | - | ||||
| 17 | - | if sanitized != htmlInput { | |||
| 18 | - | return sanitized, fmt.Errorf("HTML input was altered during sanitization") | |||
| 19 | - | } | |||
| 20 | - | ||||
| 21 | - | return sanitized, nil | |||
| 22 | - | } | |||
D pkg/templating/txt-render.go +0 -63
| @@ -1,63 +0,0 @@ | |||
| 1 | - | package templating | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bytes" | |
| 5 | - | "errors" | |
| 6 | - | "fmt" | |
| 7 | - | "io/fs" | |
| 8 | - | "text/template" | |
| 9 | - | ||
| 10 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |
| 11 | - | ) | |
| 12 | - | ||
| 13 | - | type TXTRenderer struct { | |
| 14 | - | Models *template.Template | |
| 15 | - | Format string | |
| 16 | - | ParseError error | |
| 17 | - | Logger *utils.Logger | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | // Render function renders the template with the given data | |
| 21 | - | func (r *TXTRenderer) TXTRender(data interface{}) (string, error) { | |
| 22 | - | if r.ParseError != nil { | |
| 23 | - | r.Logger.Error().Err(r.ParseError).Msg("Parse error") | |
| 24 | - | return "", r.ParseError | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | if r.Models == nil { | |
| 28 | - | err := errors.New("invalid or nil model") | |
| 29 | - | r.Logger.Error().Err(r.ParseError).Msg("Parse error") | |
| 30 | - | ||
| 31 | - | return "", err | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | buf := new(bytes.Buffer) | |
| 35 | - | err := r.Models.Execute(buf, data) | |
| 36 | - | if err != nil { | |
| 37 | - | r.Logger.Error().Err(err).Msg("Render error") | |
| 38 | - | return "", err | |
| 39 | - | } | |
| 40 | - | ||
| 41 | - | return buf.String(), nil | |
| 42 | - | } | |
| 43 | - | ||
| 44 | - | // GetRenderer function returns a new Renderer instance | |
| 45 | - | func GetTXTRenderer(filename string, fs fs.FS, logger *utils.Logger) (*TXTRenderer, error) { | |
| 46 | - | fmt.Println(filename) | |
| 47 | - | // Check if the file exists in the provided fs.FS using fs.Open | |
| 48 | - | file, err := fs.Open(filename) | |
| 49 | - | if err != nil { | |
| 50 | - | logger.Error().Err(err).Msg("Template or model '%s' not found" + filename) | |
| 51 | - | return nil, err | |
| 52 | - | } | |
| 53 | - | file.Close() // Close the file after checking | |
| 54 | - | mdls, err := template.New(filename).ParseFS(fs, filename) | |
| 55 | - | if err != nil { | |
| 56 | - | logger.Error().Err(err).Msg("Failed to parse template") | |
| 57 | - | return nil, err | |
| 58 | - | } | |
| 59 | - | return &TXTRenderer{ | |
| 60 | - | Models: mdls, | |
| 61 | - | Logger: logger, | |
| 62 | - | }, nil | |
| 63 | - | } | |
| @@ -1,63 +0,0 @@ | |||||
| 1 | - | package templating | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "bytes" | |||
| 5 | - | "errors" | |||
| 6 | - | "fmt" | |||
| 7 | - | "io/fs" | |||
| 8 | - | "text/template" | |||
| 9 | - | ||||
| 10 | - | "gogs.bnema.dev/gordon-echo/pkg/utils" | |||
| 11 | - | ) | |||
| 12 | - | ||||
| 13 | - | type TXTRenderer struct { | |||
| 14 | - | Models *template.Template | |||
| 15 | - | Format string | |||
| 16 | - | ParseError error | |||
| 17 | - | Logger *utils.Logger | |||
| 18 | - | } | |||
| 19 | - | ||||
| 20 | - | // Render function renders the template with the given data | |||
| 21 | - | func (r *TXTRenderer) TXTRender(data interface{}) (string, error) { | |||
| 22 | - | if r.ParseError != nil { | |||
| 23 | - | r.Logger.Error().Err(r.ParseError).Msg("Parse error") | |||
| 24 | - | return "", r.ParseError | |||
| 25 | - | } | |||
| 26 | - | ||||
| 27 | - | if r.Models == nil { | |||
| 28 | - | err := errors.New("invalid or nil model") | |||
| 29 | - | r.Logger.Error().Err(r.ParseError).Msg("Parse error") | |||
| 30 | - | ||||
| 31 | - | return "", err | |||
| 32 | - | } | |||
| 33 | - | ||||
| 34 | - | buf := new(bytes.Buffer) | |||
| 35 | - | err := r.Models.Execute(buf, data) | |||
| 36 | - | if err != nil { | |||
| 37 | - | r.Logger.Error().Err(err).Msg("Render error") | |||
| 38 | - | return "", err | |||
| 39 | - | } | |||
| 40 | - | ||||
| 41 | - | return buf.String(), nil | |||
| 42 | - | } | |||
| 43 | - | ||||
| 44 | - | // GetRenderer function returns a new Renderer instance | |||
| 45 | - | func GetTXTRenderer(filename string, fs fs.FS, logger *utils.Logger) (*TXTRenderer, error) { | |||
| 46 | - | fmt.Println(filename) | |||
| 47 | - | // Check if the file exists in the provided fs.FS using fs.Open | |||
| 48 | - | file, err := fs.Open(filename) | |||
| 49 | - | if err != nil { | |||
| 50 | - | logger.Error().Err(err).Msg("Template or model '%s' not found" + filename) | |||
| 51 | - | return nil, err | |||
| 52 | - | } | |||
| 53 | - | file.Close() // Close the file after checking | |||
| 54 | - | mdls, err := template.New(filename).ParseFS(fs, filename) | |||
| 55 | - | if err != nil { | |||
| 56 | - | logger.Error().Err(err).Msg("Failed to parse template") | |||
| 57 | - | return nil, err | |||
| 58 | - | } | |||
| 59 | - | return &TXTRenderer{ | |||
| 60 | - | Models: mdls, | |||
| 61 | - | Logger: logger, | |||
| 62 | - | }, nil | |||
| 63 | - | } | |||
D pkg/utils/detect-first-launch.go +0 -14
| @@ -1,14 +0,0 @@ | |||
| 1 | - | package utils | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "os" | |
| 5 | - | ) | |
| 6 | - | ||
| 7 | - | // IsConfigFilePresent checks if it's the first launch of the app based on the existence of gordon.yml | |
| 8 | - | func IsConfigFilePresent() bool { | |
| 9 | - | // Check if gordon.yml exists at the root of the project | |
| 10 | - | if _, err := os.Stat("gordon.yml"); os.IsNotExist(err) { | |
| 11 | - | return true | |
| 12 | - | } | |
| 13 | - | return false | |
| 14 | - | } | |
| @@ -1,14 +0,0 @@ | |||||
| 1 | - | package utils | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "os" | |||
| 5 | - | ) | |||
| 6 | - | ||||
| 7 | - | // IsConfigFilePresent checks if it's the first launch of the app based on the existence of gordon.yml | |||
| 8 | - | func IsConfigFilePresent() bool { | |||
| 9 | - | // Check if gordon.yml exists at the root of the project | |||
| 10 | - | if _, err := os.Stat("gordon.yml"); os.IsNotExist(err) { | |||
| 11 | - | return true | |||
| 12 | - | } | |||
| 13 | - | return false | |||
| 14 | - | } | |||
D pkg/utils/logger.go +0 -282
| @@ -1,282 +0,0 @@ | |||
| 1 | - | package utils | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bufio" | |
| 5 | - | "fmt" | |
| 6 | - | "io" | |
| 7 | - | "os" | |
| 8 | - | ||
| 9 | - | "github.com/labstack/gommon/log" | |
| 10 | - | "github.com/rs/zerolog" | |
| 11 | - | ) | |
| 12 | - | ||
| 13 | - | const ( | |
| 14 | - | DEBUG LogLevel = "debug" | |
| 15 | - | INFO LogLevel = "info" | |
| 16 | - | WARN LogLevel = "warn" | |
| 17 | - | ERROR LogLevel = "error" | |
| 18 | - | FATAL LogLevel = "fatal" | |
| 19 | - | PANIC LogLevel = "panic" | |
| 20 | - | ) | |
| 21 | - | ||
| 22 | - | type LogLevel string | |
| 23 | - | ||
| 24 | - | type Logger struct { | |
| 25 | - | zerolog.Logger | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | type stdoutCapture struct { | |
| 29 | - | logger *Logger | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | type EchoLoggerWrapper struct { | |
| 33 | - | *Logger | |
| 34 | - | output io.Writer | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | // LogFunc is a function that logs a message with optional context. | |
| 38 | - | type LogFunc func() *zerolog.Event | |
| 39 | - | ||
| 40 | - | // Map log levels to zerolog log functions | |
| 41 | - | func (l *Logger) getLogFunc(level LogLevel) LogFunc { | |
| 42 | - | switch level { | |
| 43 | - | case DEBUG: | |
| 44 | - | return func() *zerolog.Event { return l.Logger.Debug() } | |
| 45 | - | case INFO: | |
| 46 | - | return func() *zerolog.Event { return l.Logger.Info() } | |
| 47 | - | case WARN: | |
| 48 | - | return func() *zerolog.Event { return l.Logger.Warn() } | |
| 49 | - | case ERROR: | |
| 50 | - | return func() *zerolog.Event { return l.Logger.Error() } | |
| 51 | - | case FATAL: | |
| 52 | - | return func() *zerolog.Event { return l.Logger.Fatal() } | |
| 53 | - | case PANIC: | |
| 54 | - | return func() *zerolog.Event { return l.Logger.Panic() } | |
| 55 | - | default: | |
| 56 | - | return func() *zerolog.Event { return l.Logger.Info() } // default to INFO if unknown level | |
| 57 | - | } | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | func (l *Logger) Log(level LogLevel, i ...interface{}) { | |
| 61 | - | logFunc := l.getLogFunc(level) | |
| 62 | - | logFunc().Msg(fmt.Sprint(i...)) | |
| 63 | - | } | |
| 64 | - | ||
| 65 | - | func NewLogger() *Logger { | |
| 66 | - | writer := zerolog.ConsoleWriter{Out: os.Stdout} | |
| 67 | - | zl := zerolog.New(writer).With().Timestamp().Logger() | |
| 68 | - | return &Logger{zl} | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | func (s *stdoutCapture) Write(p []byte) (n int, err error) { | |
| 72 | - | s.logger.Info().Str("type", "app").Msg(string(p)) | |
| 73 | - | return len(p), nil | |
| 74 | - | } | |
| 75 | - | ||
| 76 | - | func CaptureSTDOUT(logger *Logger) { | |
| 77 | - | // Create a reading and writing pipe | |
| 78 | - | reader, writer, _ := os.Pipe() | |
| 79 | - | ||
| 80 | - | // Replace os.Stdout with the writer end of the pipe | |
| 81 | - | os.Stdout = writer | |
| 82 | - | ||
| 83 | - | // Start a goroutine to read from the pipe | |
| 84 | - | go func() { | |
| 85 | - | scanner := bufio.NewScanner(reader) | |
| 86 | - | for scanner.Scan() { | |
| 87 | - | text := scanner.Text() | |
| 88 | - | logger.Debug().Str("type", "app").Msg(text) | |
| 89 | - | } | |
| 90 | - | }() | |
| 91 | - | } | |
| 92 | - | ||
| 93 | - | func NewEchoLoggerWrapper(l *Logger) *EchoLoggerWrapper { | |
| 94 | - | return &EchoLoggerWrapper{Logger: l, output: os.Stdout} | |
| 95 | - | } | |
| 96 | - | ||
| 97 | - | func (l *Logger) SetOutput(w io.Writer) *Logger { | |
| 98 | - | l.Logger = l.Logger.Output(w) | |
| 99 | - | return l | |
| 100 | - | } | |
| 101 | - | ||
| 102 | - | func (l *Logger) SetLevel(level LogLevel) *Logger { | |
| 103 | - | l.Logger = l.Logger.Level(getZerologLevel(level)) | |
| 104 | - | return l | |
| 105 | - | } | |
| 106 | - | ||
| 107 | - | func getZerologLevel(level LogLevel) zerolog.Level { | |
| 108 | - | switch level { | |
| 109 | - | case DEBUG: | |
| 110 | - | return zerolog.DebugLevel | |
| 111 | - | case INFO: | |
| 112 | - | return zerolog.InfoLevel | |
| 113 | - | case WARN: | |
| 114 | - | return zerolog.WarnLevel | |
| 115 | - | case ERROR: | |
| 116 | - | return zerolog.ErrorLevel | |
| 117 | - | case FATAL: | |
| 118 | - | return zerolog.FatalLevel | |
| 119 | - | case PANIC: | |
| 120 | - | return zerolog.PanicLevel | |
| 121 | - | default: | |
| 122 | - | return zerolog.InfoLevel // default to INFO if unknown level | |
| 123 | - | } | |
| 124 | - | } | |
| 125 | - | ||
| 126 | - | func CreateLogsDir(logDir string) error { | |
| 127 | - | if _, err := os.Stat(logDir); os.IsNotExist(err) { | |
| 128 | - | err := os.MkdirAll(logDir, 0755) | |
| 129 | - | if err != nil { | |
| 130 | - | return err | |
| 131 | - | } | |
| 132 | - | } | |
| 133 | - | return nil | |
| 134 | - | } | |
| 135 | - | ||
| 136 | - | func (l *EchoLoggerWrapper) Output() io.Writer { | |
| 137 | - | return l.output | |
| 138 | - | } | |
| 139 | - | ||
| 140 | - | func (l *EchoLoggerWrapper) SetOutput(w io.Writer) { | |
| 141 | - | l.output = w | |
| 142 | - | } | |
| 143 | - | ||
| 144 | - | func (l *EchoLoggerWrapper) Prefix() string { | |
| 145 | - | return "" | |
| 146 | - | } | |
| 147 | - | ||
| 148 | - | func (l *EchoLoggerWrapper) SetPrefix(p string) { | |
| 149 | - | // do nothing | |
| 150 | - | } | |
| 151 | - | ||
| 152 | - | func (l *EchoLoggerWrapper) Level() log.Lvl { | |
| 153 | - | return log.Lvl(l.Logger.GetLevel()) | |
| 154 | - | } | |
| 155 | - | ||
| 156 | - | // SetHeader | |
| 157 | - | func (l *EchoLoggerWrapper) SetHeader(h string) { | |
| 158 | - | // do nothing | |
| 159 | - | } | |
| 160 | - | ||
| 161 | - | func (l *EchoLoggerWrapper) SetLevel(log.Lvl) { | |
| 162 | - | // do nothing | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | // SetEvent creates a new event with the given level and message. | |
| 166 | - | func (l *EchoLoggerWrapper) SetEvent(level zerolog.Level, msg string) *zerolog.Event { | |
| 167 | - | //cannot use utils.NewEchoLoggerWrapper(app.HTTPLogger) (value of type *utils.EchoLoggerWrapper) as echo.Logger value in assignment: *utils.EchoLoggerWrapper does not implement echo.Logger (wrong type for method Error) | |
| 168 | - | return l.Logger.WithLevel(level).Str("message", msg) | |
| 169 | - | } | |
| 170 | - | ||
| 171 | - | func (l *EchoLoggerWrapper) OutputStdout() bool { | |
| 172 | - | return true | |
| 173 | - | } | |
| 174 | - | ||
| 175 | - | // Helper function for methods that accept variadic slice of interfaces | |
| 176 | - | func (l *EchoLoggerWrapper) LogWithArgs(level LogLevel, i ...interface{}) { | |
| 177 | - | l.Logger.Log(level, i...) | |
| 178 | - | } | |
| 179 | - | ||
| 180 | - | // Helper function for methods that accept a format string and arguments | |
| 181 | - | func (l *EchoLoggerWrapper) LogWithFormat(level LogLevel, format string, args ...interface{}) { | |
| 182 | - | l.Logger.Log(level, fmt.Sprintf(format, args...)) | |
| 183 | - | } | |
| 184 | - | ||
| 185 | - | // Helper function for methods that accept a log.JSON object | |
| 186 | - | func (l *EchoLoggerWrapper) LogWithJSON(level LogLevel, j log.JSON) { | |
| 187 | - | l.Logger.Log(level, j) | |
| 188 | - | } | |
| 189 | - | ||
| 190 | - | // Debug | |
| 191 | - | func (l *EchoLoggerWrapper) Debug(i ...interface{}) { | |
| 192 | - | l.LogWithArgs(DEBUG, i...) | |
| 193 | - | } | |
| 194 | - | ||
| 195 | - | func (l *EchoLoggerWrapper) Debugf(format string, args ...interface{}) { | |
| 196 | - | l.LogWithFormat(DEBUG, format, args...) | |
| 197 | - | } | |
| 198 | - | ||
| 199 | - | func (l *EchoLoggerWrapper) Debugj(j log.JSON) { | |
| 200 | - | l.LogWithJSON(DEBUG, j) | |
| 201 | - | } | |
| 202 | - | ||
| 203 | - | // Info | |
| 204 | - | func (l *EchoLoggerWrapper) Info(i ...interface{}) { | |
| 205 | - | l.LogWithArgs(INFO, i...) | |
| 206 | - | } | |
| 207 | - | ||
| 208 | - | func (l *EchoLoggerWrapper) Infof(format string, args ...interface{}) { | |
| 209 | - | l.LogWithFormat(INFO, format, args...) | |
| 210 | - | } | |
| 211 | - | ||
| 212 | - | func (l *EchoLoggerWrapper) Infoj(j log.JSON) { | |
| 213 | - | l.LogWithJSON(INFO, j) | |
| 214 | - | } | |
| 215 | - | ||
| 216 | - | // Warn | |
| 217 | - | ||
| 218 | - | func (l *EchoLoggerWrapper) Warn(i ...interface{}) { | |
| 219 | - | l.LogWithArgs(WARN, i...) | |
| 220 | - | } | |
| 221 | - | ||
| 222 | - | func (l *EchoLoggerWrapper) Warnf(format string, args ...interface{}) { | |
| 223 | - | l.LogWithFormat(WARN, format, args...) | |
| 224 | - | } | |
| 225 | - | ||
| 226 | - | func (l *EchoLoggerWrapper) Warnj(j log.JSON) { | |
| 227 | - | l.LogWithJSON(WARN, j) | |
| 228 | - | } | |
| 229 | - | ||
| 230 | - | // Error | |
| 231 | - | func (l *EchoLoggerWrapper) Error(i ...interface{}) { | |
| 232 | - | l.LogWithArgs(ERROR, i...) | |
| 233 | - | } | |
| 234 | - | ||
| 235 | - | func (l *EchoLoggerWrapper) Errorf(format string, args ...interface{}) { | |
| 236 | - | l.LogWithFormat(ERROR, format, args...) | |
| 237 | - | } | |
| 238 | - | ||
| 239 | - | func (l *EchoLoggerWrapper) Errorj(j log.JSON) { | |
| 240 | - | l.LogWithJSON(ERROR, j) | |
| 241 | - | } | |
| 242 | - | ||
| 243 | - | // Fatal | |
| 244 | - | func (l *EchoLoggerWrapper) Fatal(i ...interface{}) { | |
| 245 | - | l.LogWithArgs(FATAL, i...) | |
| 246 | - | } | |
| 247 | - | ||
| 248 | - | func (l *EchoLoggerWrapper) Fatalf(format string, args ...interface{}) { | |
| 249 | - | l.LogWithFormat(FATAL, format, args...) | |
| 250 | - | } | |
| 251 | - | ||
| 252 | - | func (l *EchoLoggerWrapper) Fatalj(j log.JSON) { | |
| 253 | - | l.LogWithJSON(FATAL, j) | |
| 254 | - | } | |
| 255 | - | ||
| 256 | - | // Panic | |
| 257 | - | ||
| 258 | - | func (l *EchoLoggerWrapper) Panic(i ...interface{}) { | |
| 259 | - | l.LogWithArgs(PANIC, i...) | |
| 260 | - | } | |
| 261 | - | ||
| 262 | - | func (l *EchoLoggerWrapper) Panicf(format string, args ...interface{}) { | |
| 263 | - | l.LogWithFormat(PANIC, format, args...) | |
| 264 | - | } | |
| 265 | - | ||
| 266 | - | func (l *EchoLoggerWrapper) Panicj(j log.JSON) { | |
| 267 | - | l.LogWithJSON(PANIC, j) | |
| 268 | - | } | |
| 269 | - | ||
| 270 | - | ||
| 271 | - | ||
| 272 | - | func (l *EchoLoggerWrapper) Print(i ...interface{}) { | |
| 273 | - | l.LogWithArgs(INFO, i...) | |
| 274 | - | } | |
| 275 | - | ||
| 276 | - | func (l *EchoLoggerWrapper) Printf(format string, args ...interface{}) { | |
| 277 | - | l.LogWithFormat(INFO, format, args...) | |
| 278 | - | } | |
| 279 | - | ||
| 280 | - | func (l *EchoLoggerWrapper) Printj(j log.JSON) { | |
| 281 | - | l.LogWithJSON(INFO, j) | |
| 282 | - | } | |
| @@ -1,282 +0,0 @@ | |||||
| 1 | - | package utils | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "bufio" | |||
| 5 | - | "fmt" | |||
| 6 | - | "io" | |||
| 7 | - | "os" | |||
| 8 | - | ||||
| 9 | - | "github.com/labstack/gommon/log" | |||
| 10 | - | "github.com/rs/zerolog" | |||
| 11 | - | ) | |||
| 12 | - | ||||
| 13 | - | const ( | |||
| 14 | - | DEBUG LogLevel = "debug" | |||
| 15 | - | INFO LogLevel = "info" | |||
| 16 | - | WARN LogLevel = "warn" | |||
| 17 | - | ERROR LogLevel = "error" | |||
| 18 | - | FATAL LogLevel = "fatal" | |||
| 19 | - | PANIC LogLevel = "panic" | |||
| 20 | - | ) | |||
| 21 | - | ||||
| 22 | - | type LogLevel string | |||
| 23 | - | ||||
| 24 | - | type Logger struct { | |||
| 25 | - | zerolog.Logger | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | type stdoutCapture struct { | |||
| 29 | - | logger *Logger | |||
| 30 | - | } | |||
| 31 | - | ||||
| 32 | - | type EchoLoggerWrapper struct { | |||
| 33 | - | *Logger | |||
| 34 | - | output io.Writer | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | // LogFunc is a function that logs a message with optional context. | |||
| 38 | - | type LogFunc func() *zerolog.Event | |||
| 39 | - | ||||
| 40 | - | // Map log levels to zerolog log functions | |||
| 41 | - | func (l *Logger) getLogFunc(level LogLevel) LogFunc { | |||
| 42 | - | switch level { | |||
| 43 | - | case DEBUG: | |||
| 44 | - | return func() *zerolog.Event { return l.Logger.Debug() } | |||
| 45 | - | case INFO: | |||
| 46 | - | return func() *zerolog.Event { return l.Logger.Info() } | |||
| 47 | - | case WARN: | |||
| 48 | - | return func() *zerolog.Event { return l.Logger.Warn() } | |||
| 49 | - | case ERROR: | |||
| 50 | - | return func() *zerolog.Event { return l.Logger.Error() } | |||
| 51 | - | case FATAL: | |||
| 52 | - | return func() *zerolog.Event { return l.Logger.Fatal() } | |||
| 53 | - | case PANIC: | |||
| 54 | - | return func() *zerolog.Event { return l.Logger.Panic() } | |||
| 55 | - | default: | |||
| 56 | - | return func() *zerolog.Event { return l.Logger.Info() } // default to INFO if unknown level | |||
| 57 | - | } | |||
| 58 | - | } | |||
| 59 | - | ||||
| 60 | - | func (l *Logger) Log(level LogLevel, i ...interface{}) { | |||
| 61 | - | logFunc := l.getLogFunc(level) | |||
| 62 | - | logFunc().Msg(fmt.Sprint(i...)) | |||
| 63 | - | } | |||
| 64 | - | ||||
| 65 | - | func NewLogger() *Logger { | |||
| 66 | - | writer := zerolog.ConsoleWriter{Out: os.Stdout} | |||
| 67 | - | zl := zerolog.New(writer).With().Timestamp().Logger() | |||
| 68 | - | return &Logger{zl} | |||
| 69 | - | } | |||
| 70 | - | ||||
| 71 | - | func (s *stdoutCapture) Write(p []byte) (n int, err error) { | |||
| 72 | - | s.logger.Info().Str("type", "app").Msg(string(p)) | |||
| 73 | - | return len(p), nil | |||
| 74 | - | } | |||
| 75 | - | ||||
| 76 | - | func CaptureSTDOUT(logger *Logger) { | |||
| 77 | - | // Create a reading and writing pipe | |||
| 78 | - | reader, writer, _ := os.Pipe() | |||
| 79 | - | ||||
| 80 | - | // Replace os.Stdout with the writer end of the pipe | |||
| 81 | - | os.Stdout = writer | |||
| 82 | - | ||||
| 83 | - | // Start a goroutine to read from the pipe | |||
| 84 | - | go func() { | |||
| 85 | - | scanner := bufio.NewScanner(reader) | |||
| 86 | - | for scanner.Scan() { | |||
| 87 | - | text := scanner.Text() | |||
| 88 | - | logger.Debug().Str("type", "app").Msg(text) | |||
| 89 | - | } | |||
| 90 | - | }() | |||
| 91 | - | } | |||
| 92 | - | ||||
| 93 | - | func NewEchoLoggerWrapper(l *Logger) *EchoLoggerWrapper { | |||
| 94 | - | return &EchoLoggerWrapper{Logger: l, output: os.Stdout} | |||
| 95 | - | } | |||
| 96 | - | ||||
| 97 | - | func (l *Logger) SetOutput(w io.Writer) *Logger { | |||
| 98 | - | l.Logger = l.Logger.Output(w) | |||
| 99 | - | return l | |||
| 100 | - | } | |||
| 101 | - | ||||
| 102 | - | func (l *Logger) SetLevel(level LogLevel) *Logger { | |||
| 103 | - | l.Logger = l.Logger.Level(getZerologLevel(level)) | |||
| 104 | - | return l | |||
| 105 | - | } | |||
| 106 | - | ||||
| 107 | - | func getZerologLevel(level LogLevel) zerolog.Level { | |||
| 108 | - | switch level { | |||
| 109 | - | case DEBUG: | |||
| 110 | - | return zerolog.DebugLevel | |||
| 111 | - | case INFO: | |||
| 112 | - | return zerolog.InfoLevel | |||
| 113 | - | case WARN: | |||
| 114 | - | return zerolog.WarnLevel | |||
| 115 | - | case ERROR: | |||
| 116 | - | return zerolog.ErrorLevel | |||
| 117 | - | case FATAL: | |||
| 118 | - | return zerolog.FatalLevel | |||
| 119 | - | case PANIC: | |||
| 120 | - | return zerolog.PanicLevel | |||
| 121 | - | default: | |||
| 122 | - | return zerolog.InfoLevel // default to INFO if unknown level | |||
| 123 | - | } | |||
| 124 | - | } | |||
| 125 | - | ||||
| 126 | - | func CreateLogsDir(logDir string) error { | |||
| 127 | - | if _, err := os.Stat(logDir); os.IsNotExist(err) { | |||
| 128 | - | err := os.MkdirAll(logDir, 0755) | |||
| 129 | - | if err != nil { | |||
| 130 | - | return err | |||
| 131 | - | } | |||
| 132 | - | } | |||
| 133 | - | return nil | |||
| 134 | - | } | |||
| 135 | - | ||||
| 136 | - | func (l *EchoLoggerWrapper) Output() io.Writer { | |||
| 137 | - | return l.output | |||
| 138 | - | } | |||
| 139 | - | ||||
| 140 | - | func (l *EchoLoggerWrapper) SetOutput(w io.Writer) { | |||
| 141 | - | l.output = w | |||
| 142 | - | } | |||
| 143 | - | ||||
| 144 | - | func (l *EchoLoggerWrapper) Prefix() string { | |||
| 145 | - | return "" | |||
| 146 | - | } | |||
| 147 | - | ||||
| 148 | - | func (l *EchoLoggerWrapper) SetPrefix(p string) { | |||
| 149 | - | // do nothing | |||
| 150 | - | } | |||
| 151 | - | ||||
| 152 | - | func (l *EchoLoggerWrapper) Level() log.Lvl { | |||
| 153 | - | return log.Lvl(l.Logger.GetLevel()) | |||
| 154 | - | } | |||
| 155 | - | ||||
| 156 | - | // SetHeader | |||
| 157 | - | func (l *EchoLoggerWrapper) SetHeader(h string) { | |||
| 158 | - | // do nothing | |||
| 159 | - | } | |||
| 160 | - | ||||
| 161 | - | func (l *EchoLoggerWrapper) SetLevel(log.Lvl) { | |||
| 162 | - | // do nothing | |||
| 163 | - | } | |||
| 164 | - | ||||
| 165 | - | // SetEvent creates a new event with the given level and message. | |||
| 166 | - | func (l *EchoLoggerWrapper) SetEvent(level zerolog.Level, msg string) *zerolog.Event { | |||
| 167 | - | //cannot use utils.NewEchoLoggerWrapper(app.HTTPLogger) (value of type *utils.EchoLoggerWrapper) as echo.Logger value in assignment: *utils.EchoLoggerWrapper does not implement echo.Logger (wrong type for method Error) | |||
| 168 | - | return l.Logger.WithLevel(level).Str("message", msg) | |||
| 169 | - | } | |||
| 170 | - | ||||
| 171 | - | func (l *EchoLoggerWrapper) OutputStdout() bool { | |||
| 172 | - | return true | |||
| 173 | - | } | |||
| 174 | - | ||||
| 175 | - | // Helper function for methods that accept variadic slice of interfaces | |||
| 176 | - | func (l *EchoLoggerWrapper) LogWithArgs(level LogLevel, i ...interface{}) { | |||
| 177 | - | l.Logger.Log(level, i...) | |||
| 178 | - | } | |||
| 179 | - | ||||
| 180 | - | // Helper function for methods that accept a format string and arguments | |||
| 181 | - | func (l *EchoLoggerWrapper) LogWithFormat(level LogLevel, format string, args ...interface{}) { | |||
| 182 | - | l.Logger.Log(level, fmt.Sprintf(format, args...)) | |||
| 183 | - | } | |||
| 184 | - | ||||
| 185 | - | // Helper function for methods that accept a log.JSON object | |||
| 186 | - | func (l *EchoLoggerWrapper) LogWithJSON(level LogLevel, j log.JSON) { | |||
| 187 | - | l.Logger.Log(level, j) | |||
| 188 | - | } | |||
| 189 | - | ||||
| 190 | - | // Debug | |||
| 191 | - | func (l *EchoLoggerWrapper) Debug(i ...interface{}) { | |||
| 192 | - | l.LogWithArgs(DEBUG, i...) | |||
| 193 | - | } | |||
| 194 | - | ||||
| 195 | - | func (l *EchoLoggerWrapper) Debugf(format string, args ...interface{}) { | |||
| 196 | - | l.LogWithFormat(DEBUG, format, args...) | |||
| 197 | - | } | |||
| 198 | - | ||||
| 199 | - | func (l *EchoLoggerWrapper) Debugj(j log.JSON) { | |||
| 200 | - | l.LogWithJSON(DEBUG, j) | |||
| 201 | - | } | |||
| 202 | - | ||||
| 203 | - | // Info | |||
| 204 | - | func (l *EchoLoggerWrapper) Info(i ...interface{}) { | |||
| 205 | - | l.LogWithArgs(INFO, i...) | |||
| 206 | - | } | |||
| 207 | - | ||||
| 208 | - | func (l *EchoLoggerWrapper) Infof(format string, args ...interface{}) { | |||
| 209 | - | l.LogWithFormat(INFO, format, args...) | |||
| 210 | - | } | |||
| 211 | - | ||||
| 212 | - | func (l *EchoLoggerWrapper) Infoj(j log.JSON) { | |||
| 213 | - | l.LogWithJSON(INFO, j) | |||
| 214 | - | } | |||
| 215 | - | ||||
| 216 | - | // Warn | |||
| 217 | - | ||||
| 218 | - | func (l *EchoLoggerWrapper) Warn(i ...interface{}) { | |||
| 219 | - | l.LogWithArgs(WARN, i...) | |||
| 220 | - | } | |||
| 221 | - | ||||
| 222 | - | func (l *EchoLoggerWrapper) Warnf(format string, args ...interface{}) { | |||
| 223 | - | l.LogWithFormat(WARN, format, args...) | |||
| 224 | - | } | |||
| 225 | - | ||||
| 226 | - | func (l *EchoLoggerWrapper) Warnj(j log.JSON) { | |||
| 227 | - | l.LogWithJSON(WARN, j) | |||
| 228 | - | } | |||
| 229 | - | ||||
| 230 | - | // Error | |||
| 231 | - | func (l *EchoLoggerWrapper) Error(i ...interface{}) { | |||
| 232 | - | l.LogWithArgs(ERROR, i...) | |||
| 233 | - | } | |||
| 234 | - | ||||
| 235 | - | func (l *EchoLoggerWrapper) Errorf(format string, args ...interface{}) { | |||
| 236 | - | l.LogWithFormat(ERROR, format, args...) | |||
| 237 | - | } | |||
| 238 | - | ||||
| 239 | - | func (l *EchoLoggerWrapper) Errorj(j log.JSON) { | |||
| 240 | - | l.LogWithJSON(ERROR, j) | |||
| 241 | - | } | |||
| 242 | - | ||||
| 243 | - | // Fatal | |||
| 244 | - | func (l *EchoLoggerWrapper) Fatal(i ...interface{}) { | |||
| 245 | - | l.LogWithArgs(FATAL, i...) | |||
| 246 | - | } | |||
| 247 | - | ||||
| 248 | - | func (l *EchoLoggerWrapper) Fatalf(format string, args ...interface{}) { | |||
| 249 | - | l.LogWithFormat(FATAL, format, args...) | |||
| 250 | - | } | |||
| 251 | - | ||||
| 252 | - | func (l *EchoLoggerWrapper) Fatalj(j log.JSON) { | |||
| 253 | - | l.LogWithJSON(FATAL, j) | |||
| 254 | - | } | |||
| 255 | - | ||||
| 256 | - | // Panic | |||
| 257 | - | ||||
| 258 | - | func (l *EchoLoggerWrapper) Panic(i ...interface{}) { | |||
| 259 | - | l.LogWithArgs(PANIC, i...) | |||
| 260 | - | } | |||
| 261 | - | ||||
| 262 | - | func (l *EchoLoggerWrapper) Panicf(format string, args ...interface{}) { | |||
| 263 | - | l.LogWithFormat(PANIC, format, args...) | |||
| 264 | - | } | |||
| 265 | - | ||||
| 266 | - | func (l *EchoLoggerWrapper) Panicj(j log.JSON) { | |||
| 267 | - | l.LogWithJSON(PANIC, j) | |||
| 268 | - | } | |||
| 269 | - | ||||
| 270 | - | ||||
| 271 | - | ||||
| 272 | - | func (l *EchoLoggerWrapper) Print(i ...interface{}) { | |||
| 273 | - | l.LogWithArgs(INFO, i...) | |||
| 274 | - | } | |||
| 275 | - | ||||
| 276 | - | func (l *EchoLoggerWrapper) Printf(format string, args ...interface{}) { | |||
| 277 | - | l.LogWithFormat(INFO, format, args...) | |||
| 278 | - | } | |||
| 279 | - | ||||
| 280 | - | func (l *EchoLoggerWrapper) Printj(j log.JSON) { | |||
| 281 | - | l.LogWithJSON(INFO, j) | |||
| 282 | - | } | |||
D pkg/utils/parser.go +0 -101
| @@ -1,101 +0,0 @@ | |||
| 1 | - | package utils | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "io" | |
| 6 | - | "io/fs" | |
| 7 | - | "os" | |
| 8 | - | ||
| 9 | - | "github.com/tidwall/gjson" | |
| 10 | - | "gopkg.in/yaml.v3" | |
| 11 | - | ) | |
| 12 | - | ||
| 13 | - | // Yaml Parser | |
| 14 | - | type LangData map[string]interface{} | |
| 15 | - | ||
| 16 | - | type StringsYamlData struct { | |
| 17 | - | StringsModel StringsModel `yaml:"strings_model"` | |
| 18 | - | EN LangData `yaml:"en"` | |
| 19 | - | FR LangData `yaml:"fr"` | |
| 20 | - | CurrentLang LangData | |
| 21 | - | } | |
| 22 | - | ||
| 23 | - | type ComposeModelData struct { | |
| 24 | - | ComposeModel ComposeModel `yaml:"compose_model"` | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | type StringsModel struct { | |
| 28 | - | Header string `yaml:"header"` | |
| 29 | - | Subheader string `yaml:"subheader"` | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | type ComposeModel struct { | |
| 33 | - | Header string `yaml:"header"` | |
| 34 | - | Subheader string `yaml:"subheader"` | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | // ReadDataFromYAML loads the data from the given YAML file from any filesystem | |
| 38 | - | func ReadDataFromYAML(fs fs.FS, filename string, out interface{}) error { | |
| 39 | - | // Open the YAML file | |
| 40 | - | file, err := fs.Open(filename) | |
| 41 | - | if err != nil { | |
| 42 | - | return fmt.Errorf("failed to open %s: %w", filename, err) | |
| 43 | - | } | |
| 44 | - | defer file.Close() | |
| 45 | - | ||
| 46 | - | // Read the content of the file | |
| 47 | - | content, err := io.ReadAll(file) | |
| 48 | - | if err != nil { | |
| 49 | - | return fmt.Errorf("failed to read %s: %w", filename, err) | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | // Unmarshal the YAML content into the out interface | |
| 53 | - | if err := yaml.Unmarshal(content, out); err != nil { | |
| 54 | - | return fmt.Errorf("failed to unmarshal %s: %w", filename, err) | |
| 55 | - | } | |
| 56 | - | ||
| 57 | - | return nil | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | // PopulateDataFromYAML retrieves the data from the YAML file and sets the appropriate language data | |
| 61 | - | func PopulateDataFromYAML(lang string, fs fs.FS, filename string, out interface{}) error { | |
| 62 | - | err := ReadDataFromYAML(fs, filename, out) | |
| 63 | - | if err != nil { | |
| 64 | - | return fmt.Errorf("failed to read data from YAML: %w", err) | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | // If the type assertion succeeds, we set the appropriate language data | |
| 68 | - | if data, ok := out.(*StringsYamlData); ok { | |
| 69 | - | switch lang { | |
| 70 | - | case "en": | |
| 71 | - | data.CurrentLang = data.EN | |
| 72 | - | case "fr": | |
| 73 | - | data.CurrentLang = data.FR | |
| 74 | - | default: // default to English if no match | |
| 75 | - | data.CurrentLang = data.EN | |
| 76 | - | } | |
| 77 | - | } | |
| 78 | - | ||
| 79 | - | return nil | |
| 80 | - | } | |
| 81 | - | ||
| 82 | - | // Json Parser | |
| 83 | - | ||
| 84 | - | // ReadDataFromJSONFile loads the data from the given JSON file | |
| 85 | - | func ReadDataFromJSONFile(filePath string) (string, error) { | |
| 86 | - | bytes, err := os.ReadFile(filePath) | |
| 87 | - | if err != nil { | |
| 88 | - | return "", err | |
| 89 | - | } | |
| 90 | - | return string(bytes), nil | |
| 91 | - | } | |
| 92 | - | ||
| 93 | - | // QueryJSON queries the provided JSON content based on the given gjson query and returns the results as a slice of strings | |
| 94 | - | func QueryJSON(jsonContent, query string) []string { | |
| 95 | - | results := gjson.GetMany(jsonContent, query) | |
| 96 | - | var values []string | |
| 97 | - | for _, result := range results { | |
| 98 | - | values = append(values, result.String()) | |
| 99 | - | } | |
| 100 | - | return values | |
| 101 | - | } | |
| @@ -1,101 +0,0 @@ | |||||
| 1 | - | package utils | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "fmt" | |||
| 5 | - | "io" | |||
| 6 | - | "io/fs" | |||
| 7 | - | "os" | |||
| 8 | - | ||||
| 9 | - | "github.com/tidwall/gjson" | |||
| 10 | - | "gopkg.in/yaml.v3" | |||
| 11 | - | ) | |||
| 12 | - | ||||
| 13 | - | // Yaml Parser | |||
| 14 | - | type LangData map[string]interface{} | |||
| 15 | - | ||||
| 16 | - | type StringsYamlData struct { | |||
| 17 | - | StringsModel StringsModel `yaml:"strings_model"` | |||
| 18 | - | EN LangData `yaml:"en"` | |||
| 19 | - | FR LangData `yaml:"fr"` | |||
| 20 | - | CurrentLang LangData | |||
| 21 | - | } | |||
| 22 | - | ||||
| 23 | - | type ComposeModelData struct { | |||
| 24 | - | ComposeModel ComposeModel `yaml:"compose_model"` | |||
| 25 | - | } | |||
| 26 | - | ||||
| 27 | - | type StringsModel struct { | |||
| 28 | - | Header string `yaml:"header"` | |||
| 29 | - | Subheader string `yaml:"subheader"` | |||
| 30 | - | } | |||
| 31 | - | ||||
| 32 | - | type ComposeModel struct { | |||
| 33 | - | Header string `yaml:"header"` | |||
| 34 | - | Subheader string `yaml:"subheader"` | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | // ReadDataFromYAML loads the data from the given YAML file from any filesystem | |||
| 38 | - | func ReadDataFromYAML(fs fs.FS, filename string, out interface{}) error { | |||
| 39 | - | // Open the YAML file | |||
| 40 | - | file, err := fs.Open(filename) | |||
| 41 | - | if err != nil { | |||
| 42 | - | return fmt.Errorf("failed to open %s: %w", filename, err) | |||
| 43 | - | } | |||
| 44 | - | defer file.Close() | |||
| 45 | - | ||||
| 46 | - | // Read the content of the file | |||
| 47 | - | content, err := io.ReadAll(file) | |||
| 48 | - | if err != nil { | |||
| 49 | - | return fmt.Errorf("failed to read %s: %w", filename, err) | |||
| 50 | - | } | |||
| 51 | - | ||||
| 52 | - | // Unmarshal the YAML content into the out interface | |||
| 53 | - | if err := yaml.Unmarshal(content, out); err != nil { | |||
| 54 | - | return fmt.Errorf("failed to unmarshal %s: %w", filename, err) | |||
| 55 | - | } | |||
| 56 | - | ||||
| 57 | - | return nil | |||
| 58 | - | } | |||
| 59 | - | ||||
| 60 | - | // PopulateDataFromYAML retrieves the data from the YAML file and sets the appropriate language data | |||
| 61 | - | func PopulateDataFromYAML(lang string, fs fs.FS, filename string, out interface{}) error { | |||
| 62 | - | err := ReadDataFromYAML(fs, filename, out) | |||
| 63 | - | if err != nil { | |||
| 64 | - | return fmt.Errorf("failed to read data from YAML: %w", err) | |||
| 65 | - | } | |||
| 66 | - | ||||
| 67 | - | // If the type assertion succeeds, we set the appropriate language data | |||
| 68 | - | if data, ok := out.(*StringsYamlData); ok { | |||
| 69 | - | switch lang { | |||
| 70 | - | case "en": | |||
| 71 | - | data.CurrentLang = data.EN | |||
| 72 | - | case "fr": | |||
| 73 | - | data.CurrentLang = data.FR | |||
| 74 | - | default: // default to English if no match | |||
| 75 | - | data.CurrentLang = data.EN | |||
| 76 | - | } | |||
| 77 | - | } | |||
| 78 | - | ||||
| 79 | - | return nil | |||
| 80 | - | } | |||
| 81 | - | ||||
| 82 | - | // Json Parser | |||
| 83 | - | ||||
| 84 | - | // ReadDataFromJSONFile loads the data from the given JSON file | |||
| 85 | - | func ReadDataFromJSONFile(filePath string) (string, error) { | |||
| 86 | - | bytes, err := os.ReadFile(filePath) | |||
| 87 | - | if err != nil { | |||
| 88 | - | return "", err | |||
| 89 | - | } | |||
| 90 | - | return string(bytes), nil | |||
| 91 | - | } | |||
| 92 | - | ||||
| 93 | - | // QueryJSON queries the provided JSON content based on the given gjson query and returns the results as a slice of strings | |||
| 94 | - | func QueryJSON(jsonContent, query string) []string { | |||
| 95 | - | results := gjson.GetMany(jsonContent, query) | |||
| 96 | - | var values []string | |||
| 97 | - | for _, result := range results { | |||
| 98 | - | values = append(values, result.String()) | |||
| 99 | - | } | |||
| 100 | - | return values | |||
| 101 | - | } | |||
D pkg/utils/tty.go +0 -65
| @@ -1,65 +0,0 @@ | |||
| 1 | - | package utils | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "net/http" | |
| 5 | - | "os" | |
| 6 | - | "os/exec" | |
| 7 | - | "os/signal" | |
| 8 | - | "syscall" | |
| 9 | - | "time" | |
| 10 | - | ||
| 11 | - | "github.com/rs/zerolog" | |
| 12 | - | ) | |
| 13 | - | ||
| 14 | - | // SetRawMode sets the terminal to raw mode. | |
| 15 | - | func SetRawMode() { | |
| 16 | - | cmd := exec.Command("stty", "raw", "-echo") | |
| 17 | - | cmd.Stdin = os.Stdin | |
| 18 | - | _ = cmd.Run() | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | // RestoreTerminal restores the terminal settings. | |
| 22 | - | func RestoreTerminal() { | |
| 23 | - | cmd := exec.Command("stty", "-raw", "echo") | |
| 24 | - | cmd.Stdin = os.Stdin | |
| 25 | - | _ = cmd.Run() | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | func RunAppCatchSIGINT(startServerFunc func() error, logger *zerolog.Logger) { | |
| 29 | - | // Start the server in a separate goroutine | |
| 30 | - | go func() { | |
| 31 | - | if err := startServerFunc(); err != nil && err != http.ErrServerClosed { | |
| 32 | - | logger.Fatal().Err(err).Msg("Failed to start the server") | |
| 33 | - | } | |
| 34 | - | }() | |
| 35 | - | ||
| 36 | - | // Catch SIGINT and SIGTERM signals without raw mode initially | |
| 37 | - | c := make(chan os.Signal, 1) | |
| 38 | - | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) | |
| 39 | - | <-c | |
| 40 | - | ||
| 41 | - | logger.Warn().Msg("Graceful shutdown initiated") | |
| 42 | - | logger.Warn().Msg("Server will shutdown in 5 seconds press ENTER to cancel") | |
| 43 | - | ||
| 44 | - | // Now, set raw mode | |
| 45 | - | SetRawMode() | |
| 46 | - | defer RestoreTerminal() | |
| 47 | - | ||
| 48 | - | // Channel to signal a key press | |
| 49 | - | keypress := make(chan struct{}) | |
| 50 | - | ||
| 51 | - | // Goroutine to listen for a key press | |
| 52 | - | go func() { | |
| 53 | - | var b []byte = make([]byte, 1) | |
| 54 | - | os.Stdin.Read(b) | |
| 55 | - | keypress <- struct{}{} | |
| 56 | - | }() | |
| 57 | - | ||
| 58 | - | // Wait for either a key press or the 5-second timer | |
| 59 | - | select { | |
| 60 | - | case <-time.After(5 * time.Second): | |
| 61 | - | logger.Info().Msg("Server shutdown") | |
| 62 | - | case <-keypress: | |
| 63 | - | logger.Info().Msg("Shutdown cancelled by user") | |
| 64 | - | } | |
| 65 | - | } | |
| @@ -1,65 +0,0 @@ | |||||
| 1 | - | package utils | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "net/http" | |||
| 5 | - | "os" | |||
| 6 | - | "os/exec" | |||
| 7 | - | "os/signal" | |||
| 8 | - | "syscall" | |||
| 9 | - | "time" | |||
| 10 | - | ||||
| 11 | - | "github.com/rs/zerolog" | |||
| 12 | - | ) | |||
| 13 | - | ||||
| 14 | - | // SetRawMode sets the terminal to raw mode. | |||
| 15 | - | func SetRawMode() { | |||
| 16 | - | cmd := exec.Command("stty", "raw", "-echo") | |||
| 17 | - | cmd.Stdin = os.Stdin | |||
| 18 | - | _ = cmd.Run() | |||
| 19 | - | } | |||
| 20 | - | ||||
| 21 | - | // RestoreTerminal restores the terminal settings. | |||
| 22 | - | func RestoreTerminal() { | |||
| 23 | - | cmd := exec.Command("stty", "-raw", "echo") | |||
| 24 | - | cmd.Stdin = os.Stdin | |||
| 25 | - | _ = cmd.Run() | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | func RunAppCatchSIGINT(startServerFunc func() error, logger *zerolog.Logger) { | |||
| 29 | - | // Start the server in a separate goroutine | |||
| 30 | - | go func() { | |||
| 31 | - | if err := startServerFunc(); err != nil && err != http.ErrServerClosed { | |||
| 32 | - | logger.Fatal().Err(err).Msg("Failed to start the server") | |||
| 33 | - | } | |||
| 34 | - | }() | |||
| 35 | - | ||||
| 36 | - | // Catch SIGINT and SIGTERM signals without raw mode initially | |||
| 37 | - | c := make(chan os.Signal, 1) | |||
| 38 | - | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) | |||
| 39 | - | <-c | |||
| 40 | - | ||||
| 41 | - | logger.Warn().Msg("Graceful shutdown initiated") | |||
| 42 | - | logger.Warn().Msg("Server will shutdown in 5 seconds press ENTER to cancel") | |||
| 43 | - | ||||
| 44 | - | // Now, set raw mode | |||
| 45 | - | SetRawMode() | |||
| 46 | - | defer RestoreTerminal() | |||
| 47 | - | ||||
| 48 | - | // Channel to signal a key press | |||
| 49 | - | keypress := make(chan struct{}) | |||
| 50 | - | ||||
| 51 | - | // Goroutine to listen for a key press | |||
| 52 | - | go func() { | |||
| 53 | - | var b []byte = make([]byte, 1) | |||
| 54 | - | os.Stdin.Read(b) | |||
| 55 | - | keypress <- struct{}{} | |||
| 56 | - | }() | |||
| 57 | - | ||||
| 58 | - | // Wait for either a key press or the 5-second timer | |||
| 59 | - | select { | |||
| 60 | - | case <-time.After(5 * time.Second): | |||
| 61 | - | logger.Info().Msg("Server shutdown") | |||
| 62 | - | case <-keypress: | |||
| 63 | - | logger.Info().Msg("Shutdown cancelled by user") | |||
| 64 | - | } | |||
| 65 | - | } | |||