detached 60d6353

Refactor project structure for better readability

commit
60d63539dee2d367fb46690618f9e0a341c1b671
author
bnema · · 2023-09-02 08:32 UTC
parent
8744b7e
signature
unsigned
20 files · +378 -164
M cmd/main.go +9 -162
@@ -1,175 +1,22 @@
11 package main
22
33 import (
4- "fmt"
5- "html/template"
6- "io/fs"
7- "net/http"
8- "os"
9-
10- "github.com/labstack/echo/v4"
11- "github.com/labstack/echo/v4/middleware"
12- "github.com/rs/zerolog"
13- "gogs.bnema.dev/gordon-echo/internal/ui"
14- "gogs.bnema.dev/gordon-echo/pkg/utils"
4+ "gogs.bnema.dev/gordon-echo/internal"
5+ "gogs.bnema.dev/gordon-echo/internal/http/routes"
156 )
167
17-// getRenderer returns a new renderer for the given template
18-func getRenderer(filename string, fs fs.FS) (*utils.Renderer, error) {
19- tmpl, err := template.New(filename).ParseFS(fs, filename)
20- if err != nil {
21- return nil, err
22- }
23- return &utils.Renderer{
24- Template: tmpl,
25- }, nil
26-}
27-
28-// bindStaticAdminUI serves the static admin UI
29-func bindStaticAdminUI(e *echo.Echo) error {
30- // Load static website data from YAML
31- staticData, err := utils.LoadDataFromYAML()
32- if err != nil {
33- return err
34- }
35-
36- // Main handler for /admin
37- e.GET("/admin/:lang", func(c echo.Context) error {
38- lang := c.Param("lang")
39-
40- switch lang {
41- case "fr":
42- staticData.CurrentLang = staticData.FR
43- default: // default to English if no match
44- staticData.CurrentLang = staticData.EN
45- }
46-
47- renderer, err := getRenderer("index.html", ui.PublicFS)
48- if err != nil {
49- return err
50- }
51-
52- html, err := renderer.Render(staticData)
53- if err != nil {
54- return c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
55- }
56-
57- return c.HTML(http.StatusOK, html)
58- })
59-
60- e.GET("/htmx", func(c echo.Context) error {
61- fragment := c.Request().Header.Get("X-Fragment")
62-
63- if fragment == "hello" {
64- // Fetch and return the "hello" fragment
65- content, err := utils.GetHTMLFragmentByID("hello")
66- fmt.Println("X-Fragment:", c.Request().Header.Get("X-Fragment"))
67- if err != nil {
68- return c.String(http.StatusInternalServerError, err.Error())
69- }
70- return c.HTML(http.StatusOK, content)
71- }
72-
73- // Handle other fragments or default behavior
74- return c.String(http.StatusBadRequest, "Invalid fragment")
75- })
76-
77- // !!! IMPORTANT !!! This must be the last route (except for the 404 route)
78- e.GET("/*", func(c echo.Context) error {
79- // Set cache headers
80- c.Response().Header().Set("Cache-Control", "public, max-age=86400")
81-
82- // Proceed with serving the file
83- return echo.StaticDirectoryHandler(ui.PublicFS, false)(c)
84- })
85-
86- // 404 handler
87- e.HTTPErrorHandler = func(err error, c echo.Context) {
88- if he, ok := err.(*echo.HTTPError); ok {
89- if he.Code == http.StatusNotFound {
90- // 404.html
91- renderer, err := getRenderer("404.html", ui.PublicFS)
92- if err != nil {
93- c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
94- }
95-
96- data := map[string]interface{}{
97- "website": map[string]interface{}{
98- "title": "Page Not Found",
99- },
100- }
101-
102- html, err := renderer.Render(data)
103- if err != nil {
104- c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
105- }
106-
107- c.HTML(http.StatusNotFound, html)
108- return
109- }
110- }
111-
112- e.DefaultHTTPErrorHandler(err, c)
113- }
114-
115- return nil
116-
117-}
118-
1198 func main() {
120- err := utils.CreateLogsDir()
121- if err != nil {
122- panic(err)
123- }
9+ // Setup logging
10+ internal.SetupLogging()
12411
125- // Setup App Logger
126- appLogFile, err := os.OpenFile("logs/app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
127- if err != nil {
128- panic(err)
129- }
130- defer appLogFile.Close()
12+ // Initialize loggers
13+ appLogger := internal.InitializeAppLogger()
14+ echoLogger := internal.InitializeHTTPLogger()
13115
132- appConsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
133- appMulti := zerolog.MultiLevelWriter(appLogFile, appConsoleWriter)
134- appLogger := zerolog.New(appMulti).With().Timestamp().Str("type", "app").Logger()
135-
136- // Setup Echo Logger
137- httpLogFile, err := os.OpenFile("logs/http.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
138- if err != nil {
139- panic(err)
140- }
141- defer httpLogFile.Close()
142-
143- httpConsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
144- httpMulti := zerolog.MultiLevelWriter(httpLogFile, httpConsoleWriter)
145- echoLogger := zerolog.New(httpMulti).With().Timestamp().Logger()
146-
147- e := echo.New()
148-
149- // Use zerolog for Echo's requests
150- e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
151- LogURI: true,
152- LogStatus: true,
153- LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
154- echoLogger.Info().
155- Str("type", "http").
156- Str("remote_ip", c.RealIP()).
157- Str("method", c.Request().Method).
158- Str("URI", v.URI).
159- Int("status", v.Status).
160- Msg("request")
161- return nil
162- },
163- }))
164- bindStaticAdminUI(e)
165-
166- // Will crash in case of error
167- if err := bindStaticAdminUI(e); err != nil {
168- e.Logger.Fatal(err)
169- }
16+ // Initialize echo server with routes and middlewares
17+ e := routes.NewRouter(appLogger, echoLogger)
17018
17119 if err := e.Start(":1323"); err != nil {
17220 appLogger.Error().Err(err).Msg("Failed to start the server")
17321 }
174-
17522 }
@@ -1,175 +1,22 @@
1 package main1 package main
2 2
3 import (3 import (
4- "fmt"4+ "gogs.bnema.dev/gordon-echo/internal"
5- "html/template"5+ "gogs.bnema.dev/gordon-echo/internal/http/routes"
6- "io/fs"
7- "net/http"
8- "os"
9-
10- "github.com/labstack/echo/v4"
11- "github.com/labstack/echo/v4/middleware"
12- "github.com/rs/zerolog"
13- "gogs.bnema.dev/gordon-echo/internal/ui"
14- "gogs.bnema.dev/gordon-echo/pkg/utils"
15 )6 )
16 7
17-// getRenderer returns a new renderer for the given template
18-func getRenderer(filename string, fs fs.FS) (*utils.Renderer, error) {
19- tmpl, err := template.New(filename).ParseFS(fs, filename)
20- if err != nil {
21- return nil, err
22- }
23- return &utils.Renderer{
24- Template: tmpl,
25- }, nil
26-}
27-
28-// bindStaticAdminUI serves the static admin UI
29-func bindStaticAdminUI(e *echo.Echo) error {
30- // Load static website data from YAML
31- staticData, err := utils.LoadDataFromYAML()
32- if err != nil {
33- return err
34- }
35-
36- // Main handler for /admin
37- e.GET("/admin/:lang", func(c echo.Context) error {
38- lang := c.Param("lang")
39-
40- switch lang {
41- case "fr":
42- staticData.CurrentLang = staticData.FR
43- default: // default to English if no match
44- staticData.CurrentLang = staticData.EN
45- }
46-
47- renderer, err := getRenderer("index.html", ui.PublicFS)
48- if err != nil {
49- return err
50- }
51-
52- html, err := renderer.Render(staticData)
53- if err != nil {
54- return c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
55- }
56-
57- return c.HTML(http.StatusOK, html)
58- })
59-
60- e.GET("/htmx", func(c echo.Context) error {
61- fragment := c.Request().Header.Get("X-Fragment")
62-
63- if fragment == "hello" {
64- // Fetch and return the "hello" fragment
65- content, err := utils.GetHTMLFragmentByID("hello")
66- fmt.Println("X-Fragment:", c.Request().Header.Get("X-Fragment"))
67- if err != nil {
68- return c.String(http.StatusInternalServerError, err.Error())
69- }
70- return c.HTML(http.StatusOK, content)
71- }
72-
73- // Handle other fragments or default behavior
74- return c.String(http.StatusBadRequest, "Invalid fragment")
75- })
76-
77- // !!! IMPORTANT !!! This must be the last route (except for the 404 route)
78- e.GET("/*", func(c echo.Context) error {
79- // Set cache headers
80- c.Response().Header().Set("Cache-Control", "public, max-age=86400")
81-
82- // Proceed with serving the file
83- return echo.StaticDirectoryHandler(ui.PublicFS, false)(c)
84- })
85-
86- // 404 handler
87- e.HTTPErrorHandler = func(err error, c echo.Context) {
88- if he, ok := err.(*echo.HTTPError); ok {
89- if he.Code == http.StatusNotFound {
90- // 404.html
91- renderer, err := getRenderer("404.html", ui.PublicFS)
92- if err != nil {
93- c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
94- }
95-
96- data := map[string]interface{}{
97- "website": map[string]interface{}{
98- "title": "Page Not Found",
99- },
100- }
101-
102- html, err := renderer.Render(data)
103- if err != nil {
104- c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
105- }
106-
107- c.HTML(http.StatusNotFound, html)
108- return
109- }
110- }
111-
112- e.DefaultHTTPErrorHandler(err, c)
113- }
114-
115- return nil
116-
117-}
118-
119 func main() {8 func main() {
120- err := utils.CreateLogsDir()9+ // Setup logging
121- if err != nil {10+ internal.SetupLogging()
122- panic(err)
123- }
124 11
125- // Setup App Logger12+ // Initialize loggers
126- appLogFile, err := os.OpenFile("logs/app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)13+ appLogger := internal.InitializeAppLogger()
127- if err != nil {14+ echoLogger := internal.InitializeHTTPLogger()
128- panic(err)
129- }
130- defer appLogFile.Close()
131 15
132- appConsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}16+ // Initialize echo server with routes and middlewares
133- appMulti := zerolog.MultiLevelWriter(appLogFile, appConsoleWriter)17+ e := routes.NewRouter(appLogger, echoLogger)
134- appLogger := zerolog.New(appMulti).With().Timestamp().Str("type", "app").Logger()
135-
136- // Setup Echo Logger
137- httpLogFile, err := os.OpenFile("logs/http.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
138- if err != nil {
139- panic(err)
140- }
141- defer httpLogFile.Close()
142-
143- httpConsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
144- httpMulti := zerolog.MultiLevelWriter(httpLogFile, httpConsoleWriter)
145- echoLogger := zerolog.New(httpMulti).With().Timestamp().Logger()
146-
147- e := echo.New()
148-
149- // Use zerolog for Echo's requests
150- e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
151- LogURI: true,
152- LogStatus: true,
153- LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
154- echoLogger.Info().
155- Str("type", "http").
156- Str("remote_ip", c.RealIP()).
157- Str("method", c.Request().Method).
158- Str("URI", v.URI).
159- Int("status", v.Status).
160- Msg("request")
161- return nil
162- },
163- }))
164- bindStaticAdminUI(e)
165-
166- // Will crash in case of error
167- if err := bindStaticAdminUI(e); err != nil {
168- e.Logger.Fatal(err)
169- }
170 18
171 if err := e.Start(":1323"); err != nil {19 if err := e.Start(":1323"); err != nil {
172 appLogger.Error().Err(err).Msg("Failed to start the server")20 appLogger.Error().Err(err).Msg("Failed to start the server")
173 }21 }
174-
175 }22 }
A internal/app.go +42 -0
@@ -0,0 +1,42 @@
1+package internal
2+
3+import (
4+ "os"
5+
6+ "github.com/rs/zerolog"
7+ "gogs.bnema.dev/gordon-echo/pkg/utils"
8+)
9+
10+// Define the main app struct
11+type App struct {
12+ // Define the app logger
13+ Logger *utils.Logger
14+ // Define the app config
15+}
16+
17+func InitializeAppLogger() zerolog.Logger {
18+ appLogFile, err := os.OpenFile("logs/app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
19+ if err != nil {
20+ panic(err)
21+ }
22+ appConsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
23+ appMulti := zerolog.MultiLevelWriter(appLogFile, appConsoleWriter)
24+ return zerolog.New(appMulti).With().Timestamp().Str("type", "app").Logger()
25+}
26+
27+func InitializeHTTPLogger() zerolog.Logger {
28+ httpLogFile, err := os.OpenFile("logs/http.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
29+ if err != nil {
30+ panic(err)
31+ }
32+ httpConsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
33+ httpMulti := zerolog.MultiLevelWriter(httpLogFile, httpConsoleWriter)
34+ return zerolog.New(httpMulti).With().Timestamp().Logger()
35+}
36+
37+func SetupLogging() {
38+ err := utils.CreateLogsDir()
39+ if err != nil {
40+ panic(err)
41+ }
42+}
@@ -0,0 +1,42 @@
1+package internal
2+
3+import (
4+ "os"
5+
6+ "github.com/rs/zerolog"
7+ "gogs.bnema.dev/gordon-echo/pkg/utils"
8+)
9+
10+// Define the main app struct
11+type App struct {
12+ // Define the app logger
13+ Logger *utils.Logger
14+ // Define the app config
15+}
16+
17+func InitializeAppLogger() zerolog.Logger {
18+ appLogFile, err := os.OpenFile("logs/app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
19+ if err != nil {
20+ panic(err)
21+ }
22+ appConsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
23+ appMulti := zerolog.MultiLevelWriter(appLogFile, appConsoleWriter)
24+ return zerolog.New(appMulti).With().Timestamp().Str("type", "app").Logger()
25+}
26+
27+func InitializeHTTPLogger() zerolog.Logger {
28+ httpLogFile, err := os.OpenFile("logs/http.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
29+ if err != nil {
30+ panic(err)
31+ }
32+ httpConsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
33+ httpMulti := zerolog.MultiLevelWriter(httpLogFile, httpConsoleWriter)
34+ return zerolog.New(httpMulti).With().Timestamp().Logger()
35+}
36+
37+func SetupLogging() {
38+ err := utils.CreateLogsDir()
39+ if err != nil {
40+ panic(err)
41+ }
42+}
A internal/http/handlers/404.go +37 -0
@@ -0,0 +1,37 @@
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/ui"
9+ "gogs.bnema.dev/gordon-echo/pkg/utils"
10+)
11+
12+func Custom404Handler(err error, c echo.Context) {
13+ if he, ok := err.(*echo.HTTPError); ok {
14+ if he.Code == http.StatusNotFound {
15+ renderer, err := utils.GetRenderer("404.gohtml", ui.PublicFS)
16+ if err != nil {
17+ c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
18+ }
19+
20+ data := map[string]interface{}{
21+ "website": map[string]interface{}{
22+ "title": "Page Not Found",
23+ },
24+ }
25+
26+ html, err := renderer.Render(data)
27+ if err != nil {
28+ c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
29+ }
30+
31+ c.HTML(http.StatusNotFound, html)
32+ return
33+ }
34+ }
35+
36+ DefaultHTTPErrorHandler(err, c)
37+}
@@ -0,0 +1,37 @@
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/ui"
9+ "gogs.bnema.dev/gordon-echo/pkg/utils"
10+)
11+
12+func Custom404Handler(err error, c echo.Context) {
13+ if he, ok := err.(*echo.HTTPError); ok {
14+ if he.Code == http.StatusNotFound {
15+ renderer, err := utils.GetRenderer("404.gohtml", ui.PublicFS)
16+ if err != nil {
17+ c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
18+ }
19+
20+ data := map[string]interface{}{
21+ "website": map[string]interface{}{
22+ "title": "Page Not Found",
23+ },
24+ }
25+
26+ html, err := renderer.Render(data)
27+ if err != nil {
28+ c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
29+ }
30+
31+ c.HTML(http.StatusNotFound, html)
32+ return
33+ }
34+ }
35+
36+ DefaultHTTPErrorHandler(err, c)
37+}
A internal/http/handlers/default.go +30 -0
@@ -0,0 +1,30 @@
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/ui"
9+ "gogs.bnema.dev/gordon-echo/pkg/utils"
10+)
11+
12+func DefaultHTTPErrorHandler(err error, c echo.Context) {
13+ renderer, err := utils.GetRenderer("500.gohtml", ui.PublicFS)
14+ if err != nil {
15+ c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
16+ }
17+
18+ data := map[string]interface{}{
19+ "website": map[string]interface{}{
20+ "title": "Internal Server Error",
21+ },
22+ }
23+
24+ html, err := renderer.Render(data)
25+ if err != nil {
26+ c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
27+ }
28+
29+ c.HTML(http.StatusInternalServerError, html)
30+}
@@ -0,0 +1,30 @@
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/ui"
9+ "gogs.bnema.dev/gordon-echo/pkg/utils"
10+)
11+
12+func DefaultHTTPErrorHandler(err error, c echo.Context) {
13+ renderer, err := utils.GetRenderer("500.gohtml", ui.PublicFS)
14+ if err != nil {
15+ c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
16+ }
17+
18+ data := map[string]interface{}{
19+ "website": map[string]interface{}{
20+ "title": "Internal Server Error",
21+ },
22+ }
23+
24+ html, err := renderer.Render(data)
25+ if err != nil {
26+ c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
27+ }
28+
29+ c.HTML(http.StatusInternalServerError, html)
30+}
A internal/http/handlers/htmx.go +50 -0
@@ -0,0 +1,50 @@
1+package handlers
2+
3+import (
4+ "net/http"
5+
6+ "github.com/labstack/echo/v4"
7+ "gogs.bnema.dev/gordon-echo/pkg/htmx"
8+ "gogs.bnema.dev/gordon-echo/pkg/utils"
9+)
10+
11+func HTMXHandler(c echo.Context) error {
12+ // Detect and extract HTMX data from the request
13+ htmxRequest := htmx.GetRequest(c)
14+
15+ // If the request is not an HTMX request, return an error
16+ if !htmxRequest.Enabled {
17+ return c.String(http.StatusBadRequest, "Non-HTMX request")
18+ }
19+
20+ // If it's a GET request, handle the fragment
21+ if c.Request().Method == http.MethodGet {
22+ return HTMXFragmentHandler(c)
23+ }
24+
25+ // For other HTTP methods, return an error
26+
27+ return c.String(http.StatusBadRequest, "Invalid request method")
28+}
29+
30+func HTMXFragmentHandler(c echo.Context) error {
31+ fragment := c.Request().Header.Get("X-Fragment")
32+ if fragment == "" {
33+ return c.String(http.StatusBadRequest, "Missing fragment header")
34+ }
35+
36+ content, err := utils.GetHTMLFragmentByID(fragment)
37+ if err != nil {
38+ // You might want to differentiate between different types of errors:
39+ // 1. If the fragment doesn't match any known component (return a 404 error)
40+ // 2. If there's another error while fetching the component (return a 500 error)
41+ // For this example, I'm treating all errors as internal server errors for simplicity.
42+ return c.String(http.StatusInternalServerError, err.Error())
43+ }
44+
45+ // Create and apply an HTMX response if necessary
46+ // htmxResponse := htmx.Response{Refresh: true}
47+ // htmxResponse.Apply(c)
48+
49+ return c.HTML(http.StatusOK, content)
50+}
@@ -0,0 +1,50 @@
1+package handlers
2+
3+import (
4+ "net/http"
5+
6+ "github.com/labstack/echo/v4"
7+ "gogs.bnema.dev/gordon-echo/pkg/htmx"
8+ "gogs.bnema.dev/gordon-echo/pkg/utils"
9+)
10+
11+func HTMXHandler(c echo.Context) error {
12+ // Detect and extract HTMX data from the request
13+ htmxRequest := htmx.GetRequest(c)
14+
15+ // If the request is not an HTMX request, return an error
16+ if !htmxRequest.Enabled {
17+ return c.String(http.StatusBadRequest, "Non-HTMX request")
18+ }
19+
20+ // If it's a GET request, handle the fragment
21+ if c.Request().Method == http.MethodGet {
22+ return HTMXFragmentHandler(c)
23+ }
24+
25+ // For other HTTP methods, return an error
26+
27+ return c.String(http.StatusBadRequest, "Invalid request method")
28+}
29+
30+func HTMXFragmentHandler(c echo.Context) error {
31+ fragment := c.Request().Header.Get("X-Fragment")
32+ if fragment == "" {
33+ return c.String(http.StatusBadRequest, "Missing fragment header")
34+ }
35+
36+ content, err := utils.GetHTMLFragmentByID(fragment)
37+ if err != nil {
38+ // You might want to differentiate between different types of errors:
39+ // 1. If the fragment doesn't match any known component (return a 404 error)
40+ // 2. If there's another error while fetching the component (return a 500 error)
41+ // For this example, I'm treating all errors as internal server errors for simplicity.
42+ return c.String(http.StatusInternalServerError, err.Error())
43+ }
44+
45+ // Create and apply an HTMX response if necessary
46+ // htmxResponse := htmx.Response{Refresh: true}
47+ // htmxResponse.Apply(c)
48+
49+ return c.HTML(http.StatusOK, content)
50+}
A internal/http/middlewares/logger.go +24 -0
@@ -0,0 +1,24 @@
1+package middlewares
2+
3+import (
4+ "github.com/labstack/echo/v4"
5+ "github.com/labstack/echo/v4/middleware"
6+ "github.com/rs/zerolog"
7+)
8+
9+func NewRequestLoggerMiddleware(logger zerolog.Logger) echo.MiddlewareFunc {
10+ return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
11+ LogURI: true,
12+ LogStatus: true,
13+ LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
14+ logger.Info().
15+ Str("type", "http").
16+ Str("remote_ip", c.RealIP()).
17+ Str("method", c.Request().Method).
18+ Str("URI", v.URI).
19+ Int("status", v.Status).
20+ Msg("request")
21+ return nil
22+ },
23+ })
24+}
@@ -0,0 +1,24 @@
1+package middlewares
2+
3+import (
4+ "github.com/labstack/echo/v4"
5+ "github.com/labstack/echo/v4/middleware"
6+ "github.com/rs/zerolog"
7+)
8+
9+func NewRequestLoggerMiddleware(logger zerolog.Logger) echo.MiddlewareFunc {
10+ return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
11+ LogURI: true,
12+ LogStatus: true,
13+ LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
14+ logger.Info().
15+ Str("type", "http").
16+ Str("remote_ip", c.RealIP()).
17+ Str("method", c.Request().Method).
18+ Str("URI", v.URI).
19+ Int("status", v.Status).
20+ Msg("request")
21+ return nil
22+ },
23+ })
24+}
A internal/http/routes/admin_routes.go +44 -0
@@ -0,0 +1,44 @@
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/ui"
9+ "gogs.bnema.dev/gordon-echo/pkg/utils"
10+)
11+
12+func AdminRoute(c echo.Context) error {
13+ staticData, err := utils.LoadDataFromYAML()
14+ fmt.Println(staticData)
15+ if err != nil {
16+ return err
17+ }
18+
19+ lang := c.Param("lang")
20+
21+ switch lang {
22+ case "fr":
23+ staticData.CurrentLang = staticData.FR
24+ default: // default to English if no match
25+ staticData.CurrentLang = staticData.EN
26+ }
27+
28+ renderer, err := utils.GetRenderer("index.gohtml", ui.PublicFS)
29+ if err != nil {
30+ return err
31+ }
32+
33+ html, err := renderer.Render(staticData)
34+ if err != nil {
35+ return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
36+ }
37+
38+ return c.HTML(http.StatusOK, html)
39+}
40+
41+func StaticRoute(c echo.Context) error {
42+ c.Response().Header().Set("Cache-Control", "public, max-age=86400")
43+ return echo.StaticDirectoryHandler(ui.PublicFS, false)(c)
44+}
@@ -0,0 +1,44 @@
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/ui"
9+ "gogs.bnema.dev/gordon-echo/pkg/utils"
10+)
11+
12+func AdminRoute(c echo.Context) error {
13+ staticData, err := utils.LoadDataFromYAML()
14+ fmt.Println(staticData)
15+ if err != nil {
16+ return err
17+ }
18+
19+ lang := c.Param("lang")
20+
21+ switch lang {
22+ case "fr":
23+ staticData.CurrentLang = staticData.FR
24+ default: // default to English if no match
25+ staticData.CurrentLang = staticData.EN
26+ }
27+
28+ renderer, err := utils.GetRenderer("index.gohtml", ui.PublicFS)
29+ if err != nil {
30+ return err
31+ }
32+
33+ html, err := renderer.Render(staticData)
34+ if err != nil {
35+ return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
36+ }
37+
38+ return c.HTML(http.StatusOK, html)
39+}
40+
41+func StaticRoute(c echo.Context) error {
42+ c.Response().Header().Set("Cache-Control", "public, max-age=86400")
43+ return echo.StaticDirectoryHandler(ui.PublicFS, false)(c)
44+}
A internal/http/routes/router.go +29 -0
@@ -0,0 +1,29 @@
1+package routes
2+
3+import (
4+ "github.com/labstack/echo/v4"
5+ "github.com/rs/zerolog"
6+ "gogs.bnema.dev/gordon-echo/internal/http/handlers"
7+ "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
8+)
9+
10+func NewRouter(appLogger, echoLogger zerolog.Logger) *echo.Echo {
11+ e := echo.New()
12+
13+ // Register middlewares
14+ e.Use(middlewares.NewRequestLoggerMiddleware(echoLogger))
15+
16+ // Register routes
17+ e = bindStaticAdminUI(e)
18+
19+ return e
20+}
21+
22+func bindStaticAdminUI(e *echo.Echo) *echo.Echo {
23+ e.GET("/admin/:lang", AdminRoute)
24+ e.GET("/htmx", handlers.HTMXHandler)
25+ e.GET("/*", StaticRoute)
26+ e.HTTPErrorHandler = handlers.Custom404Handler
27+ e.HTTPErrorHandler = handlers.DefaultHTTPErrorHandler
28+ return e
29+}
@@ -0,0 +1,29 @@
1+package routes
2+
3+import (
4+ "github.com/labstack/echo/v4"
5+ "github.com/rs/zerolog"
6+ "gogs.bnema.dev/gordon-echo/internal/http/handlers"
7+ "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
8+)
9+
10+func NewRouter(appLogger, echoLogger zerolog.Logger) *echo.Echo {
11+ e := echo.New()
12+
13+ // Register middlewares
14+ e.Use(middlewares.NewRequestLoggerMiddleware(echoLogger))
15+
16+ // Register routes
17+ e = bindStaticAdminUI(e)
18+
19+ return e
20+}
21+
22+func bindStaticAdminUI(e *echo.Echo) *echo.Echo {
23+ e.GET("/admin/:lang", AdminRoute)
24+ e.GET("/htmx", handlers.HTMXHandler)
25+ e.GET("/*", StaticRoute)
26+ e.HTTPErrorHandler = handlers.Custom404Handler
27+ e.HTTPErrorHandler = handlers.DefaultHTTPErrorHandler
28+ return e
29+}
A internal/http/services/sqlite.go +1 -0
@@ -0,0 +1,1 @@
1+package services
@@ -0,0 +1,1 @@
1+package services
R internal/ui/public/404.html → internal/ui/public/404.gohtml +0 -0 renamed, 100% similar

renamed, 100% similar

A internal/ui/public/500.gohtml +12 -0
@@ -0,0 +1,12 @@
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 wrong</p>
11+ </body>
12+</html>
@@ -0,0 +1,12 @@
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 wrong</p>
11+ </body>
12+</html>
R internal/ui/public/index.html → internal/ui/public/index.gohtml +0 -0 renamed, 100% similar

renamed, 100% similar

R internal/ui/templates/components.html → internal/ui/templates/components.gohtml +0 -0 renamed, 100% similar

renamed, 100% similar

R internal/ui/templates/dashboard.html → internal/ui/templates/dashboard.gohtml +0 -0 renamed, 100% similar

renamed, 100% similar

R internal/ui/templates/manager.html → internal/ui/templates/manager.gohtml +0 -0 renamed, 100% similar

renamed, 100% similar

R internal/ui/templates/user.html → internal/ui/templates/user.gohtml +0 -0 renamed, 100% similar

renamed, 100% similar

A pkg/htmx/htmx.go +84 -0
@@ -0,0 +1,84 @@
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 {
51+ return 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+
61+// Apply applies data from a Response to a server response
62+func (r Response) Apply(ctx echo.Context) {
63+ if r.Push != "" {
64+ ctx.Response().Header().Set(HeaderPush, r.Push)
65+ }
66+ if r.Redirect != "" {
67+ ctx.Response().Header().Set(HeaderRedirect, r.Redirect)
68+ }
69+ if r.Refresh {
70+ ctx.Response().Header().Set(HeaderRefresh, "true")
71+ }
72+ if r.Trigger != "" {
73+ ctx.Response().Header().Set(HeaderTrigger, r.Trigger)
74+ }
75+ if r.TriggerAfterSwap != "" {
76+ ctx.Response().Header().Set(HeaderTriggerAfterSwap, r.TriggerAfterSwap)
77+ }
78+ if r.TriggerAfterSettle != "" {
79+ ctx.Response().Header().Set(HeaderTriggerAfterSettle, r.TriggerAfterSettle)
80+ }
81+ if r.NoContent {
82+ ctx.Response().Status = http.StatusNoContent
83+ }
84+}
@@ -0,0 +1,84 @@
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 {
51+ return 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+
61+// Apply applies data from a Response to a server response
62+func (r Response) Apply(ctx echo.Context) {
63+ if r.Push != "" {
64+ ctx.Response().Header().Set(HeaderPush, r.Push)
65+ }
66+ if r.Redirect != "" {
67+ ctx.Response().Header().Set(HeaderRedirect, r.Redirect)
68+ }
69+ if r.Refresh {
70+ ctx.Response().Header().Set(HeaderRefresh, "true")
71+ }
72+ if r.Trigger != "" {
73+ ctx.Response().Header().Set(HeaderTrigger, r.Trigger)
74+ }
75+ if r.TriggerAfterSwap != "" {
76+ ctx.Response().Header().Set(HeaderTriggerAfterSwap, r.TriggerAfterSwap)
77+ }
78+ if r.TriggerAfterSettle != "" {
79+ ctx.Response().Header().Set(HeaderTriggerAfterSettle, r.TriggerAfterSettle)
80+ }
81+ if r.NoContent {
82+ ctx.Response().Status = http.StatusNoContent
83+ }
84+}
M pkg/utils/logger.go +3 -2
@@ -9,7 +9,6 @@ import (
99
1010 const (
1111 App LoggerType = iota
12- APP
1312 HTTP
1413 Database
1514 Admin
@@ -28,14 +27,16 @@ type LogFileManager struct {
2827 mu sync.Mutex
2928 }
3029
30+// NewLogger returns a new logger instance
3131 func NewLogger() *Logger {
32- writer := zerolog.ConsoleWriter{Out: os.Stdout} // changed to os.Stdout
32+ writer := zerolog.ConsoleWriter{Out: os.Stdout}
3333
3434 // Create a new logger
3535 zl := zerolog.New(writer).With().Timestamp().Logger()
3636 return &Logger{zl}
3737 }
3838
39+// NewLogFileManager returns a new log file manager
3940 func NewLogFileManager() *LogFileManager {
4041 return &LogFileManager{
4142 files: make(map[LoggerType]*os.File),
@@ -9,7 +9,6 @@ import (
9 9
10 const (10 const (
11 App LoggerType = iota11 App LoggerType = iota
12- APP
13 HTTP12 HTTP
14 Database13 Database
15 Admin14 Admin
@@ -28,14 +27,16 @@ type LogFileManager struct {
28 mu sync.Mutex27 mu sync.Mutex
29 }28 }
30 29
30+// NewLogger returns a new logger instance
31 func NewLogger() *Logger {31 func NewLogger() *Logger {
32- writer := zerolog.ConsoleWriter{Out: os.Stdout} // changed to os.Stdout32+ writer := zerolog.ConsoleWriter{Out: os.Stdout}
33 33
34 // Create a new logger34 // Create a new logger
35 zl := zerolog.New(writer).With().Timestamp().Logger()35 zl := zerolog.New(writer).With().Timestamp().Logger()
36 return &Logger{zl}36 return &Logger{zl}
37 }37 }
38 38
39+// NewLogFileManager returns a new log file manager
39 func NewLogFileManager() *LogFileManager {40 func NewLogFileManager() *LogFileManager {
40 return &LogFileManager{41 return &LogFileManager{
41 files: make(map[LoggerType]*os.File),42 files: make(map[LoggerType]*os.File),
M pkg/utils/parser.go +1 -0
@@ -29,6 +29,7 @@ func LoadDataFromYAML() (Data, error) {
2929
3030 // Open the YAML file inside the ui/components directory
3131 file, err := ui.TemplateFS.Open("strings.yaml")
32+ fmt.Println(file)
3233 if err != nil {
3334 return data, fmt.Errorf("failed to open strings.yaml: %w", err)
3435 }
@@ -29,6 +29,7 @@ func LoadDataFromYAML() (Data, error) {
29 29
30 // Open the YAML file inside the ui/components directory30 // Open the YAML file inside the ui/components directory
31 file, err := ui.TemplateFS.Open("strings.yaml")31 file, err := ui.TemplateFS.Open("strings.yaml")
32+ fmt.Println(file)
32 if err != nil {33 if err != nil {
33 return data, fmt.Errorf("failed to open strings.yaml: %w", err)34 return data, fmt.Errorf("failed to open strings.yaml: %w", err)
34 }35 }
M pkg/utils/render.go +12 -0
@@ -4,6 +4,7 @@ import (
44 "bytes"
55 "errors"
66 "html/template"
7+ "io/fs"
78 )
89
910 type Renderer struct {
@@ -29,3 +30,14 @@ func (r *Renderer) Render(data interface{}) (string, error) {
2930
3031 return buf.String(), nil
3132 }
33+
34+// GetRenderer function returns a new Renderer instance
35+func GetRenderer(filename string, fs fs.FS) (*Renderer, error) {
36+ tmpl, err := template.New(filename).ParseFS(fs, filename)
37+ if err != nil {
38+ return nil, err
39+ }
40+ return &Renderer{
41+ Template: tmpl,
42+ }, nil
43+}
@@ -4,6 +4,7 @@ import (
4 "bytes"4 "bytes"
5 "errors"5 "errors"
6 "html/template"6 "html/template"
7+ "io/fs"
7 )8 )
8 9
9 type Renderer struct {10 type Renderer struct {
@@ -29,3 +30,14 @@ func (r *Renderer) Render(data interface{}) (string, error) {
29 30
30 return buf.String(), nil31 return buf.String(), nil
31 }32 }
33+
34+// GetRenderer function returns a new Renderer instance
35+func GetRenderer(filename string, fs fs.FS) (*Renderer, error) {
36+ tmpl, err := template.New(filename).ParseFS(fs, filename)
37+ if err != nil {
38+ return nil, err
39+ }
40+ return &Renderer{
41+ Template: tmpl,
42+ }, nil
43+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close