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

View file

@ -0,0 +1,64 @@
package components
import (
"github.com/mikestefanello/pagoda/pkg/msg"
"github.com/mikestefanello/pagoda/pkg/ui"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func FlashMessages(r *ui.Request) Node {
var g Group
for _, typ := range []msg.Type{
msg.TypeSuccess,
msg.TypeInfo,
msg.TypeWarning,
msg.TypeDanger,
} {
for _, str := range msg.Get(r.Context, typ) {
g = append(g, Notification(typ, str))
}
}
return g
}
func Notification(typ msg.Type, text string) Node {
var class string
switch typ {
case msg.TypeSuccess:
class = "success"
case msg.TypeInfo:
class = "info"
case msg.TypeWarning:
class = "warning"
case msg.TypeDanger:
class = "danger"
}
return Div(
Class("notification is-"+class),
Attr("x-data", "{show: true}"),
Attr("x-show", "show"),
Button(
Class("delete"),
Attr("@click", "show = false"),
),
Text(text),
)
}
func Message(class, header string, body Node) Node {
return Article(
Class("message "+class),
If(header != "", Div(
Class("message-header"),
P(Text(header)),
)),
Div(
Class("message-body"),
body,
),
)
}

203
pkg/ui/components/form.go Normal file
View file

@ -0,0 +1,203 @@
package components
import (
"github.com/mikestefanello/pagoda/pkg/form"
"github.com/mikestefanello/pagoda/pkg/ui"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type (
InputFieldParams struct {
Form form.Form
FormField string
Name string
InputType string
Label string
Value string
Placeholder string
Help string
}
RadiosParams struct {
Form form.Form
FormField string
Name string
Label string
Value string
Options []Radio
}
Radio struct {
Value string
Label string
}
TextareaFieldParams struct {
Form form.Form
FormField string
Name string
Label string
Value string
Help string
}
)
func ControlGroup(controls ...Node) Node {
g := make(Group, len(controls))
for i, control := range controls {
g[i] = Div(
Class("control"),
control,
)
}
return Div(
Class("field is-grouped"),
g,
)
}
func TextareaField(el TextareaFieldParams) Node {
return Div(
Class("field"),
Label(
For("name"),
Class("label"),
Text(el.Label),
),
Div(
Class("control"),
Textarea(
ID(el.Name),
Name(el.Name),
Class("textarea "+formFieldStatusClass(el.Form, el.FormField)),
Text(el.Value),
),
),
If(el.Help != "", P(Class("help"), Text(el.Help))),
formFieldErrors(el.Form, el.FormField),
)
}
func Radios(el RadiosParams) Node {
buttons := make(Group, len(el.Options))
for i, opt := range el.Options {
buttons[i] = Label(
Class("radio"),
Input(
Type("radio"),
Name(el.Name),
Value(opt.Value),
If(el.Value == opt.Value, Checked()),
),
Text(" "+opt.Label),
)
}
return Div(
Class("control field"),
Label(Class("label"), Text(el.Label)),
Div(
Class("radios"),
buttons,
),
formFieldErrors(el.Form, el.FormField),
)
}
func InputField(el InputFieldParams) Node {
return Div(
Class("field"),
Label(
Class("label"),
For(el.Name),
Text(el.Label),
),
Div(
Class("control"),
Input(
ID(el.Name),
Name(el.Name),
Type(el.InputType),
If(el.Placeholder != "", Placeholder(el.Placeholder)),
Class("input "+formFieldStatusClass(el.Form, el.FormField)),
Value(el.Value),
),
),
If(el.Help != "", P(Class("help"), Text(el.Help))),
formFieldErrors(el.Form, el.FormField),
)
}
func FileField(name, label string) Node {
return Div(
Class("field file"),
Label(
Class("file-label"),
Input(
Class("file-input"),
Type("file"),
Name(name),
),
Span(
Class("file-cta"),
Span(
Class("file-label"),
Text(label),
),
),
),
)
}
func formFieldStatusClass(fm form.Form, formField string) string {
switch {
case !fm.IsSubmitted():
return ""
case fm.FieldHasErrors(formField):
return "is-danger"
default:
return "is-success"
}
}
func formFieldErrors(fm form.Form, field string) Node {
errs := fm.GetFieldErrors(field)
if len(errs) == 0 {
return nil
}
g := make(Group, len(errs))
for i, err := range errs {
g[i] = P(
Class("help is-danger"),
Text(err),
)
}
return g
}
func CSRF(r *ui.Request) Node {
return Input(
Type("hidden"),
Name("csrf"),
Value(r.CSRF),
)
}
func FormButton(class, label string) Node {
return Button(
Class("button "+class),
Text(label),
)
}
func ButtonLink(href, class, label string) Node {
return A(
Href(href),
Class("button "+class),
Text(label),
)
}

60
pkg/ui/components/head.go Normal file
View file

@ -0,0 +1,60 @@
package components
import (
"fmt"
"strings"
"github.com/mikestefanello/pagoda/pkg/ui"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func JS(r *ui.Request) Node {
const htmxErr = `
document.body.addEventListener('htmx:beforeSwap', function(evt) {
if (evt.detail.xhr.status >= 400){
evt.detail.shouldSwap = true;
evt.detail.target = htmx.find("body");
}
});
`
const htmxCSRF = `
document.body.addEventListener('htmx:configRequest', function(evt) {
if (evt.detail.verb !== "get") {
evt.detail.parameters['csrf'] = '%s';
}
})
`
var csrf Node
if len(r.CSRF) > 0 {
csrf = Script(Raw(fmt.Sprintf(htmxCSRF, r.CSRF)))
}
return Group{
Script(Src("https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js")),
Script(Src("https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"), Defer()),
Script(Raw(htmxErr)),
csrf,
}
}
func CSS() Node {
return Link(
Href("https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css"),
Rel("stylesheet"),
)
}
func Metatags(r *ui.Request) Node {
return Group{
Meta(Charset("utf-8")),
Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
Link(Rel("icon"), Href(ui.File("favicon.png"))),
TitleEl(Text(r.Config.App.Name), If(r.Title != "", Text(" | "+r.Title))),
If(r.Metatags.Description != "", Meta(Name("description"), Content(r.Metatags.Description))),
If(len(r.Metatags.Keywords) > 0, Meta(Name("keywords"), Content(strings.Join(r.Metatags.Keywords, ", ")))),
}
}

View file

@ -0,0 +1,9 @@
package components
import (
. "maragu.dev/gomponents"
)
func HxBoost() Node {
return Attr("hx-boost", "true")
}

19
pkg/ui/components/nav.go Normal file
View file

@ -0,0 +1,19 @@
package components
import (
"github.com/mikestefanello/pagoda/pkg/ui"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func MenuLink(r *ui.Request, title, routeName string, routeParams ...any) Node {
href := r.Path(routeName, routeParams...)
return Li(
A(
Href(href),
Text(title),
If(href == r.CurrentPath, Class("is-active")),
),
)
}

56
pkg/ui/components/tabs.go Normal file
View file

@ -0,0 +1,56 @@
package components
import (
"fmt"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type Tab struct {
Title, Body string
}
func Tabs(heading, description string, items []Tab) Node {
renderTitles := func() Node {
g := make(Group, len(items))
for i, item := range items {
g[i] = Li(
Attr(":class", fmt.Sprintf("{'is-active': tab === %d}", i)),
Attr("@click", fmt.Sprintf("tab = %d", i)),
A(Text(item.Title)),
)
}
return g
}
renderBodies := func() Node {
g := make(Group, len(items))
for i, item := range items {
g[i] = Div(
Attr("x-show", fmt.Sprintf("tab == %d", i)),
P(Raw(" "+item.Body)),
)
}
return g
}
return Div(
P(
Class("subtitle mt-5"),
Text(heading),
),
P(
Class("mb-4"),
Text(description),
),
Div(
Attr("x-data", "{tab: 0}"),
Div(
Class("tabs"),
Ul(renderTitles()),
),
renderBodies(),
),
)
}