Migrate from templates to Gomponents (#103)

This commit is contained in:
Mike Stefanello 2025-03-05 20:01:58 -05:00 committed by GitHub
parent 0bf9ab7189
commit 051d032038
104 changed files with 2768 additions and 2824 deletions

58
pkg/ui/pages/about.go Normal file
View file

@ -0,0 +1,58 @@
package pages
import (
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/pkg/ui"
"github.com/mikestefanello/pagoda/pkg/ui/cache"
. "github.com/mikestefanello/pagoda/pkg/ui/components"
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func About(ctx echo.Context) error {
r := ui.NewRequest(ctx)
r.Title = "About"
r.Metatags.Description = "Learn a little about what's included in Pagoda."
// The tabs are static so we can render and cache them.
tabs := cache.SetIfNotExists("pages.about.Tabs", func() Node {
return Group{
Tabs(
"Frontend",
"The following incredible projects make developing advanced, modern frontends possible and simple without having to write a single line of JS or CSS. You can go extremely far without leaving the comfort of Go with server-side rendered HTML.",
[]Tab{
{
Title: "HTMX",
Body: "Completes HTML as a hypertext by providing attributes to AJAXify anything and much more. Visit <a href=\"https://htmx.org/\">htmx.org</a> to learn more.",
},
{
Title: "Alpine.js",
Body: "Drop-in, Vue-like functionality written directly in your markup. Visit <a href=\"https://alpinejs.dev/\">alpinejs.dev</a> to learn more.",
},
{
Title: "Bulma",
Body: "Ready-to-use frontend components that you can easily combine to build responsive web interfaces with no JavaScript requirements. Visit <a href=\"https://bulma.io/\">bulma.io</a> to learn more.",
},
},
),
Div(Class("mb-4")),
Tabs(
"Backend",
"The following incredible projects provide the foundation of the Go backend. See the repository for a complete list of included projects.",
[]Tab{
{
Title: "Echo",
Body: "High performance, extensible, minimalist Go web framework. Visit <a href=\"https://echo.labstack.com/\">echo.labstack.com</a> to learn more.",
},
{
Title: "Ent",
Body: "Simple, yet powerful ORM for modeling and querying data. Visit <a href=\"https://entgo.io/\">entgo.io</a> to learn more.",
},
},
),
}
})
return r.Render(layouts.Primary, tabs)
}

46
pkg/ui/pages/auth.go Normal file
View file

@ -0,0 +1,46 @@
package pages
import (
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/pkg/ui"
"github.com/mikestefanello/pagoda/pkg/ui/forms"
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func Login(ctx echo.Context, form *forms.Login) error {
r := ui.NewRequest(ctx)
r.Title = "Login"
return r.Render(layouts.Auth, form.Render(r))
}
func Register(ctx echo.Context, form *forms.Register) error {
r := ui.NewRequest(ctx)
r.Title = "Register"
return r.Render(layouts.Auth, form.Render(r))
}
func ForgotPassword(ctx echo.Context, form *forms.ForgotPassword) error {
r := ui.NewRequest(ctx)
r.Title = "Forgot password"
g := Group{
Div(
Class("content"),
P(Text("Enter your email address and we'll email you a link that allows you to reset your password.")),
),
form.Render(r),
}
return r.Render(layouts.Auth, g)
}
func ResetPassword(ctx echo.Context, form *forms.ResetPassword) error {
r := ui.NewRequest(ctx)
r.Title = "Reset your password"
return r.Render(layouts.Auth, form.Render(r))
}

15
pkg/ui/pages/cache.go Normal file
View file

@ -0,0 +1,15 @@
package pages
import (
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/pkg/ui"
"github.com/mikestefanello/pagoda/pkg/ui/forms"
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
)
func UpdateCache(ctx echo.Context, form *forms.Cache) error {
r := ui.NewRequest(ctx)
r.Title = "Set a cache entry"
return r.Render(layouts.Primary, form.Render(r))
}

41
pkg/ui/pages/contact.go Normal file
View file

@ -0,0 +1,41 @@
package pages
import (
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/pkg/ui"
"github.com/mikestefanello/pagoda/pkg/ui/components"
"github.com/mikestefanello/pagoda/pkg/ui/forms"
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func ContactUs(ctx echo.Context, form *forms.Contact) error {
r := ui.NewRequest(ctx)
r.Title = "Contact us"
r.Metatags.Description = "Get in touch with us."
g := make(Group, 0)
if r.Htmx.Target != "contact" {
g = append(g, components.Message(
"is-link",
"",
Group{
P(Text("This is an example of a form with inline, server-side validation and HTMX-powered AJAX submissions without writing a single line of JavaScript.")),
P(Text("Only the form below will update async upon submission.")),
}))
}
if form.IsDone() {
g = append(g, components.Message(
"is-large is-success",
"Thank you!",
Text("No email was actually sent but this entire operation was handled server-side and degrades without JavaScript enabled."),
))
} else {
g = append(g, form.Render(r))
}
return r.Render(layouts.Primary, g)
}

38
pkg/ui/pages/error.go Normal file
View file

@ -0,0 +1,38 @@
package pages
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/pkg/routenames"
"github.com/mikestefanello/pagoda/pkg/ui"
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func Error(ctx echo.Context, code int) error {
r := ui.NewRequest(ctx)
r.Title = http.StatusText(code)
var body Node
switch code {
case http.StatusInternalServerError:
body = Text("Please try again.")
case http.StatusForbidden, http.StatusUnauthorized:
body = Text("You are not authorized to view the requested page.")
case http.StatusNotFound:
body = Group{
Text("Click "),
A(
Href(r.Path(routenames.Home)),
Text("here"),
),
Text(" to go return home."),
}
default:
body = Text("Something went wrong.")
}
return r.Render(layouts.Primary, P(body))
}

53
pkg/ui/pages/file.go Normal file
View file

@ -0,0 +1,53 @@
package pages
import (
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/pkg/ui"
. "github.com/mikestefanello/pagoda/pkg/ui/components"
"github.com/mikestefanello/pagoda/pkg/ui/forms"
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
"github.com/mikestefanello/pagoda/pkg/ui/models"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func UploadFile(ctx echo.Context, files []*models.File) error {
r := ui.NewRequest(ctx)
r.Title = "Upload a file"
fileList := make(Group, len(files))
for i, file := range files {
fileList[i] = file.Render()
}
n := Group{
Message(
"is-link",
"",
P(Text("This is a very basic example of how to handle file uploads. Files uploaded will be saved to the directory specified in your configuration.")),
),
Hr(),
forms.File{}.Render(r),
Hr(),
H3(
Class("title"),
Text("Uploaded files"),
),
Message("is-warning", "", P(Text("Below are all files in the configured upload directory."))),
Table(
Class("table"),
THead(
Tr(
Th(Text("Filename")),
Th(Text("Size")),
Th(Text("Modified on")),
),
),
TBody(
fileList,
),
),
}
return r.Render(layouts.Primary, n)
}

69
pkg/ui/pages/home.go Normal file
View file

@ -0,0 +1,69 @@
package pages
import (
"fmt"
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/pkg/routenames"
"github.com/mikestefanello/pagoda/pkg/ui"
. "github.com/mikestefanello/pagoda/pkg/ui/components"
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
"github.com/mikestefanello/pagoda/pkg/ui/models"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func Home(ctx echo.Context, posts *models.Posts) error {
r := ui.NewRequest(ctx)
r.Metatags.Description = "This is the home page."
r.Metatags.Keywords = []string{"Software", "Coding", "Go"}
g := make(Group, 0)
if r.Htmx.Target != "posts" {
var hello string
if r.IsAuth {
hello = fmt.Sprintf("Hello, %s", r.AuthUser.Name)
} else {
hello = "Hello"
}
g = append(g,
Section(
Class("hero is-info welcome is-small mb-5"),
Div(
Class("hero-body"),
Div(
Class("container"),
H1(
Class("title"),
Text(hello),
),
H2(
Class("subtitle"),
If(!r.IsAuth, Text("Please login in to your account.")),
If(r.IsAuth, Text("Welcome back!")),
),
),
),
),
H2(Class("title"), Text("Recent posts")),
H3(Class("subtitle"), Text("Below is an example of both paging and AJAX fetching using HTMX")),
)
}
g = append(g, posts.Render(r.Path(routenames.Home)))
if r.Htmx.Target != "posts" {
g = append(g, Message(
"is-small is-warning mt-5",
"Serving files",
Group{
Text("In the example posts above, check how the file URL contains a cache-buster query parameter which changes only when the app is restarted. "),
Text("Static files also contain cache-control headers which are configured via middleware."),
},
))
}
return r.Render(layouts.Primary, g)
}

20
pkg/ui/pages/search.go Normal file
View file

@ -0,0 +1,20 @@
package pages
import (
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/pkg/ui"
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
"github.com/mikestefanello/pagoda/pkg/ui/models"
. "maragu.dev/gomponents"
)
func SearchResults(ctx echo.Context, results []*models.SearchResult) error {
r := ui.NewRequest(ctx)
g := make(Group, len(results))
for i, result := range results {
g[i] = result.Render()
}
return r.Render(layouts.Primary, g)
}

33
pkg/ui/pages/task.go Normal file
View file

@ -0,0 +1,33 @@
package pages
import (
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/pkg/ui"
"github.com/mikestefanello/pagoda/pkg/ui/components"
"github.com/mikestefanello/pagoda/pkg/ui/forms"
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func AddTask(ctx echo.Context, form *forms.Task) error {
r := ui.NewRequest(ctx)
r.Title = "Create a task"
r.Metatags.Description = "Test creating a task to see how it works."
g := make(Group, 0)
if r.Htmx.Target != "task" {
g = append(g, components.Message(
"is-link",
"",
Group{
P(Raw("Submitting this form will create an <i>ExampleTask</i> in the task queue. After the specified delay, the message will be logged by the queue processor.")),
P(Text("See pkg/tasks and the README for more information.")),
}))
}
g = append(g, form.Render(r))
return r.Render(layouts.Primary, g)
}