From 3c9911da035193eae7a83e332509e853e385bb40 Mon Sep 17 00:00:00 2001 From: Tony Grosinger Date: Sat, 13 Jul 2024 21:20:37 -0700 Subject: [PATCH] First step towards converting to use templ for rendering --- Makefile | 6 +- go.mod | 1 + go.sum | 2 + pkg/funcmap/funcmap.go | 4 + pkg/handlers/pages.go | 27 +- pkg/page/page.go | 14 +- pkg/services/db.go | 2 + pkg/services/posts.go | 31 ++ pkg/services/template_renderer.go | 48 +++ templ/components/messages.templ | 21 ++ templ/components/messages_templ.go | 99 ++++++ templ/layouts/main.templ | 168 ++++++++++ templ/layouts/main_templ.go | 506 +++++++++++++++++++++++++++++ templ/pages/home.templ | 94 ++++++ templ/pages/home_templ.go | 233 +++++++++++++ templates/pages/home.gohtml | 82 ----- 16 files changed, 1226 insertions(+), 112 deletions(-) create mode 100644 pkg/services/posts.go create mode 100644 templ/components/messages.templ create mode 100644 templ/components/messages_templ.go create mode 100644 templ/layouts/main.templ create mode 100644 templ/layouts/main_templ.go create mode 100644 templ/pages/home.templ create mode 100644 templ/pages/home_templ.go delete mode 100644 templates/pages/home.gohtml diff --git a/Makefile b/Makefile index 815cad9..a4f968b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Run the application .PHONY: run -run: +run: sqlc templ clear go run cmd/web/main.go @@ -14,6 +14,10 @@ sqlc: rm -f pkg/models/sqlc/* sqlc generate +.PHONY: templ +templ: + templ generate + # Check for direct dependency updates .PHONY: check-updates check-updates: diff --git a/go.mod b/go.mod index 404d900..ecf92a9 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( ariga.io/atlas v0.21.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect + github.com/a-h/templ v0.2.747 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect diff --git a/go.sum b/go.sum index ddeda86..f85c2d4 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VP github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= +github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg= +github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= diff --git a/pkg/funcmap/funcmap.go b/pkg/funcmap/funcmap.go index b359183..f5d53b6 100644 --- a/pkg/funcmap/funcmap.go +++ b/pkg/funcmap/funcmap.go @@ -53,6 +53,10 @@ func (fm *funcMap) file(filepath string) string { return fmt.Sprintf("/%s/%s?v=%s", config.StaticPrefix, filepath, CacheBuster) } +func File(filepath string) string { + return fmt.Sprintf("/%s/%s?v=%s", config.StaticPrefix, filepath, CacheBuster) +} + // link outputs HTML for a link element, providing the ability to dynamically set the active class func (fm *funcMap) link(url, text, currentPath string, classes ...string) template.HTML { if currentPath == url { diff --git a/pkg/handlers/pages.go b/pkg/handlers/pages.go index 37701e5..ebe00c5 100644 --- a/pkg/handlers/pages.go +++ b/pkg/handlers/pages.go @@ -1,13 +1,13 @@ package handlers import ( - "fmt" "html/template" "github.com/labstack/echo/v4" "git.grosinger.net/tgrosinger/saasitone/pkg/page" "git.grosinger.net/tgrosinger/saasitone/pkg/services" + "git.grosinger.net/tgrosinger/saasitone/templ/pages" "git.grosinger.net/tgrosinger/saasitone/templates" ) @@ -19,11 +19,7 @@ const ( type ( Pages struct { *services.TemplateRenderer - } - - post struct { - Title string - Body string + *services.DBClient } aboutData struct { @@ -44,6 +40,7 @@ func init() { func (h *Pages) Init(c *services.Container) error { h.TemplateRenderer = c.TemplateRenderer + h.DBClient = c.DB return nil } @@ -59,23 +56,11 @@ func (h *Pages) Home(ctx echo.Context) error { p.Metatags.Description = "Welcome to the homepage." p.Metatags.Keywords = []string{"Go", "MVC", "Web", "Software"} p.Pager = page.NewPager(ctx, 4) - p.Data = h.fetchPosts(&p.Pager) - return h.RenderPage(ctx, p) -} + data := h.Post.FetchAll(&p.Pager) + component := pages.Home(p, data) -// fetchPosts is an mock example of fetching posts to illustrate how paging works -func (h *Pages) fetchPosts(pager *page.Pager) []post { - pager.SetItems(20) - posts := make([]post, 20) - - for k := range posts { - posts[k] = post{ - Title: fmt.Sprintf("Post example #%d", k+1), - Body: fmt.Sprintf("Lorem ipsum example #%d ddolor sit amet, consectetur adipiscing elit. Nam elementum vulputate tristique.", k+1), - } - } - return posts[pager.GetOffset() : pager.GetOffset()+pager.ItemsPerPage] + return h.RenderPageTempl(ctx, p, component) } func (h *Pages) About(ctx echo.Context) error { diff --git a/pkg/page/page.go b/pkg/page/page.go index 7252de2..9826c24 100644 --- a/pkg/page/page.go +++ b/pkg/page/page.go @@ -1,7 +1,6 @@ package page import ( - "html/template" "net/http" "time" @@ -39,6 +38,9 @@ type Page struct { // URL stores the URL of the current request URL string + // ToURL is a function to convert a route name and optional route parameters to a URL + ToURL func(name string, params ...interface{}) string + // Data stores whatever additional data that needs to be passed to the templates. // This is what the handler uses to pass the content of the page. Data any @@ -127,6 +129,7 @@ func New(ctx echo.Context) Page { Context: ctx, Path: ctx.Request().URL.Path, URL: ctx.Request().URL.String(), + ToURL: ctx.Echo().Reverse, StatusCode: http.StatusOK, Pager: NewPager(ctx, DefaultItemsPerPage), Headers: make(map[string]string), @@ -151,11 +154,6 @@ func New(ctx echo.Context) Page { // GetMessages gets all flash messages for a given type. // This allows for easy access to flash messages from the templates. -func (p Page) GetMessages(typ msg.Type) []template.HTML { - strs := msg.Get(p.Context, typ) - ret := make([]template.HTML, len(strs)) - for k, v := range strs { - ret[k] = template.HTML(v) - } - return ret +func (p Page) GetMessages(typ msg.Type) []string { + return msg.Get(p.Context, typ) } diff --git a/pkg/services/db.go b/pkg/services/db.go index cc9673c..54e2ef3 100644 --- a/pkg/services/db.go +++ b/pkg/services/db.go @@ -18,6 +18,7 @@ type DBClient struct { C *sqlc.Queries User *DBUserClient + Post *DBPostClient } func NewDBClient(cfg *config.Config) (*DBClient, error) { @@ -43,6 +44,7 @@ func NewDBClient(cfg *config.Config) (*DBClient, error) { C: sqlc.New(db), } client.User = &DBUserClient{db: db} + client.Post = &DBPostClient{db: db} migrationsDirPath := cfg.Storage.MigrationsDir logger.Info("Loading schema migrations", diff --git a/pkg/services/posts.go b/pkg/services/posts.go new file mode 100644 index 0000000..a8206b8 --- /dev/null +++ b/pkg/services/posts.go @@ -0,0 +1,31 @@ +package services + +import ( + "database/sql" + "fmt" + + "git.grosinger.net/tgrosinger/saasitone/pkg/page" +) + +type Post struct { + Title string + Body string +} + +type DBPostClient struct { + db *sql.DB +} + +// FetchAll is an mock example of fetching posts to illustrate how paging works +func (c *DBPostClient) FetchAll(pager *page.Pager) []Post { + pager.SetItems(20) + posts := make([]Post, 20) + + for k := range posts { + posts[k] = Post{ + Title: fmt.Sprintf("Post example #%d", k+1), + Body: fmt.Sprintf("Lorem ipsum example #%d ddolor sit amet, consectetur adipiscing elit. Nam elementum vulputate tristique.", k+1), + } + } + return posts[pager.GetOffset() : pager.GetOffset()+pager.ItemsPerPage] +} diff --git a/pkg/services/template_renderer.go b/pkg/services/template_renderer.go index ef73097..796cab3 100644 --- a/pkg/services/template_renderer.go +++ b/pkg/services/template_renderer.go @@ -9,12 +9,14 @@ import ( "net/http" "sync" + "github.com/a-h/templ" "github.com/labstack/echo/v4" "git.grosinger.net/tgrosinger/saasitone/config" "git.grosinger.net/tgrosinger/saasitone/pkg/context" "git.grosinger.net/tgrosinger/saasitone/pkg/log" "git.grosinger.net/tgrosinger/saasitone/pkg/page" + "git.grosinger.net/tgrosinger/saasitone/templ/layouts" "git.grosinger.net/tgrosinger/saasitone/templates" ) @@ -96,6 +98,52 @@ func (t *TemplateRenderer) Parse() *templateBuilder { } } +func (t *TemplateRenderer) RenderPageTempl(ctx echo.Context, page page.Page, content templ.Component) error { + // Page name is required + if page.Name == "" { + return echo.NewHTTPError(http.StatusInternalServerError, "page render failed due to missing name") + } + + // Use the app name in configuration if a value was not set + if page.AppName == "" { + page.AppName = t.config.App.Name + } + + var err error + var buf bytes.Buffer + if page.HTMX.Request.Enabled && !page.HTMX.Request.Boosted { + // This is an HTMX non-boosted request. + // Only partial content should be rendered. + err = content.Render(ctx.Request().Context(), &buf) + } else { + err = layouts.Main(page, content).Render(ctx.Request().Context(), &buf) + } + if err != nil { + return echo.NewHTTPError( + http.StatusInternalServerError, + fmt.Sprintf("failed to parse and execute templates: %s", err), + ) + } + + // Set the status code + ctx.Response().Status = page.StatusCode + + // Set any headers + for k, v := range page.Headers { + ctx.Response().Header().Set(k, v) + } + + // Apply the HTMX response, if one + if page.HTMX.Response != nil { + page.HTMX.Response.Apply(ctx) + } + + // Cache this page, if caching was enabled + t.cachePage(ctx, page, &buf) + + return ctx.HTMLBlob(ctx.Response().Status, buf.Bytes()) +} + // RenderPage renders a Page as an HTTP response func (t *TemplateRenderer) RenderPage(ctx echo.Context, page page.Page) error { var buf *bytes.Buffer diff --git a/templ/components/messages.templ b/templ/components/messages.templ new file mode 100644 index 0000000..b53a2b4 --- /dev/null +++ b/templ/components/messages.templ @@ -0,0 +1,21 @@ +package components + +import ( + "git.grosinger.net/tgrosinger/saasitone/pkg/page" + "git.grosinger.net/tgrosinger/saasitone/pkg/msg" +) + +templ Messages(p page.Page) { + for _, msgType := range []msg.Type{msg.TypeSuccess, msg.TypeInfo, msg.TypeWarning, msg.TypeDanger} { + for _, msg := range p.GetMessages(msgType) { + @message(msgType, msg) + } + } +} + +templ message(msgType msg.Type, msg string) { +
+ + { msg } +
+} diff --git a/templ/components/messages_templ.go b/templ/components/messages_templ.go new file mode 100644 index 0000000..0bf9d65 --- /dev/null +++ b/templ/components/messages_templ.go @@ -0,0 +1,99 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.707 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import ( + "git.grosinger.net/tgrosinger/saasitone/pkg/msg" + "git.grosinger.net/tgrosinger/saasitone/pkg/page" +) + +func Messages(p page.Page) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + for _, msgType := range []msg.Type{msg.TypeSuccess, msg.TypeInfo, msg.TypeWarning, msg.TypeDanger} { + for _, msg := range p.GetMessages(msgType) { + templ_7745c5c3_Err = message(msgType, msg).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func message(msgType msg.Type, msg string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var3 = []any{"notification is-light", "is-" + msgType} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(msg) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/components/messages.templ`, Line: 19, Col: 7} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/templ/layouts/main.templ b/templ/layouts/main.templ new file mode 100644 index 0000000..d9f2101 --- /dev/null +++ b/templ/layouts/main.templ @@ -0,0 +1,168 @@ +package layouts + +import ( + "strings" + + "git.grosinger.net/tgrosinger/saasitone/pkg/page" + "git.grosinger.net/tgrosinger/saasitone/pkg/funcmap" + "git.grosinger.net/tgrosinger/saasitone/templ/components" +) + +templ Main(p page.Page, content templ.Component) { + + + + @metatags(p) + @css() + @js() + + + +
+
+
+ +
+
+
+ if p.Title != "" { +

{ p.Title }

+ } + @components.Messages(p) + @content +
+
+
+
+ @footer(p) + + +} + +templ link(p page.Page, url, text, classes string) { + { text } +} + +templ search(p page.Page) { + +} + +templ metatags(p page.Page) { + if p.Title != "" { + { p.AppName } | { p.Title } + } else { + { p.AppName } + } + + + + + if p.Metatags.Description != "" { + + } + if len(p.Metatags.Keywords) > 0 { + + } +} + +templ css() { + +} + +templ js() { + + +} + +templ footer(p page.Page) { + if p.CSRF != "" { + + } + +} diff --git a/templ/layouts/main_templ.go b/templ/layouts/main_templ.go new file mode 100644 index 0000000..3126add --- /dev/null +++ b/templ/layouts/main_templ.go @@ -0,0 +1,506 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.707 +package layouts + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import ( + "strings" + + "git.grosinger.net/tgrosinger/saasitone/pkg/funcmap" + "git.grosinger.net/tgrosinger/saasitone/pkg/page" + "git.grosinger.net/tgrosinger/saasitone/templ/components" +) + +func Main(p page.Page, content templ.Component) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = metatags(p).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = css().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = js().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if p.Title != "" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/layouts/main.templ`, Line: 77, Col: 35} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = components.Messages(p).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = content.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = footer(p).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func link(p page.Page, url, text, classes string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var6 = []any{classes, templ.KV("is-active", p.Path == url)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/layouts/main.templ`, Line: 91, Col: 90} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func search(p page.Page) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Search

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func metatags(p page.Page) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if p.Title != "" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(p.AppName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/layouts/main.templ`, Line: 125, Col: 20} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" | ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/layouts/main.templ`, Line: 125, Col: 34} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(p.AppName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/layouts/main.templ`, Line: 127, Col: 20} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if p.Metatags.Description != "" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(p.Metatags.Keywords) > 0 { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func css() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var18 := templ.GetChildren(ctx) + if templ_7745c5c3_Var18 == nil { + templ_7745c5c3_Var18 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func js() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var19 := templ.GetChildren(ctx) + if templ_7745c5c3_Var19 == nil { + templ_7745c5c3_Var19 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func footer(p page.Page) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var20 := templ.GetChildren(ctx) + if templ_7745c5c3_Var20 == nil { + templ_7745c5c3_Var20 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if p.CSRF != "" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/templ/pages/home.templ b/templ/pages/home.templ new file mode 100644 index 0000000..ec50634 --- /dev/null +++ b/templ/pages/home.templ @@ -0,0 +1,94 @@ +package pages + +import ( + "strconv" + + "git.grosinger.net/tgrosinger/saasitone/pkg/page" + "git.grosinger.net/tgrosinger/saasitone/pkg/services" + "git.grosinger.net/tgrosinger/saasitone/pkg/funcmap" +) + +templ Home(p page.Page, posts []services.Post) { + if (p.HTMX.Request.Target != "posts") { + @topContent(p) + } +
+ for _, post := range posts { +
+
+

+ Gopher +

+
+
+
+

+ { post.Title } +
+ { post.Body } +

+
+
+
+ } +
+ if !p.Pager.IsBeginning() { +

+ +

+ } + if !p.Pager.IsEnd() { +

+ +

+ } +
+
+ if (p.HTMX.Request.Target != "posts") { + @fileMsg() + } +} + +templ topContent(p page.Page) { +
+
+
+

+ if p.IsAuth { + Hello, { p.AuthUser.Name } + } else { + Hello + } +

+

+ if p.IsAuth { + Welcome back! + } else { + Please login in to your account. + } +

+
+
+
+
+

Recent posts

+

+ Below is an example of both paging and AJAX fetching using HTMX +

+
+} + +templ fileMsg() { +
+
+
+

Serving files

+ +
+
+ In the example posts above, check how the file URL contains a cache-buster query parameter which changes only when the app is restarted. + Static files also contain cache-control headers which are configured via middleware. + You can also use AlpineJS to dismiss this message. +
+
+} diff --git a/templ/pages/home_templ.go b/templ/pages/home_templ.go new file mode 100644 index 0000000..e1519d1 --- /dev/null +++ b/templ/pages/home_templ.go @@ -0,0 +1,233 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.707 +package pages + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import ( + "strconv" + + "git.grosinger.net/tgrosinger/saasitone/pkg/funcmap" + "git.grosinger.net/tgrosinger/saasitone/pkg/page" + "git.grosinger.net/tgrosinger/saasitone/pkg/services" +) + +func Home(p page.Page, posts []services.Post) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if p.HTMX.Request.Target != "posts" { + templ_7745c5c3_Err = topContent(p).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, post := range posts { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

\"Gopher\"

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/pages/home.templ`, Line: 26, Col: 27} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(post.Body) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/pages/home.templ`, Line: 28, Col: 18} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !p.Pager.IsBeginning() { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if !p.Pager.IsEnd() { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if p.HTMX.Request.Target != "posts" { + templ_7745c5c3_Err = fileMsg(p).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func topContent(p page.Page) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if p.IsAuth { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Hello, ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(p.AuthUser.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/pages/home.templ`, Line: 58, Col: 30} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Hello") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if p.IsAuth { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Welcome back!") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Please login in to your account.") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Recent posts

Below is an example of both paging and AJAX fetching using HTMX

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func fileMsg(p page.Page) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Serving files

In the example posts above, check how the file URL contains a cache-buster query parameter which changes only when the app is restarted. Static files also contain cache-control headers which are configured via middleware. You can also use AlpineJS to dismiss this message.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/templates/pages/home.gohtml b/templates/pages/home.gohtml deleted file mode 100644 index 9c89fc1..0000000 --- a/templates/pages/home.gohtml +++ /dev/null @@ -1,82 +0,0 @@ -{{define "content"}} - {{- if not (eq .HTMX.Request.Target "posts")}} - {{template "top-content" .}} - {{- end}} - - {{template "posts" .}} - - {{- if not (eq .HTMX.Request.Target "posts")}} - {{template "file-msg" .}} - {{- end}} -{{end}} - -{{define "top-content"}} -
-
-
-

- Hello{{if .IsAuth}}, {{.AuthUser.Name}}{{end}} -

-

{{if .IsAuth}}Welcome back!{{else}}Please login in to your account.{{end}}

-
-
-
- -
-

Recent posts

-

- Below is an example of both paging and AJAX fetching using HTMX -

-
-{{end}} - -{{define "posts"}} -
- {{- range .Data}} -
-
-

- Gopher -

-
-
-
-

- {{.Title}} -
- {{.Body}} -

-
-
-
- {{- end}} - -
- {{- if not $.Pager.IsBeginning}} -

- -

- {{- end}} - {{- if not $.Pager.IsEnd}} -

- -

- {{- end}} -
-
-{{end}} - -{{define "file-msg"}} -
-
-
-

Serving files

- -
-
- In the example posts above, check how the file URL contains a cache-buster query parameter which changes only when the app is restarted. - Static files also contain cache-control headers which are configured via middleware. - You can also use AlpineJS to dismiss this message. -
-
-{{end}} \ No newline at end of file