Swap Bulma for DaisyUI (Tailwind) (#111)
This commit is contained in:
parent
fc5db0e95a
commit
c1e9baabe6
53 changed files with 1124 additions and 632 deletions
|
|
@ -3,62 +3,64 @@ package components
|
|||
import (
|
||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/icons"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func FlashMessages(r *ui.Request) Node {
|
||||
var g Group
|
||||
var color Color
|
||||
|
||||
for _, typ := range []msg.Type{
|
||||
msg.TypeSuccess,
|
||||
msg.TypeInfo,
|
||||
msg.TypeWarning,
|
||||
msg.TypeDanger,
|
||||
msg.TypeError,
|
||||
} {
|
||||
for _, str := range msg.Get(r.Context, typ) {
|
||||
g = append(g, Notification(typ, str))
|
||||
switch typ {
|
||||
case msg.TypeSuccess:
|
||||
color = ColorSuccess
|
||||
case msg.TypeInfo:
|
||||
color = ColorInfo
|
||||
case msg.TypeWarning:
|
||||
color = ColorWarning
|
||||
case msg.TypeError:
|
||||
color = ColorError
|
||||
}
|
||||
|
||||
g = append(g, Alert(color, str))
|
||||
}
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
func Notification(typ msg.Type, text string) Node {
|
||||
func Alert(color Color, 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"
|
||||
switch color {
|
||||
case ColorSuccess:
|
||||
class = "alert-success"
|
||||
case ColorInfo:
|
||||
class = "alert-info"
|
||||
case ColorWarning:
|
||||
class = "alert-warning"
|
||||
case ColorError:
|
||||
class = "alert-error"
|
||||
}
|
||||
|
||||
return Div(
|
||||
Class("notification is-"+class),
|
||||
Role("alert"),
|
||||
Class("alert mb-2 "+class),
|
||||
Attr("x-data", "{show: true}"),
|
||||
Attr("x-show", "show"),
|
||||
Button(
|
||||
Class("delete"),
|
||||
Span(
|
||||
Attr("@click", "show = false"),
|
||||
Class("cursor-pointer"),
|
||||
icons.XCircle(),
|
||||
),
|
||||
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,
|
||||
),
|
||||
Span(Text(text)),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
121
pkg/ui/components/data.go
Normal file
121
pkg/ui/components/data.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package components
|
||||
|
||||
import (
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
type (
|
||||
CardParams struct {
|
||||
Title string
|
||||
Body Group
|
||||
Footer Group
|
||||
Color Color
|
||||
Size Size
|
||||
}
|
||||
|
||||
Stat struct {
|
||||
Title string
|
||||
Value string
|
||||
Description string
|
||||
Icon Node
|
||||
}
|
||||
)
|
||||
|
||||
func Badge(color Color, text string) Node {
|
||||
var class string
|
||||
|
||||
switch color {
|
||||
case ColorSuccess:
|
||||
class = "badge-success"
|
||||
case ColorWarning:
|
||||
class = "badge-warning"
|
||||
}
|
||||
|
||||
return Div(
|
||||
Class("badge "+class),
|
||||
Text(text),
|
||||
)
|
||||
}
|
||||
|
||||
func Divider(text string) Node {
|
||||
return Div(
|
||||
Class("divider"),
|
||||
Text(text),
|
||||
)
|
||||
}
|
||||
|
||||
func Card(params CardParams) Node {
|
||||
var colorClass, sizeClass string
|
||||
|
||||
switch params.Color {
|
||||
case ColorSuccess:
|
||||
colorClass = "bg-success text-success-content"
|
||||
case ColorPrimary:
|
||||
colorClass = "bg-primary text-primary-content"
|
||||
case ColorAccent:
|
||||
colorClass = "bg-accent text-accent-content"
|
||||
case ColorNeutral:
|
||||
colorClass = "bg-neutral text-neutral-content"
|
||||
case ColorWarning:
|
||||
colorClass = "bg-warning text-warning-content"
|
||||
case ColorInfo:
|
||||
colorClass = "bg-info text-info-content"
|
||||
}
|
||||
|
||||
switch params.Size {
|
||||
case SizeSmall:
|
||||
sizeClass = "card-sm"
|
||||
case SizeMedium:
|
||||
sizeClass = "card-md"
|
||||
case SizeLarge:
|
||||
sizeClass = "card-lg"
|
||||
}
|
||||
|
||||
return Div(
|
||||
Class("cards mb-2 "+colorClass+" "+sizeClass),
|
||||
Div(
|
||||
Class("card-body"),
|
||||
If(len(params.Title) > 0, Span(
|
||||
Class("card-title"),
|
||||
Text(params.Title),
|
||||
)),
|
||||
params.Body,
|
||||
If(params.Footer != nil, Div(
|
||||
Class("card-actions justify-end"),
|
||||
params.Footer,
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Stats(stats ...Stat) Node {
|
||||
g := make(Group, 0, len(stats))
|
||||
for _, stat := range stats {
|
||||
g = append(g, Div(
|
||||
Class("stat"),
|
||||
Iff(stat.Icon != nil, func() Node {
|
||||
return Div(
|
||||
Class("stat-figure text-secondary"),
|
||||
stat.Icon,
|
||||
)
|
||||
}),
|
||||
Div(
|
||||
Class("stat-title"),
|
||||
Text(stat.Title),
|
||||
),
|
||||
Div(
|
||||
Class("stat-value"),
|
||||
Text(stat.Value),
|
||||
),
|
||||
Div(
|
||||
Class("stat-desc"),
|
||||
Text(stat.Description),
|
||||
),
|
||||
))
|
||||
}
|
||||
return Div(
|
||||
Class("stats shadow"),
|
||||
g,
|
||||
)
|
||||
}
|
||||
|
|
@ -19,6 +19,12 @@ type (
|
|||
Help string
|
||||
}
|
||||
|
||||
FileFieldParams struct {
|
||||
Name string
|
||||
Label string
|
||||
Help string
|
||||
}
|
||||
|
||||
OptionsParams struct {
|
||||
Form form.Form
|
||||
FormField string
|
||||
|
|
@ -26,6 +32,7 @@ type (
|
|||
Label string
|
||||
Value string
|
||||
Options []Choice
|
||||
Help string
|
||||
}
|
||||
|
||||
Choice struct {
|
||||
|
|
@ -52,38 +59,22 @@ type (
|
|||
)
|
||||
|
||||
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,
|
||||
Class("mt-2 flex gap-2"),
|
||||
Group(controls),
|
||||
)
|
||||
}
|
||||
|
||||
func TextareaField(el TextareaFieldParams) Node {
|
||||
return Div(
|
||||
Class("field"),
|
||||
Label(
|
||||
For("name"),
|
||||
Class("label"),
|
||||
Text(el.Label),
|
||||
return Fieldset(
|
||||
el.Label,
|
||||
Textarea(
|
||||
Class("textarea h-24 w-2/3 "+formFieldStatusClass(el.Form, el.FormField)),
|
||||
ID(el.Name),
|
||||
Name(el.Name),
|
||||
Text(el.Value),
|
||||
),
|
||||
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))),
|
||||
Help(el.Help),
|
||||
formFieldErrors(el.Form, el.FormField),
|
||||
)
|
||||
}
|
||||
|
|
@ -91,25 +82,27 @@ func TextareaField(el TextareaFieldParams) Node {
|
|||
func Radios(el OptionsParams) Node {
|
||||
buttons := make(Group, len(el.Options))
|
||||
for i, opt := range el.Options {
|
||||
buttons[i] = Label(
|
||||
Class("radio"),
|
||||
id := "radio-" + el.Name + "-" + opt.Value
|
||||
buttons[i] = Div(
|
||||
Class("mb-2"),
|
||||
Input(
|
||||
ID(id),
|
||||
Type("radio"),
|
||||
Name(el.Name),
|
||||
Value(opt.Value),
|
||||
Class("radio mr-1 "+formFieldStatusClass(el.Form, el.FormField)),
|
||||
If(el.Value == opt.Value, Checked()),
|
||||
),
|
||||
Text(" "+opt.Label),
|
||||
Label(
|
||||
Text(opt.Label),
|
||||
For(id),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return Div(
|
||||
Class("control field"),
|
||||
Label(Class("label"), Text(el.Label)),
|
||||
Div(
|
||||
Class("radios"),
|
||||
buttons,
|
||||
),
|
||||
return Fieldset(
|
||||
el.Label,
|
||||
buttons,
|
||||
formFieldErrors(el.Form, el.FormField),
|
||||
)
|
||||
}
|
||||
|
|
@ -124,82 +117,77 @@ func SelectList(el OptionsParams) Node {
|
|||
)
|
||||
}
|
||||
|
||||
return Div(
|
||||
Class("control field"),
|
||||
Label(Class("label"), Text(el.Label)),
|
||||
Div(
|
||||
Class("select"),
|
||||
Select(
|
||||
Name(el.Name),
|
||||
buttons,
|
||||
),
|
||||
return Fieldset(
|
||||
el.Label,
|
||||
Select(
|
||||
Class("select "+formFieldStatusClass(el.Form, el.FormField)),
|
||||
buttons,
|
||||
),
|
||||
Help(el.Help),
|
||||
formFieldErrors(el.Form, el.FormField),
|
||||
)
|
||||
}
|
||||
|
||||
func Checkbox(el CheckboxParams) Node {
|
||||
return Div(
|
||||
Class("field"),
|
||||
Div(
|
||||
Class("control"),
|
||||
Label(
|
||||
Label(
|
||||
Class("label"),
|
||||
Input(
|
||||
Class("checkbox"),
|
||||
Input(
|
||||
Type("checkbox"),
|
||||
Name(el.Name),
|
||||
If(el.Checked, Checked()),
|
||||
Value("true"),
|
||||
),
|
||||
Text(" "+el.Label),
|
||||
Type("checkbox"),
|
||||
Name(el.Name),
|
||||
If(el.Checked, Checked()),
|
||||
Value("true"),
|
||||
),
|
||||
Text(" "+el.Label),
|
||||
),
|
||||
formFieldErrors(el.Form, el.FormField),
|
||||
)
|
||||
}
|
||||
|
||||
func InputField(el InputFieldParams) Node {
|
||||
return Div(
|
||||
Class("field"),
|
||||
Label(
|
||||
Class("label"),
|
||||
For(el.Name),
|
||||
Text(el.Label),
|
||||
return Fieldset(
|
||||
el.Label,
|
||||
Input(
|
||||
ID(el.Name),
|
||||
Name(el.Name),
|
||||
Type(el.InputType),
|
||||
Class("input "+formFieldStatusClass(el.Form, el.FormField)),
|
||||
Value(el.Value),
|
||||
If(el.Placeholder != "", Placeholder(el.Placeholder)),
|
||||
),
|
||||
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))),
|
||||
Help(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 Help(text string) Node {
|
||||
return If(len(text) > 0, Div(
|
||||
Class("label"),
|
||||
Text(text),
|
||||
))
|
||||
}
|
||||
|
||||
func Fieldset(label string, els ...Node) Node {
|
||||
return FieldSet(
|
||||
Class("fieldset"),
|
||||
If(len(label) > 0, Legend(
|
||||
Class("fieldset-legend"),
|
||||
Text(label),
|
||||
)),
|
||||
Group(els),
|
||||
)
|
||||
}
|
||||
|
||||
func FileField(el FileFieldParams) Node {
|
||||
return Fieldset(
|
||||
el.Label,
|
||||
Input(
|
||||
Type("file"),
|
||||
Class("file-input"),
|
||||
Name(el.Name),
|
||||
),
|
||||
Help(el.Help),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -210,9 +198,9 @@ func formFieldStatusClass(fm form.Form, formField string) string {
|
|||
case !fm.IsSubmitted():
|
||||
return ""
|
||||
case fm.FieldHasErrors(formField):
|
||||
return "is-danger"
|
||||
return "input-error"
|
||||
default:
|
||||
return "is-success"
|
||||
return "input-success"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,8 +216,8 @@ func formFieldErrors(fm form.Form, field string) Node {
|
|||
|
||||
g := make(Group, len(errs))
|
||||
for i, err := range errs {
|
||||
g[i] = P(
|
||||
Class("help is-danger"),
|
||||
g[i] = Div(
|
||||
Class("text-error"),
|
||||
Text(err),
|
||||
)
|
||||
}
|
||||
|
|
@ -245,17 +233,35 @@ func CSRF(r *ui.Request) Node {
|
|||
)
|
||||
}
|
||||
|
||||
func FormButton(class, label string) Node {
|
||||
func FormButton(color Color, label string) Node {
|
||||
return Button(
|
||||
Class("button "+class),
|
||||
Class("btn "+buttonColor(color)),
|
||||
Text(label),
|
||||
)
|
||||
}
|
||||
|
||||
func ButtonLink(href, class, label string) Node {
|
||||
func ButtonLink(color Color, href, label string) Node {
|
||||
return A(
|
||||
Href(href),
|
||||
Class("button "+class),
|
||||
Class("btn "+buttonColor(color)),
|
||||
Text(label),
|
||||
)
|
||||
}
|
||||
|
||||
func buttonColor(color Color) string {
|
||||
// Only colors being used are included so unused styles are not compiled.
|
||||
switch color {
|
||||
case ColorPrimary:
|
||||
return "btn-primary"
|
||||
case ColorInfo:
|
||||
return "btn-info"
|
||||
case ColorAccent:
|
||||
return "btn-accent"
|
||||
case ColorError:
|
||||
return "btn-error"
|
||||
case ColorLink:
|
||||
return "btn-link"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,18 @@ import (
|
|||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func JS(r *ui.Request) Node {
|
||||
func JS() Node {
|
||||
return Group{
|
||||
Script(Src("https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js")),
|
||||
Script(Src("https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"), Defer()),
|
||||
Script(Src("https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"), Defer()),
|
||||
}
|
||||
}
|
||||
|
||||
func CSS() Node {
|
||||
return Link(
|
||||
Href("https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css"),
|
||||
Href(ui.StaticFile("main.css")),
|
||||
Rel("stylesheet"),
|
||||
Type("text/css"),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ 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"))),
|
||||
Link(Rel("icon"), Href(ui.StaticFile("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, ", ")))),
|
||||
|
|
|
|||
|
|
@ -1,19 +1,72 @@
|
|||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mikestefanello/pagoda/pkg/pager"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/components"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func MenuLink(r *ui.Request, title, routeName string, routeParams ...any) Node {
|
||||
func MenuLink(r *ui.Request, icon Node, title, routeName string, routeParams ...any) Node {
|
||||
href := r.Path(routeName, routeParams...)
|
||||
|
||||
return Li(
|
||||
Class("ml-2"),
|
||||
A(
|
||||
Href(href),
|
||||
icon,
|
||||
Text(title),
|
||||
If(href == r.CurrentPath, Class("is-active")),
|
||||
Classes{
|
||||
"menu-active": href == r.CurrentPath,
|
||||
"p-2": true,
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Pager(page int, path string, hasNext bool, hxTarget string) Node {
|
||||
href := func(page int) string {
|
||||
return fmt.Sprintf("%s?%s=%d",
|
||||
path,
|
||||
pager.QueryKey,
|
||||
page,
|
||||
)
|
||||
}
|
||||
|
||||
return Div(
|
||||
Class("join"),
|
||||
A(
|
||||
Class("join-item btn"),
|
||||
Text("«"),
|
||||
If(page <= 1, Disabled()),
|
||||
Href(href(page-1)),
|
||||
Iff(len(hxTarget) > 0, func() Node {
|
||||
return Group{
|
||||
Attr("hx-get", href(page-1)),
|
||||
Attr("hx-swap", "outerHTML"),
|
||||
Attr("hx-target", hxTarget),
|
||||
}
|
||||
}),
|
||||
),
|
||||
Button(
|
||||
Class("join-item btn"),
|
||||
Textf("Page %d", page),
|
||||
),
|
||||
A(
|
||||
Class("join-item btn"),
|
||||
Text("»"),
|
||||
If(!hasNext, Disabled()),
|
||||
Href(href(page+1)),
|
||||
Iff(len(hxTarget) > 0, func() Node {
|
||||
return Group{
|
||||
Attr("hx-get", href(page+1)),
|
||||
Attr("hx-swap", "outerHTML"),
|
||||
Attr("hx-target", hxTarget),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
27
pkg/ui/components/styles.go
Normal file
27
pkg/ui/components/styles.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package components
|
||||
|
||||
type (
|
||||
Color int
|
||||
Size int
|
||||
)
|
||||
|
||||
const (
|
||||
ColorNone Color = iota
|
||||
ColorNeutral
|
||||
ColorPrimary
|
||||
ColorSecondary
|
||||
ColorAccent
|
||||
ColorInfo
|
||||
ColorSuccess
|
||||
ColorWarning
|
||||
ColorError
|
||||
ColorLink
|
||||
)
|
||||
|
||||
const (
|
||||
SizeExtraSmall Size = iota
|
||||
SizeSmall
|
||||
SizeMedium
|
||||
SizeLarge
|
||||
SizeExtraLarge
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ package components
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
|
|
@ -11,46 +12,27 @@ 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
|
||||
}
|
||||
func Tabs(tabs []Tab) Node {
|
||||
g := make(Group, 0, len(tabs)*2)
|
||||
id := fmt.Sprintf("tabs-%d", rand.Int())
|
||||
|
||||
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
|
||||
for i, tab := range tabs {
|
||||
g = append(g,
|
||||
Input(
|
||||
Type("radio"),
|
||||
Name(id),
|
||||
Class("tab"),
|
||||
Aria("label", tab.Title),
|
||||
If(i == 0, Checked()),
|
||||
),
|
||||
Div(
|
||||
Class("tab-content bg-base-100 border-base-300 p-6"),
|
||||
Raw(tab.Body),
|
||||
))
|
||||
}
|
||||
|
||||
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(),
|
||||
),
|
||||
Class("tabs tabs-lift"),
|
||||
g,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,10 +113,10 @@ func AdminEntity(r *ui.Request, schema *load.Schema, values url.Values) Node {
|
|||
Method(http.MethodPost),
|
||||
nodes,
|
||||
ControlGroup(
|
||||
FormButton("is-primary", "Submit"),
|
||||
FormButton(ColorPrimary, "Submit"),
|
||||
ButtonLink(
|
||||
ColorNone,
|
||||
r.Path(routenames.AdminEntityList(schema.Name)),
|
||||
"is-secondary",
|
||||
"Cancel",
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -14,14 +14,13 @@ func AdminEntityDelete(r *ui.Request, entityTypeName string) Node {
|
|||
return Form(
|
||||
Method(http.MethodPost),
|
||||
P(
|
||||
Class("subtitle"),
|
||||
Textf("Are you sure you want to delete this %s?", entityTypeName),
|
||||
),
|
||||
ControlGroup(
|
||||
FormButton("is-link", "Delete"),
|
||||
FormButton(ColorError, "Delete"),
|
||||
ButtonLink(
|
||||
ColorNone,
|
||||
r.Path(routenames.AdminEntityList(entityTypeName)),
|
||||
"is-secondary",
|
||||
"Cancel",
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -22,21 +22,22 @@ func (f *Cache) Render(r *ui.Request) Node {
|
|||
ID("cache"),
|
||||
Method(http.MethodPost),
|
||||
Attr("hx-post", r.Path(routenames.CacheSubmit)),
|
||||
Message(
|
||||
"is-info",
|
||||
"Test the cache",
|
||||
Group{
|
||||
P(Text("This route handler shows how the default in-memory cache works. Try updating the value using the form below and see how it persists after you reload the page.")),
|
||||
P(Text("HTMX makes it easy to re-render the cached value after the form is submitted.")),
|
||||
Card(CardParams{
|
||||
Title: "Test the cache",
|
||||
Body: Group{
|
||||
Span(Text("This route handler shows how the default in-memory cache works. Try updating the value using the form below and see how it persists after you reload the page.")),
|
||||
Span(Text("HTMX makes it easy to re-render the cached value after the form is submitted.")),
|
||||
},
|
||||
),
|
||||
Color: ColorInfo,
|
||||
Size: SizeMedium,
|
||||
}),
|
||||
Label(
|
||||
For("value"),
|
||||
Class("value"),
|
||||
Text("Value in cache: "),
|
||||
),
|
||||
If(f.CurrentValue != "", Span(Class("tag is-success"), Text(f.CurrentValue))),
|
||||
If(f.CurrentValue == "", I(Text("(empty)"))),
|
||||
If(f.CurrentValue != "", Badge(ColorSuccess, f.CurrentValue)),
|
||||
If(f.CurrentValue == "", Badge(ColorWarning, "empty")),
|
||||
InputField(InputFieldParams{
|
||||
Form: f,
|
||||
FormField: "Value",
|
||||
|
|
@ -46,7 +47,7 @@ func (f *Cache) Render(r *ui.Request) Node {
|
|||
Value: f.Value,
|
||||
}),
|
||||
ControlGroup(
|
||||
FormButton("is-link", "Update cache"),
|
||||
FormButton(ColorPrimary, "Update cache"),
|
||||
),
|
||||
CSRF(r),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func (f *Contact) Render(r *ui.Request) Node {
|
|||
Value: f.Message,
|
||||
}),
|
||||
ControlGroup(
|
||||
FormButton("is-link", "Submit"),
|
||||
FormButton(ColorPrimary, "Submit"),
|
||||
),
|
||||
CSRF(r),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,9 +18,13 @@ func (f File) Render(r *ui.Request) Node {
|
|||
Method(http.MethodPost),
|
||||
Action(r.Path(routenames.FilesSubmit)),
|
||||
EncType("multipart/form-data"),
|
||||
FileField("file", "Choose a file.. "),
|
||||
FileField(FileFieldParams{
|
||||
Name: "file",
|
||||
Label: "Test file",
|
||||
Help: "Pick a file to upload.",
|
||||
}),
|
||||
ControlGroup(
|
||||
FormButton("is-link", "Upload"),
|
||||
FormButton(ColorPrimary, "Upload"),
|
||||
),
|
||||
CSRF(r),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ func (f *ForgotPassword) Render(r *ui.Request) Node {
|
|||
Value: f.Email,
|
||||
}),
|
||||
ControlGroup(
|
||||
FormButton("is-primary", "Reset password"),
|
||||
ButtonLink(r.Path(routenames.Home), "is-light", "Cancel"),
|
||||
FormButton(ColorPrimary, "Reset password"),
|
||||
ButtonLink(ColorLink, r.Path(routenames.Home), "Cancel"),
|
||||
),
|
||||
CSRF(r),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,10 +40,25 @@ func (f *Login) Render(r *ui.Request) Node {
|
|||
Label: "Password",
|
||||
Placeholder: "******",
|
||||
}),
|
||||
Div(
|
||||
Class("text-right text-primary mt-2"),
|
||||
A(
|
||||
Href(r.Path(routenames.ForgotPassword)),
|
||||
Text("Forgot password?"),
|
||||
),
|
||||
),
|
||||
ControlGroup(
|
||||
FormButton("is-link", "Login"),
|
||||
ButtonLink(r.Path(routenames.Home), "is-light", "Cancel"),
|
||||
FormButton(ColorPrimary, "Login"),
|
||||
ButtonLink(ColorLink, r.Path(routenames.Home), "Cancel"),
|
||||
),
|
||||
CSRF(r),
|
||||
Div(
|
||||
Class("text-center text-base-content/50 mt-4"),
|
||||
Text("Don't have an account? "),
|
||||
A(
|
||||
Href(r.Path(routenames.Register)),
|
||||
Text("Register"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,16 +51,24 @@ func (f *Register) Render(r *ui.Request) Node {
|
|||
}),
|
||||
InputField(InputFieldParams{
|
||||
Form: f,
|
||||
FormField: "PasswordConfirm",
|
||||
FormField: "ConfirmPassword",
|
||||
Name: "password-confirm",
|
||||
InputType: "password",
|
||||
Label: "Confirm password",
|
||||
Placeholder: "******",
|
||||
}),
|
||||
ControlGroup(
|
||||
FormButton("is-primary", "Register"),
|
||||
ButtonLink(r.Path(routenames.Home), "is-light", "Cancel"),
|
||||
FormButton(ColorPrimary, "Register"),
|
||||
ButtonLink(ColorLink, r.Path(routenames.Home), "Cancel"),
|
||||
),
|
||||
CSRF(r),
|
||||
Div(
|
||||
Class("text-center text-base-content/50 mt-4"),
|
||||
Text("Already have an account? "),
|
||||
A(
|
||||
Href(r.Path(routenames.Login)),
|
||||
Text("Login"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func (f *ResetPassword) Render(r *ui.Request) Node {
|
|||
Placeholder: "******",
|
||||
}),
|
||||
ControlGroup(
|
||||
FormButton("is-primary", "Update password"),
|
||||
FormButton(ColorPrimary, "Update password"),
|
||||
),
|
||||
CSRF(r),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func (f *Task) Render(r *ui.Request) Node {
|
|||
Help: "The message the task will output to the log",
|
||||
}),
|
||||
ControlGroup(
|
||||
FormButton("is-link", "Add task to queue"),
|
||||
FormButton(ColorPrimary, "Add task to queue"),
|
||||
),
|
||||
CSRF(r),
|
||||
)
|
||||
|
|
|
|||
208
pkg/ui/icons/icons.go
Normal file
208
pkg/ui/icons/icons.go
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
package icons
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/cache"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func CircleStack() Node {
|
||||
return icon("CircleStack",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Eyes() Node {
|
||||
return icon("Eyes",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"),
|
||||
),
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func UserCircle() Node {
|
||||
return icon("UserCircle",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Globe() Node {
|
||||
return icon("Globe",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Home() Node {
|
||||
return icon("Home",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Info() Node {
|
||||
return icon("Info",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Mail() Node {
|
||||
return icon("Mail",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Archive() Node {
|
||||
return icon("Archive",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func PencilSquare() Node {
|
||||
return icon("PencilSquare",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Document() Node {
|
||||
return icon("Document",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Exit() Node {
|
||||
return icon("Exit",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Enter() Node {
|
||||
return icon("Enter",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0 3 3m-3-3h12.75"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func UserPlus() Node {
|
||||
return icon("UserPlus",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func QuestionCircle() Node {
|
||||
return icon("QuestionCircle",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func XCircle() Node {
|
||||
return icon("XCircle",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func MagnifyingGlass() Node {
|
||||
return icon("MagnifyingGlass",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func LockClosed() Node {
|
||||
return icon("LockClosed",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Star() Node {
|
||||
return icon("Star",
|
||||
El("path",
|
||||
Attr("stroke-linecap", "round"),
|
||||
Attr("stroke-linejoin", "round"),
|
||||
Attr("d", "M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func icon(id string, els ...Node) Node {
|
||||
return cache.SetIfNotExists(fmt.Sprintf("icon.%s", id), func() Node {
|
||||
return SVG(
|
||||
Attr("xmlns", "http://www.w3.org/2000/svg"),
|
||||
Attr("fill", "none"),
|
||||
Attr("viewBox", "0 0 24 24"),
|
||||
Attr("stroke-width", "1.5"),
|
||||
Attr("stroke", "currentColor"),
|
||||
Class("w-5 h-5"),
|
||||
Group(els),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
package layouts
|
||||
|
||||
import (
|
||||
"github.com/mikestefanello/pagoda/pkg/routenames"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/cache"
|
||||
. "github.com/mikestefanello/pagoda/pkg/ui/components"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
|
|
@ -13,31 +11,24 @@ func Auth(r *ui.Request, content Node) Node {
|
|||
return Doctype(
|
||||
HTML(
|
||||
Lang("en"),
|
||||
Data("theme", "light"),
|
||||
Data("theme", "dark"),
|
||||
Head(
|
||||
Metatags(r),
|
||||
CSS(),
|
||||
JS(r),
|
||||
JS(),
|
||||
),
|
||||
Body(
|
||||
Section(
|
||||
Class("hero is-fullheight"),
|
||||
Div(
|
||||
Class("hero flex items-center justify-center min-h-screen"),
|
||||
Div(
|
||||
Class("hero-body"),
|
||||
Class("flex-col hero-content"),
|
||||
Div(
|
||||
Class("container"),
|
||||
Class("card shadow-md bg-base-200 w-96"),
|
||||
Div(
|
||||
Class("columns is-centered"),
|
||||
Div(
|
||||
Class("column is-half"),
|
||||
If(len(r.Title) > 0, H1(Class("title"), Text(r.Title))),
|
||||
Div(
|
||||
Class("notification"),
|
||||
FlashMessages(r),
|
||||
content,
|
||||
authNavBar(r),
|
||||
),
|
||||
),
|
||||
Class("card-body"),
|
||||
If(len(r.Title) > 0, H1(Class("text-2xl font-bold"), Text(r.Title))),
|
||||
FlashMessages(r),
|
||||
content,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -47,20 +38,3 @@ func Auth(r *ui.Request, content Node) Node {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
func authNavBar(r *ui.Request) Node {
|
||||
return cache.SetIfNotExists("authNavBar", func() Node {
|
||||
return Nav(
|
||||
Class("navbar"),
|
||||
Div(
|
||||
Class("navbar-menu"),
|
||||
Div(
|
||||
Class("navbar-start"),
|
||||
A(Class("navbar-item"), Href(r.Path(routenames.Login)), Text("Login")),
|
||||
A(Class("navbar-item"), Href(r.Path(routenames.Register)), Text("Create an account")),
|
||||
A(Class("navbar-item"), Href(r.Path(routenames.ForgotPassword)), Text("Forgot password")),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"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/icons"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
|
@ -14,118 +15,97 @@ func Primary(r *ui.Request, content Node) Node {
|
|||
return Doctype(
|
||||
HTML(
|
||||
Lang("en"),
|
||||
Data("theme", "light"),
|
||||
Data("theme", "dark"),
|
||||
Head(
|
||||
Metatags(r),
|
||||
CSS(),
|
||||
JS(r),
|
||||
JS(),
|
||||
),
|
||||
Body(
|
||||
headerNavBar(r),
|
||||
Div(
|
||||
Class("container mt-5"),
|
||||
Class("drawer lg:drawer-open"),
|
||||
Input(
|
||||
ID("sidebar"),
|
||||
Type("checkbox"),
|
||||
Class("drawer-toggle"),
|
||||
),
|
||||
Div(
|
||||
Class("columns"),
|
||||
Div(
|
||||
Class("column is-2"),
|
||||
sidebarMenu(r),
|
||||
),
|
||||
Div(
|
||||
Class("column is-10"),
|
||||
Div(
|
||||
Class("box"),
|
||||
If(len(r.Title) > 0, H1(Class("title"), Text(r.Title))),
|
||||
FlashMessages(r),
|
||||
content,
|
||||
),
|
||||
Class("drawer-content flex flex-col p-7 prose-base"),
|
||||
If(len(r.Title) > 0, H1(Text(r.Title))),
|
||||
FlashMessages(r),
|
||||
content,
|
||||
Label(
|
||||
For("sidebar"),
|
||||
Class("btn btn-primary drawer-button lg:hidden"),
|
||||
Text("Open drawer"),
|
||||
),
|
||||
),
|
||||
sidebarMenu(r),
|
||||
),
|
||||
searchModal(r),
|
||||
HtmxListeners(r),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func headerNavBar(r *ui.Request) Node {
|
||||
return cache.SetIfNotExists("layout.headerNavBar", func() Node {
|
||||
return Nav(
|
||||
Class("navbar is-dark"),
|
||||
Div(
|
||||
Class("container"),
|
||||
Div(
|
||||
Class("navbar-brand"),
|
||||
HxBoost(),
|
||||
A(
|
||||
Href(r.Path(routenames.Home)),
|
||||
Class("navbar-item"),
|
||||
Text("Pagoda"),
|
||||
),
|
||||
),
|
||||
Div(
|
||||
ID("navbarMenu"),
|
||||
Class("navbar-menu"),
|
||||
Div(
|
||||
Class("navbar-end"),
|
||||
search(r),
|
||||
),
|
||||
func search() Node {
|
||||
return cache.SetIfNotExists("layout.search", func() Node {
|
||||
return Div(
|
||||
Class("ml-2"),
|
||||
Attr("x-data", ""),
|
||||
Label(
|
||||
Class("input"),
|
||||
icons.MagnifyingGlass(),
|
||||
Input(
|
||||
Type("search"),
|
||||
Class("grow"),
|
||||
Placeholder("Search"),
|
||||
Attr("@click", "search_modal.showModal();"),
|
||||
),
|
||||
),
|
||||
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func search(r *ui.Request) Node {
|
||||
return cache.SetIfNotExists("layout.search", func() Node {
|
||||
return Div(
|
||||
Class("search mr-2 mt-1"),
|
||||
Attr("x-data", "{modal:false}"),
|
||||
Input(
|
||||
Class("input"),
|
||||
Type("search"),
|
||||
Placeholder("Search..."),
|
||||
Attr("@click", "modal = true; $nextTick(() => $refs.input.focus());"),
|
||||
),
|
||||
func searchModal(r *ui.Request) Node {
|
||||
return cache.SetIfNotExists("layout.searchModal", func() Node {
|
||||
return Dialog(
|
||||
ID("search_modal"),
|
||||
Class("modal"),
|
||||
Div(
|
||||
Class("modal"),
|
||||
Attr(":class", "modal ? 'is-active' : ''"),
|
||||
Attr("x-show", "modal == true"),
|
||||
Div(
|
||||
Class("modal-background"),
|
||||
),
|
||||
Div(
|
||||
Class("modal-content"),
|
||||
Attr("@click.outside", "modal = false;"),
|
||||
Div(
|
||||
Class("box"),
|
||||
H2(
|
||||
Class("subtitle"),
|
||||
Text("Search"),
|
||||
),
|
||||
P(
|
||||
Class("control"),
|
||||
Input(
|
||||
Attr("hx-get", r.Path(routenames.Search)),
|
||||
Attr("hx-trigger", "keyup changed delay:500ms"),
|
||||
Attr("hx-target", "#results"),
|
||||
Name("query"),
|
||||
Class("input"),
|
||||
Type("search"),
|
||||
Placeholder("Search..."),
|
||||
Attr("x-ref", "input"),
|
||||
),
|
||||
),
|
||||
Div(
|
||||
Class("block"),
|
||||
),
|
||||
Div(
|
||||
ID("results"),
|
||||
),
|
||||
Class("modal-box"),
|
||||
Form(
|
||||
Method("dialog"),
|
||||
Button(
|
||||
Class("btn btn-sm btn-circle btn-ghost absolute right-2 top-2"),
|
||||
Text("✕"),
|
||||
),
|
||||
),
|
||||
H3(
|
||||
Class("text-lg font-bold mb-2"),
|
||||
Text("Search"),
|
||||
),
|
||||
Input(
|
||||
Attr("hx-get", r.Path(routenames.Search)),
|
||||
Attr("hx-trigger", "keyup changed delay:500ms"),
|
||||
Attr("hx-target", "#results"),
|
||||
Name("query"),
|
||||
Class("input w-full"),
|
||||
Type("search"),
|
||||
Placeholder("Search..."),
|
||||
),
|
||||
Ul(
|
||||
ID("results"),
|
||||
Class("list"),
|
||||
),
|
||||
),
|
||||
Form(
|
||||
Method("dialog"),
|
||||
Class("modal-backdrop"),
|
||||
Button(
|
||||
Class("modal-close is-large"),
|
||||
Aria("label", "close"),
|
||||
Text("close"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -133,66 +113,67 @@ func search(r *ui.Request) Node {
|
|||
}
|
||||
|
||||
func sidebarMenu(r *ui.Request) Node {
|
||||
header := func(text string) Node {
|
||||
return Li(
|
||||
Class("menu-title mt-3 uppercase"),
|
||||
Span(Text(text)),
|
||||
)
|
||||
}
|
||||
|
||||
adminSubMenu := func() Node {
|
||||
entityTypeNames := admin.GetEntityTypeNames()
|
||||
entityTypeLinks := make(Group, len(entityTypeNames))
|
||||
for _, n := range entityTypeNames {
|
||||
entityTypeLinks = append(entityTypeLinks, MenuLink(r, n, routenames.AdminEntityList(n)))
|
||||
entityTypeLinks = append(entityTypeLinks, MenuLink(r, icons.PencilSquare(), n, routenames.AdminEntityList(n)))
|
||||
}
|
||||
|
||||
return Group{
|
||||
P(
|
||||
Class("menu-label"),
|
||||
Text("Entities"),
|
||||
),
|
||||
Ul(
|
||||
Class("menu-list"),
|
||||
entityTypeLinks,
|
||||
),
|
||||
P(
|
||||
Class("menu-label"),
|
||||
Text("Monitoring"),
|
||||
),
|
||||
Ul(
|
||||
Class("menu-list"),
|
||||
Li(
|
||||
A(
|
||||
Href(r.Path(routenames.AdminTasks)),
|
||||
Text("Tasks"),
|
||||
Target("_blank"),
|
||||
),
|
||||
header("Entities"),
|
||||
entityTypeLinks,
|
||||
header("Monitoring"),
|
||||
Li(
|
||||
A(
|
||||
icons.CircleStack(),
|
||||
Href(r.Path(routenames.AdminTasks)),
|
||||
Text("Tasks"),
|
||||
Target("_blank"),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return Aside(
|
||||
Class("menu"),
|
||||
HxBoost(),
|
||||
P(
|
||||
Class("menu-label"),
|
||||
Text("General"),
|
||||
return Div(
|
||||
Class("drawer-side"),
|
||||
Label(
|
||||
For("sidebar"),
|
||||
Aria("label", "close sidebar"),
|
||||
Class("drawer-overlay"),
|
||||
),
|
||||
Ul(
|
||||
Class("menu-list"),
|
||||
MenuLink(r, "Dashboard", routenames.Home),
|
||||
MenuLink(r, "About", routenames.About),
|
||||
MenuLink(r, "Contact", routenames.Contact),
|
||||
MenuLink(r, "Cache", routenames.Cache),
|
||||
MenuLink(r, "Task", routenames.Task),
|
||||
MenuLink(r, "Files", routenames.Files),
|
||||
Div(
|
||||
Class("menu bg-base-200 text-base-content min-h-full w-80 p-4"),
|
||||
Div(
|
||||
Class("w-2/3 mx-auto mt-3 mb-10"),
|
||||
Img(
|
||||
Src(ui.StaticFile("logo.png")),
|
||||
),
|
||||
),
|
||||
search(),
|
||||
Ul(
|
||||
HxBoost(),
|
||||
header("General"),
|
||||
MenuLink(r, icons.Home(), "Dashboard", routenames.Home),
|
||||
MenuLink(r, icons.Info(), "About", routenames.About),
|
||||
MenuLink(r, icons.Mail(), "Contact", routenames.Contact),
|
||||
MenuLink(r, icons.Archive(), "Cache", routenames.Cache),
|
||||
MenuLink(r, icons.CircleStack(), "Task", routenames.Task),
|
||||
MenuLink(r, icons.Document(), "Files", routenames.Files),
|
||||
header("Account"),
|
||||
If(r.IsAuth, MenuLink(r, icons.Exit(), "Logout", routenames.Logout)),
|
||||
If(!r.IsAuth, MenuLink(r, icons.Enter(), "Login", routenames.Login)),
|
||||
If(!r.IsAuth, MenuLink(r, icons.UserPlus(), "Register", routenames.Register)),
|
||||
If(!r.IsAuth, MenuLink(r, icons.QuestionCircle(), "Forgot password", routenames.ForgotPasswordSubmit)),
|
||||
Iff(r.IsAdmin, adminSubMenu),
|
||||
),
|
||||
),
|
||||
P(
|
||||
Class("menu-label"),
|
||||
Text("Account"),
|
||||
),
|
||||
Ul(
|
||||
Class("menu-list"),
|
||||
If(r.IsAuth, MenuLink(r, "Logout", routenames.Logout)),
|
||||
If(!r.IsAuth, MenuLink(r, "Login", routenames.Login)),
|
||||
If(!r.IsAuth, MenuLink(r, "Register", routenames.Register)),
|
||||
If(!r.IsAuth, MenuLink(r, "Forgot password", routenames.ForgotPasswordSubmit)),
|
||||
),
|
||||
Iff(r.IsAdmin, adminSubMenu),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/mikestefanello/pagoda/pkg/pager"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui"
|
||||
. "github.com/mikestefanello/pagoda/pkg/ui/components"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
|
@ -16,6 +17,7 @@ type (
|
|||
}
|
||||
|
||||
Post struct {
|
||||
ID int
|
||||
Title, Body string
|
||||
}
|
||||
)
|
||||
|
|
@ -28,57 +30,37 @@ func (p *Posts) Render(path string) Node {
|
|||
|
||||
return Div(
|
||||
ID("posts"),
|
||||
g,
|
||||
Div(
|
||||
Class("field is-grouped is-grouped-centered"),
|
||||
If(!p.Pager.IsBeginning(), P(
|
||||
Class("control"),
|
||||
Button(
|
||||
Class("button is-primary"),
|
||||
Attr("hx-swap", "outerHTML"),
|
||||
Attr("hx-get", fmt.Sprintf("%s?%s=%d", path, pager.QueryKey, p.Pager.Page-1)),
|
||||
Attr("hx-target", "#posts"),
|
||||
Text("Previous page"),
|
||||
),
|
||||
)),
|
||||
If(!p.Pager.IsEnd(), P(
|
||||
Class("control"),
|
||||
Button(
|
||||
Class("button is-primary"),
|
||||
Attr("hx-swap", "outerHTML"),
|
||||
Attr("hx-get", fmt.Sprintf("%s?%s=%d", path, pager.QueryKey, p.Pager.Page+1)),
|
||||
Attr("hx-target", "#posts"),
|
||||
Text("Next page"),
|
||||
),
|
||||
)),
|
||||
Ul(
|
||||
Class("list bg-base-100 rounded-box shadow-md not-prose"),
|
||||
g,
|
||||
),
|
||||
Div(Class("mb-4")),
|
||||
Pager(p.Pager.Page, path, !p.Pager.IsEnd(), "#posts"),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *Post) Render() Node {
|
||||
return Article(
|
||||
Class("media"),
|
||||
Figure(
|
||||
Class("media-left"),
|
||||
P(
|
||||
Class("image is-64x64"),
|
||||
Img(
|
||||
Src(ui.File("gopher.png")),
|
||||
Alt("Gopher"),
|
||||
),
|
||||
return Li(
|
||||
Class("list-row"),
|
||||
Div(
|
||||
Class("text-4xl font-thin opacity-30 tabular-nums"),
|
||||
Text(fmt.Sprintf("%02d", p.ID)),
|
||||
),
|
||||
Div(
|
||||
Img(
|
||||
Class("size-10 rounded-box"),
|
||||
Src(ui.StaticFile("gopher.png")),
|
||||
Alt("Gopher"),
|
||||
),
|
||||
),
|
||||
Div(
|
||||
Class("media-content"),
|
||||
Class("list-col-grow"),
|
||||
Div(
|
||||
Class("content"),
|
||||
P(
|
||||
Strong(
|
||||
Text(p.Title),
|
||||
),
|
||||
Br(),
|
||||
Text(p.Body),
|
||||
),
|
||||
Text(p.Title),
|
||||
),
|
||||
Div(
|
||||
Class("text-xs font-semibold opacity-60"),
|
||||
Text(p.Body),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ type SearchResult struct {
|
|||
}
|
||||
|
||||
func (s *SearchResult) Render() Node {
|
||||
return A(
|
||||
Class("panel-block"),
|
||||
Href(s.URL),
|
||||
Text(s.Title),
|
||||
return Li(
|
||||
Class("list-row"),
|
||||
A(
|
||||
Href(s.URL),
|
||||
Text(s.Title),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ func About(ctx echo.Context) error {
|
|||
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.
|
||||
// The tabs are static, so we can render and cache them.
|
||||
tabs := cache.SetIfNotExists("pages.about.Tabs", func() Node {
|
||||
return Group{
|
||||
H2(Text("Frontend")),
|
||||
P(Text("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.")),
|
||||
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",
|
||||
|
|
@ -31,15 +31,14 @@ func About(ctx echo.Context) error {
|
|||
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.",
|
||||
Title: "DaisyUI",
|
||||
Body: "DaisyUI is the Tailwind CSS plugin you will love! It provides useful component class names to help you write less code and build faster. No JavaScript requirements. Visit <a href=\"https://daisyui.com/\">daisyui.com</a> to learn more.",
|
||||
},
|
||||
},
|
||||
),
|
||||
Div(Class("mb-4")),
|
||||
H2(Text("Backend")),
|
||||
P(Text("The following incredible projects provide the foundation of the Go backend. See the repository for a complete list of included projects.")),
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@ import (
|
|||
"entgo.io/ent/entc/load"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mikestefanello/pagoda/ent/admin"
|
||||
"github.com/mikestefanello/pagoda/pkg/pager"
|
||||
"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/forms"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/components"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
|
|
@ -51,12 +49,12 @@ func AdminEntityList(
|
|||
r.Title = entityTypeName
|
||||
|
||||
genHeader := func() Node {
|
||||
g := make(Group, 0, len(entityList.Columns)+3)
|
||||
g := make(Group, 0, len(entityList.Columns)+2)
|
||||
g = append(g, Th(Text("ID")))
|
||||
for _, h := range entityList.Columns {
|
||||
g = append(g, Th(Text(h)))
|
||||
}
|
||||
g = append(g, Th(), Th())
|
||||
g = append(g, Th())
|
||||
return g
|
||||
}
|
||||
|
||||
|
|
@ -69,14 +67,14 @@ func AdminEntityList(
|
|||
g = append(g,
|
||||
Td(
|
||||
ButtonLink(
|
||||
ColorInfo,
|
||||
r.Path(routenames.AdminEntityEdit(entityTypeName), row.ID),
|
||||
"is-link",
|
||||
"Edit",
|
||||
),
|
||||
),
|
||||
Td(
|
||||
ButtonLink(r.Path(routenames.AdminEntityDelete(entityTypeName), row.ID),
|
||||
"is-danger",
|
||||
Span(Class("mr-2")),
|
||||
ButtonLink(
|
||||
ColorError,
|
||||
r.Path(routenames.AdminEntityDelete(entityTypeName), row.ID),
|
||||
"Delete",
|
||||
),
|
||||
),
|
||||
|
|
@ -92,45 +90,27 @@ func AdminEntityList(
|
|||
return g
|
||||
}
|
||||
|
||||
pagedHref := func(page int) string {
|
||||
return fmt.Sprintf("%s?%s=%d",
|
||||
r.Path(routenames.AdminEntityList(entityTypeName)),
|
||||
pager.QueryKey,
|
||||
page,
|
||||
)
|
||||
}
|
||||
|
||||
return r.Render(layouts.Primary, Group{
|
||||
ButtonLink(
|
||||
r.Path(routenames.AdminEntityAdd(entityTypeName)),
|
||||
"is-primary",
|
||||
fmt.Sprintf("Add %s", entityTypeName),
|
||||
Div(
|
||||
Class("form-control mb-2"),
|
||||
ButtonLink(
|
||||
ColorAccent,
|
||||
r.Path(routenames.AdminEntityAdd(entityTypeName)),
|
||||
fmt.Sprintf("Add %s", entityTypeName),
|
||||
),
|
||||
),
|
||||
Table(
|
||||
Class("table"),
|
||||
Class("table table-zebra mb-2"),
|
||||
THead(
|
||||
Tr(genHeader()),
|
||||
),
|
||||
TBody(genRows()),
|
||||
),
|
||||
Nav(
|
||||
Class("pagination"),
|
||||
A(
|
||||
Classes{
|
||||
"pagination-previous": true,
|
||||
"is-disabled": entityList.Page == 1,
|
||||
},
|
||||
If(entityList.Page != 1, Href(pagedHref(entityList.Page-1))),
|
||||
Text("Previous page"),
|
||||
),
|
||||
A(
|
||||
Classes{
|
||||
"pagination-previous": true,
|
||||
"is-disabled": !entityList.HasNextPage,
|
||||
},
|
||||
If(entityList.HasNextPage, Href(pagedHref(entityList.Page+1))),
|
||||
Text("Next page"),
|
||||
),
|
||||
Pager(
|
||||
entityList.Page,
|
||||
r.Path(routenames.AdminEntityAdd(entityTypeName)),
|
||||
entityList.HasNextPage,
|
||||
"",
|
||||
),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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/components"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/forms"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
|
||||
. "maragu.dev/gomponents"
|
||||
|
|
@ -17,21 +17,25 @@ func ContactUs(ctx echo.Context, form *forms.Contact) error {
|
|||
|
||||
g := Group{
|
||||
Iff(r.Htmx.Target != "contact", func() Node {
|
||||
return 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.")),
|
||||
return Card(CardParams{
|
||||
Title: "Card component",
|
||||
Body: Group{
|
||||
Span(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.")),
|
||||
Span(Text("Only the form below will update async upon submission.")),
|
||||
},
|
||||
)
|
||||
Color: ColorWarning,
|
||||
Size: SizeMedium,
|
||||
})
|
||||
}),
|
||||
Iff(form.IsDone(), func() Node {
|
||||
return 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."),
|
||||
)
|
||||
return Card(CardParams{
|
||||
Title: "Thank you!",
|
||||
Body: Group{
|
||||
Span(Text("No email was actually sent but this entire operation was handled server-side and degrades without JavaScript enabled.")),
|
||||
},
|
||||
Color: ColorSuccess,
|
||||
Size: SizeLarge,
|
||||
})
|
||||
}),
|
||||
Iff(!form.IsDone(), func() Node {
|
||||
return form.Render(r)
|
||||
|
|
|
|||
|
|
@ -21,19 +21,19 @@ func UploadFile(ctx echo.Context, files []*models.File) error {
|
|||
}
|
||||
|
||||
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(),
|
||||
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.")),
|
||||
Divider(""),
|
||||
forms.File{}.Render(r),
|
||||
Hr(),
|
||||
Divider(""),
|
||||
H3(
|
||||
Class("title"),
|
||||
Text("Uploaded files"),
|
||||
),
|
||||
Message("is-warning", "", P(Text("Below are all files in the configured upload directory."))),
|
||||
Card(CardParams{
|
||||
Body: Group{Text("Below are all files in the configured upload directory.")},
|
||||
Color: ColorWarning,
|
||||
Size: SizeMedium,
|
||||
}),
|
||||
Table(
|
||||
Class("table"),
|
||||
THead(
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
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/icons"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/models"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/components"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
|
|
@ -38,69 +36,71 @@ func Home(ctx echo.Context, posts *models.Posts) error {
|
|||
|
||||
headerMsg := func() Node {
|
||||
return Group{
|
||||
Section(
|
||||
Class("hero is-info welcome is-small mb-3"),
|
||||
Div(
|
||||
Class("hero-body"),
|
||||
Div(
|
||||
Class("container"),
|
||||
H1(
|
||||
Class("title"),
|
||||
Iff(r.IsAuth, func() Node {
|
||||
return Text(fmt.Sprintf("Hello, %s", r.AuthUser.Name))
|
||||
}),
|
||||
If(!r.IsAuth, Text("Hello")),
|
||||
),
|
||||
H2(
|
||||
Class("subtitle"),
|
||||
If(!r.IsAuth, Text("Please login in to your account.")),
|
||||
If(r.IsAuth, Text("Welcome back!")),
|
||||
),
|
||||
),
|
||||
),
|
||||
Stats(
|
||||
Stat{
|
||||
Title: "User name",
|
||||
Value: func() string {
|
||||
if r.IsAuth {
|
||||
return r.AuthUser.Name
|
||||
}
|
||||
return "(not logged in)"
|
||||
}(),
|
||||
Description: "The logged in user's name",
|
||||
Icon: icons.UserCircle(),
|
||||
},
|
||||
Stat{
|
||||
Title: "Admin status",
|
||||
Value: func() string {
|
||||
if r.IsAdmin {
|
||||
return "Administrator"
|
||||
}
|
||||
return "Non-administrator"
|
||||
}(),
|
||||
Description: "Use `make admin` to create an admin account",
|
||||
Icon: icons.LockClosed(),
|
||||
},
|
||||
Stat{
|
||||
Title: "GitHub Stars",
|
||||
Value: "2,500+",
|
||||
Description: "Star if you like Pagoda",
|
||||
Icon: icons.Star(),
|
||||
},
|
||||
),
|
||||
Section(
|
||||
Class("hero is-light is-small mb-5"),
|
||||
Div(
|
||||
Class("hero-body"),
|
||||
Div(
|
||||
Class("container"),
|
||||
B(Text("Admin status: ")),
|
||||
Span(
|
||||
Classes{
|
||||
"tag": true,
|
||||
"is-success": r.IsAdmin,
|
||||
"is-danger": !r.IsAdmin,
|
||||
},
|
||||
Text(fmt.Sprint(r.IsAdmin)),
|
||||
),
|
||||
If(!r.IsAdmin, Span(
|
||||
Class("is-size-7 ml-3"),
|
||||
Raw(`(<a href="https://github.com/mikestefanello/pagoda#create-an-admin-account">click here</a> for instructions to make an admin account)`),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
H2(Class("title"), Text("Recent posts")),
|
||||
H3(Class("subtitle"), Text("Below is an example of both paging and AJAX fetching using HTMX")),
|
||||
H2(Text("Recent posts")),
|
||||
Span(Text("Below is an example of both paging and AJAX fetching using HTMX")),
|
||||
}
|
||||
}
|
||||
|
||||
filesMsg := func() Node {
|
||||
return 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."),
|
||||
},
|
||||
cards := func() Node {
|
||||
return Div(
|
||||
Class("flex w-full gap-2 mt-5"),
|
||||
Card(CardParams{
|
||||
Title: "Serving files",
|
||||
Body: 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."),
|
||||
},
|
||||
Color: ColorWarning,
|
||||
Size: SizeSmall,
|
||||
}),
|
||||
Card(CardParams{
|
||||
Title: "Documentation",
|
||||
Body: Group{
|
||||
Text("Have you read through the entire documentation? If not, you may be missing functionality or have questions. "),
|
||||
},
|
||||
Footer: Group{
|
||||
ButtonLink(ColorNeutral, "https://github.com/mikestefanello/pagoda?tab=readme-ov-file#table-of-contents", "Learn more"),
|
||||
},
|
||||
Color: ColorNeutral,
|
||||
Size: SizeSmall,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
g := Group{
|
||||
Iff(r.Htmx.Target != "posts", headerMsg),
|
||||
posts.Render(r.Path(routenames.Home)),
|
||||
Iff(r.Htmx.Target != "posts", filesMsg),
|
||||
Iff(r.Htmx.Target != "posts", cards),
|
||||
}
|
||||
|
||||
return r.Render(layouts.Primary, g)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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/components"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/forms"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
|
||||
. "maragu.dev/gomponents"
|
||||
|
|
@ -17,23 +17,23 @@ func AddTask(ctx echo.Context, form *forms.Task) error {
|
|||
|
||||
g := Group{
|
||||
Iff(r.Htmx.Target != "task", func() Node {
|
||||
return 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(Raw("See <i>pkg/tasks</i> and the README for more information.")),
|
||||
})
|
||||
return 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(Raw("See <i>pkg/tasks</i> and the README for more information.")),
|
||||
}
|
||||
}),
|
||||
form.Render(r),
|
||||
Iff(r.Htmx.Target != "task", func() Node {
|
||||
return components.Message(
|
||||
"is-warning",
|
||||
"",
|
||||
Group{
|
||||
If(!r.IsAdmin, P(Text("Log in as an admin in order to access the task and queue monitoring UI."))),
|
||||
If(r.IsAdmin, P(Text("View all queued tasks by clicking on the Tasks link in the sidebar."))),
|
||||
})
|
||||
var text string
|
||||
if r.IsAdmin {
|
||||
text = "View all queued tasks by clicking on the Tasks link in the sidebar."
|
||||
} else {
|
||||
text = "Log in as an admin in order to access the task and queue monitoring UI."
|
||||
}
|
||||
return Group{
|
||||
Div(Class("mt-5")),
|
||||
Alert(ColorWarning, text),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ type (
|
|||
}
|
||||
|
||||
// LayoutFunc is a callback function intended to render your page node within a given layout.
|
||||
// This is handled as a callback in order to automatically support HTMX requests so that you can respond
|
||||
// This is handled as a callback to automatically support HTMX requests so that you can respond
|
||||
// with only the page content and not the entire layout.
|
||||
// See Request.Render().
|
||||
LayoutFunc func(*Request, gomponents.Node) gomponents.Node
|
||||
|
|
|
|||
13
pkg/ui/ui.go
13
pkg/ui/ui.go
|
|
@ -3,8 +3,6 @@ package ui
|
|||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mikestefanello/pagoda/config"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -12,7 +10,12 @@ var (
|
|||
cacheBuster = fmt.Sprint(time.Now().Unix())
|
||||
)
|
||||
|
||||
// File generates a relative URL to a static file including a cache-buster query parameter.
|
||||
func File(filepath string) string {
|
||||
return fmt.Sprintf("/%s/%s?v=%s", config.StaticPrefix, filepath, cacheBuster)
|
||||
// PublicFile generates a relative URL to a public file.
|
||||
func PublicFile(filepath string) string {
|
||||
return fmt.Sprintf("/%s/%s", "files", filepath)
|
||||
}
|
||||
|
||||
// StaticFile generates a relative URL to a static file including a cache-buster query parameter.
|
||||
func StaticFile(filepath string) string {
|
||||
return fmt.Sprintf("/%s/%s?v=%s", "static", filepath, cacheBuster)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,19 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mikestefanello/pagoda/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
func TestPublicFile(t *testing.T) {
|
||||
path := "abc.txt"
|
||||
got := File(path)
|
||||
expected := fmt.Sprintf("/%s/%s?v=%s", config.StaticPrefix, path, cacheBuster)
|
||||
got := PublicFile(path)
|
||||
expected := fmt.Sprintf("/%s/%s", "files", path)
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
|
||||
func TestStaticFile(t *testing.T) {
|
||||
path := "abc.txt"
|
||||
got := StaticFile(path)
|
||||
expected := fmt.Sprintf("/%s/%s?v=%s", "static", path, cacheBuster)
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue