detached 2f72eed

Integrate HTMX for dynamic content, add language detection middleware, and fix template rendering.

spc
commit
2f72eed982a464db46de37f08acd0be4fcb547e4
author
bnema · · 2023-09-02 16:27 UTC
committer
bnema · · 2023-09-04 06:59 UTC
parent
5785c68
signature
unsigned
16 files · +131 -44
M internal/http/handlers/htmx.go +10 -2
@@ -4,6 +4,7 @@ import (
44 "net/http"
55
66 "github.com/labstack/echo/v4"
7+ "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
78 "gogs.bnema.dev/gordon-echo/pkg/htmx"
89 "gogs.bnema.dev/gordon-echo/pkg/utils"
910 )
@@ -34,12 +35,19 @@ func HTMXHandler(c echo.Context) error {
3435 }
3536
3637 func htmxFragmentHandler(c echo.Context, logger *utils.Logger) error {
37- fragment := c.Request().Header.Get("X-Fragment")
38+ fragment := c.Request().Header.Get("HX-Fragment")
3839 if fragment == "" {
3940 logger.Error().Msg("Missing fragment header")
4041 return c.String(http.StatusBadRequest, "Missing fragment header")
4142 }
42- content, err := utils.GetHTMLFragmentByID(fragment)
43+
44+ lang := c.Get(middlewares.LangKey).(string)
45+ staticData, err := utils.PopulateDataFromYAML(lang)
46+ if err != nil {
47+ return err
48+ }
49+
50+ content, err := utils.GetHTMLFragmentByID(fragment, staticData.CurrentLang)
4351 if err != nil {
4452 logger.Error().Err(err).Msg("Failed to get fragment")
4553 return c.String(http.StatusInternalServerError, err.Error())
@@ -4,6 +4,7 @@ import (
4 "net/http"4 "net/http"
5 5
6 "github.com/labstack/echo/v4"6 "github.com/labstack/echo/v4"
7+ "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
7 "gogs.bnema.dev/gordon-echo/pkg/htmx"8 "gogs.bnema.dev/gordon-echo/pkg/htmx"
8 "gogs.bnema.dev/gordon-echo/pkg/utils"9 "gogs.bnema.dev/gordon-echo/pkg/utils"
9 )10 )
@@ -34,12 +35,19 @@ func HTMXHandler(c echo.Context) error {
34 }35 }
35 36
36 func htmxFragmentHandler(c echo.Context, logger *utils.Logger) error {37 func htmxFragmentHandler(c echo.Context, logger *utils.Logger) error {
37- fragment := c.Request().Header.Get("X-Fragment")38+ fragment := c.Request().Header.Get("HX-Fragment")
38 if fragment == "" {39 if fragment == "" {
39 logger.Error().Msg("Missing fragment header")40 logger.Error().Msg("Missing fragment header")
40 return c.String(http.StatusBadRequest, "Missing fragment header")41 return c.String(http.StatusBadRequest, "Missing fragment header")
41 }42 }
42- content, err := utils.GetHTMLFragmentByID(fragment)43+
44+ lang := c.Get(middlewares.LangKey).(string)
45+ staticData, err := utils.PopulateDataFromYAML(lang)
46+ if err != nil {
47+ return err
48+ }
49+
50+ content, err := utils.GetHTMLFragmentByID(fragment, staticData.CurrentLang)
43 if err != nil {51 if err != nil {
44 logger.Error().Err(err).Msg("Failed to get fragment")52 logger.Error().Err(err).Msg("Failed to get fragment")
45 return c.String(http.StatusInternalServerError, err.Error())53 return c.String(http.StatusInternalServerError, err.Error())
A internal/http/middlewares/detect-lang.go +27 -0
@@ -0,0 +1,27 @@
1+package middlewares
2+
3+import (
4+ "strings"
5+
6+ "github.com/labstack/echo/v4"
7+)
8+
9+// LangKey is a key for setting and getting the language from the context
10+const LangKey = "CurrentLang"
11+
12+// LanguageDetectionMiddleware detects the current language and sets it in the context
13+func LanguageDetectionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
14+ return func(c echo.Context) error {
15+ lang := detectCurrentLanguage(c)
16+ c.Set(LangKey, lang)
17+ return next(c)
18+ }
19+}
20+
21+func detectCurrentLanguage(c echo.Context) string {
22+ header := c.Request().Header.Get("Accept-Language")
23+ if strings.Contains(header, "fr") {
24+ return "fr"
25+ }
26+ return "en" // Default to English
27+}
@@ -0,0 +1,27 @@
1+package middlewares
2+
3+import (
4+ "strings"
5+
6+ "github.com/labstack/echo/v4"
7+)
8+
9+// LangKey is a key for setting and getting the language from the context
10+const LangKey = "CurrentLang"
11+
12+// LanguageDetectionMiddleware detects the current language and sets it in the context
13+func LanguageDetectionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
14+ return func(c echo.Context) error {
15+ lang := detectCurrentLanguage(c)
16+ c.Set(LangKey, lang)
17+ return next(c)
18+ }
19+}
20+
21+func detectCurrentLanguage(c echo.Context) string {
22+ header := c.Request().Header.Get("Accept-Language")
23+ if strings.Contains(header, "fr") {
24+ return "fr"
25+ }
26+ return "en" // Default to English
27+}
M internal/http/routes/admin_routes.go +6 -22
@@ -3,37 +3,21 @@ package routes
33 import (
44 "fmt"
55 "net/http"
6- "strings"
76
87 "github.com/labstack/echo/v4"
8+ "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
99 "gogs.bnema.dev/gordon-echo/internal/ui"
1010 "gogs.bnema.dev/gordon-echo/pkg/utils"
1111 )
1212
13-func getPreferredLanguage(c echo.Context) string {
14- header := c.Request().Header.Get("Accept-Language")
15- // Check the header value, parse it and determine the primary language.
16- // For this example, let's assume English and French only.
17- if strings.Contains(header, "fr") {
18- return "fr"
19- }
20- return "en" // Default to English
21-}
22-
2313 func AdminRoute(c echo.Context) error {
24- staticData, err := utils.ReadDataFromYAML()
25- if err != nil {
26- return err
27- }
28-
29- lang := getPreferredLanguage(c)
14+ // Retrieve the current language from the context
15+ lang := c.Get(middlewares.LangKey).(string)
3016 fmt.Println(lang)
3117
32- switch lang {
33- case "fr":
34- staticData.CurrentLang = staticData.FR
35- default: // default to English if no match
36- staticData.CurrentLang = staticData.EN
18+ staticData, err := utils.PopulateDataFromYAML(lang)
19+ if err != nil {
20+ return err
3721 }
3822
3923 renderer, err := utils.GetRenderer("index.gohtml", ui.PublicFS, utils.NewLogger())
@@ -3,37 +3,21 @@ package routes
3 import (3 import (
4 "fmt"4 "fmt"
5 "net/http"5 "net/http"
6- "strings"
7 6
8 "github.com/labstack/echo/v4"7 "github.com/labstack/echo/v4"
8+ "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
9 "gogs.bnema.dev/gordon-echo/internal/ui"9 "gogs.bnema.dev/gordon-echo/internal/ui"
10 "gogs.bnema.dev/gordon-echo/pkg/utils"10 "gogs.bnema.dev/gordon-echo/pkg/utils"
11 )11 )
12 12
13-func getPreferredLanguage(c echo.Context) string {
14- header := c.Request().Header.Get("Accept-Language")
15- // Check the header value, parse it and determine the primary language.
16- // For this example, let's assume English and French only.
17- if strings.Contains(header, "fr") {
18- return "fr"
19- }
20- return "en" // Default to English
21-}
22-
23 func AdminRoute(c echo.Context) error {13 func AdminRoute(c echo.Context) error {
24- staticData, err := utils.ReadDataFromYAML()14+ // Retrieve the current language from the context
25- if err != nil {15+ lang := c.Get(middlewares.LangKey).(string)
26- return err
27- }
28-
29- lang := getPreferredLanguage(c)
30 fmt.Println(lang)16 fmt.Println(lang)
31 17
32- switch lang {18+ staticData, err := utils.PopulateDataFromYAML(lang)
33- case "fr":19+ if err != nil {
34- staticData.CurrentLang = staticData.FR20+ return err
35- default: // default to English if no match
36- staticData.CurrentLang = staticData.EN
37 }21 }
38 22
39 renderer, err := utils.GetRenderer("index.gohtml", ui.PublicFS, utils.NewLogger())23 renderer, err := utils.GetRenderer("index.gohtml", ui.PublicFS, utils.NewLogger())
M internal/http/routes/router.go +1 -0
@@ -12,6 +12,7 @@ func NewRouter(app *app.App) *echo.Echo {
1212
1313 // Register middlewares
1414 e.Use(middlewares.NewRequestLoggerMiddleware(app.HTTPLogger.Logger))
15+ e.Use(middlewares.LanguageDetectionMiddleware)
1516
1617 // Register routes
1718 e = bindStaticAdminUI(e)
@@ -12,6 +12,7 @@ func NewRouter(app *app.App) *echo.Echo {
12 12
13 // Register middlewares13 // Register middlewares
14 e.Use(middlewares.NewRequestLoggerMiddleware(app.HTTPLogger.Logger))14 e.Use(middlewares.NewRequestLoggerMiddleware(app.HTTPLogger.Logger))
15+ e.Use(middlewares.LanguageDetectionMiddleware)
15 16
16 // Register routes17 // Register routes
17 e = bindStaticAdminUI(e)18 e = bindStaticAdminUI(e)
A internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2 +0 -0 binary file: no text diff

binary file: no text diff

A internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Light.woff2 +0 -0 binary file: no text diff

binary file: no text diff

A internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Medium.woff2 +0 -0 binary file: no text diff

binary file: no text diff

A internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2 +0 -0 binary file: no text diff

binary file: no text diff

A internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Thin.woff2 +0 -0 binary file: no text diff

binary file: no text diff

A internal/ui/public/assets/fonts/JetBrainsMonoNerdFont/JetBrainsMonoNerdFont-Bold.woff2 +0 -0 binary file: no text diff

binary file: no text diff

A internal/ui/public/assets/fonts/JetBrainsMonoNerdFont/JetBrainsMonoNerdFont-Regular.woff2 +0 -0 binary file: no text diff

binary file: no text diff

M internal/ui/public/index.gohtml +23 -14
@@ -1,9 +1,13 @@
1+<!-- @format -->
2+
13 <!DOCTYPE html>
24 <html lang="en">
35 <head>
46 <meta charset="UTF-8" />
57 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
68 <title>{{ .Header.Meta.Title }}</title>
9+ <meta name="description" content="{{ .Header.Meta.Description }}" />
10+ <meta name="keywords" content="{{ .Header.Meta.Keywords }}" />
711 <!-- Custom CSS from assets/css -->
812 <link rel="stylesheet" href="assets/css/custom.css" />
913 <!-- Alpine -->
@@ -12,23 +16,28 @@
1216 <script src="libs/htmx.min.js"></script>
1317 <!-- UnoCSS (need to embed locally) -->
1418 <script src="https://cdn.jsdelivr.net/npm/@unocss/runtime"></script>
19+ <!-- color -->
1520 <!-- Custom JS from assets/js -->
1621 <script src="assets/js/custom.js"></script>
1722 </head>
1823
19- <header
20- id="header"
21- hx-get="/htmx"
22- hx-headers='{"X-Fragment": "header"}'
23- hx-trigger="load"
24- hx-swap="innerHTML"
25- ></header>
24+ <body class="bg-gray-100 flex flex-col min-h-screen">
25+ <!-- Header Fragment -->
26+ <div
27+ id="header"
28+ hx-get="/htmx"
29+ hx-headers='{"HX-Fragment": "header"}'
30+ hx-trigger="load"
31+ hx-swap="outerHTML"
32+ ></div>
2633
27- <body
28- id="app"
29- hx-get="/htmx"
30- hx-headers='{"X-Fragment": "hello"}'
31- hx-trigger="load"
32- hx-swap="innerHTML"
33- ></body>
34+ <!-- App Fragment -->
35+ <div
36+ id="app"
37+ hx-get="/htmx"
38+ hx-headers='{"HX-Fragment": "hello"}'
39+ hx-trigger="load"
40+ hx-swap="outerHTML"
41+ ></div>
42+ </body>
3443 </html>
@@ -1,9 +1,13 @@
1+<!-- @format -->
2+
1 <!DOCTYPE html>3 <!DOCTYPE html>
2 <html lang="en">4 <html lang="en">
3 <head>5 <head>
4 <meta charset="UTF-8" />6 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />7 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>{{ .Header.Meta.Title }}</title>8 <title>{{ .Header.Meta.Title }}</title>
9+ <meta name="description" content="{{ .Header.Meta.Description }}" />
10+ <meta name="keywords" content="{{ .Header.Meta.Keywords }}" />
7 <!-- Custom CSS from assets/css -->11 <!-- Custom CSS from assets/css -->
8 <link rel="stylesheet" href="assets/css/custom.css" />12 <link rel="stylesheet" href="assets/css/custom.css" />
9 <!-- Alpine -->13 <!-- Alpine -->
@@ -12,23 +16,28 @@
12 <script src="libs/htmx.min.js"></script>16 <script src="libs/htmx.min.js"></script>
13 <!-- UnoCSS (need to embed locally) -->17 <!-- UnoCSS (need to embed locally) -->
14 <script src="https://cdn.jsdelivr.net/npm/@unocss/runtime"></script>18 <script src="https://cdn.jsdelivr.net/npm/@unocss/runtime"></script>
19+ <!-- color -->
15 <!-- Custom JS from assets/js -->20 <!-- Custom JS from assets/js -->
16 <script src="assets/js/custom.js"></script>21 <script src="assets/js/custom.js"></script>
17 </head>22 </head>
18 23
19- <header24+ <body class="bg-gray-100 flex flex-col min-h-screen">
20- id="header"25+ <!-- Header Fragment -->
21- hx-get="/htmx"26+ <div
22- hx-headers='{"X-Fragment": "header"}'27+ id="header"
23- hx-trigger="load"28+ hx-get="/htmx"
24- hx-swap="innerHTML"29+ hx-headers='{"HX-Fragment": "header"}'
25- ></header>30+ hx-trigger="load"
31+ hx-swap="outerHTML"
32+ ></div>
26 33
27- <body34+ <!-- App Fragment -->
28- id="app"35+ <div
29- hx-get="/htmx"36+ id="app"
30- hx-headers='{"X-Fragment": "hello"}'37+ hx-get="/htmx"
31- hx-trigger="load"38+ hx-headers='{"HX-Fragment": "hello"}'
32- hx-swap="innerHTML"39+ hx-trigger="load"
33- ></body>40+ hx-swap="outerHTML"
41+ ></div>
42+ </body>
34 </html>43 </html>
M internal/ui/templates/components.gohtml +2 -0
@@ -1,3 +1,5 @@
1+<!-- @format -->
2+
13 <div id="header">
24 <h1
35 class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
@@ -1,3 +1,5 @@
1+<!-- @format -->
2+
1 <div id="header">3 <div id="header">
2 <h14 <h1
3 class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"5 class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
M pkg/utils/fragments.go +13 -6
@@ -1,6 +1,7 @@
11 package utils
22
33 import (
4+ "bytes"
45 "fmt"
56
67 "github.com/PuerkitoBio/goquery"
@@ -8,19 +9,25 @@ import (
89 )
910
1011 // GetHTMLFragmentByID returns the HTML fragment with the specified id
11-func GetHTMLFragmentByID(id string) (string, error) {
12- data, err := ui.TemplateFS.Open("components.gohtmlll")
12+func GetHTMLFragmentByID(id string, data interface{}) (string, error) {
13+ // 1. Render the template
14+ renderer, err := GetRenderer("components.gohtml", ui.TemplateFS, NewLogger().GetTypeLogger(App))
1315 if err != nil {
14- return "", fmt.Errorf("failed to open components: %w", err)
16+ return "", fmt.Errorf("failed to get renderer: %w", err)
1517 }
16- defer data.Close()
1718
18- doc, err := goquery.NewDocumentFromReader(data)
19+ renderedHTML, err := renderer.Render(data)
20+ if err != nil {
21+ return "", fmt.Errorf("failed to render template: %w", err)
22+ }
23+
24+ // 2. Parse the rendered HTML with goquery
25+ doc, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(renderedHTML)))
1926 if err != nil {
2027 return "", fmt.Errorf("failed to parse HTML document: %w", err)
2128 }
2229
23- // Find the div with the specified id
30+ // 3. Find the div with the specified id
2431 fragmentContent, err := doc.Find("#" + id).Html()
2532 if err != nil {
2633 return "", fmt.Errorf("failed to extract HTML for id %s: %w", id, err)
@@ -1,6 +1,7 @@
1 package utils1 package utils
2 2
3 import (3 import (
4+ "bytes"
4 "fmt"5 "fmt"
5 6
6 "github.com/PuerkitoBio/goquery"7 "github.com/PuerkitoBio/goquery"
@@ -8,19 +9,25 @@ import (
8 )9 )
9 10
10 // GetHTMLFragmentByID returns the HTML fragment with the specified id11 // GetHTMLFragmentByID returns the HTML fragment with the specified id
11-func GetHTMLFragmentByID(id string) (string, error) {12+func GetHTMLFragmentByID(id string, data interface{}) (string, error) {
12- data, err := ui.TemplateFS.Open("components.gohtmlll")13+ // 1. Render the template
14+ renderer, err := GetRenderer("components.gohtml", ui.TemplateFS, NewLogger().GetTypeLogger(App))
13 if err != nil {15 if err != nil {
14- return "", fmt.Errorf("failed to open components: %w", err)16+ return "", fmt.Errorf("failed to get renderer: %w", err)
15 }17 }
16- defer data.Close()
17 18
18- doc, err := goquery.NewDocumentFromReader(data)19+ renderedHTML, err := renderer.Render(data)
20+ if err != nil {
21+ return "", fmt.Errorf("failed to render template: %w", err)
22+ }
23+
24+ // 2. Parse the rendered HTML with goquery
25+ doc, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(renderedHTML)))
19 if err != nil {26 if err != nil {
20 return "", fmt.Errorf("failed to parse HTML document: %w", err)27 return "", fmt.Errorf("failed to parse HTML document: %w", err)
21 }28 }
22 29
23- // Find the div with the specified id30+ // 3. Find the div with the specified id
24 fragmentContent, err := doc.Find("#" + id).Html()31 fragmentContent, err := doc.Find("#" + id).Html()
25 if err != nil {32 if err != nil {
26 return "", fmt.Errorf("failed to extract HTML for id %s: %w", id, err)33 return "", fmt.Errorf("failed to extract HTML for id %s: %w", id, err)
M pkg/utils/parser.go +17 -0
@@ -63,4 +63,21 @@ func ReadDataFromYAML() (YamlData, error) {
6363 return data, nil
6464 }
6565
66+// PopulateDataFromYAML retrieves the data from the YAML file and sets the appropriate language data
67+func PopulateDataFromYAML(currentLang string) (YamlData, error) {
68+ data, err := ReadDataFromYAML()
69+ if err != nil {
70+ return data, err
71+ }
72+
73+ switch currentLang {
74+ case "fr":
75+ data.CurrentLang = data.FR
76+ default: // default to English if no match
77+ data.CurrentLang = data.EN
78+ }
79+
80+ return data, nil
81+}
82+
6683 // Json Parser
@@ -63,4 +63,21 @@ func ReadDataFromYAML() (YamlData, error) {
63 return data, nil63 return data, nil
64 }64 }
65 65
66+// PopulateDataFromYAML retrieves the data from the YAML file and sets the appropriate language data
67+func PopulateDataFromYAML(currentLang string) (YamlData, error) {
68+ data, err := ReadDataFromYAML()
69+ if err != nil {
70+ return data, err
71+ }
72+
73+ switch currentLang {
74+ case "fr":
75+ data.CurrentLang = data.FR
76+ default: // default to English if no match
77+ data.CurrentLang = data.EN
78+ }
79+
80+ return data, nil
81+}
82+
66 // Json Parser83 // Json Parser
A uno.config.ts +32 -0
@@ -0,0 +1,32 @@
1+// uno.config.ts
2+import {
3+ defineConfig, presetAttributify, presetIcons,
4+ presetTypography, presetUno, presetWebFonts,
5+ transformerDirectives, transformerVariantGroup
6+} from 'unocss'
7+
8+export default defineConfig({
9+ shortcuts: [
10+ // ...
11+ ],
12+ theme: {
13+ colors: {
14+ // ...
15+ }
16+ },
17+ presets: [
18+ presetUno(),
19+ presetAttributify(),
20+ presetIcons(),
21+ presetTypography(),
22+ presetWebFonts({
23+ fonts: {
24+ // ...
25+ },
26+ }),
27+ ],
28+ transformers: [
29+ transformerDirectives(),
30+ transformerVariantGroup(),
31+ ],
32+})⏎̸
@@ -0,0 +1,32 @@
1+// uno.config.ts
2+import {
3+ defineConfig, presetAttributify, presetIcons,
4+ presetTypography, presetUno, presetWebFonts,
5+ transformerDirectives, transformerVariantGroup
6+} from 'unocss'
7+
8+export default defineConfig({
9+ shortcuts: [
10+ // ...
11+ ],
12+ theme: {
13+ colors: {
14+ // ...
15+ }
16+ },
17+ presets: [
18+ presetUno(),
19+ presetAttributify(),
20+ presetIcons(),
21+ presetTypography(),
22+ presetWebFonts({
23+ fonts: {
24+ // ...
25+ },
26+ }),
27+ ],
28+ transformers: [
29+ transformerDirectives(),
30+ transformerVariantGroup(),
31+ ],
32+})⏎̸

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