Skip to content

Commit d240598

Browse files
committed
Refactor HTML components
1 parent 7cabdb3 commit d240598

File tree

12 files changed

+241
-189
lines changed

12 files changed

+241
-189
lines changed

html/nav.go renamed to html/components/EventNav.go

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package html
1+
package components
22

33
import (
44
"encoding/json"
@@ -10,51 +10,14 @@ import (
1010
. "maragu.dev/gomponents/html"
1111
)
1212

13-
// UserNav renders the user navigation.
14-
func UserNav(user *domain.User, children Node) Node {
15-
return Nav(Class("sticky top-0 bg-white max-w-3xl mx-auto z-1"),
16-
Ul(Class("flex justify-between font-semibold flex-row space-x-8 mb-5"),
17-
// TODO: find better icons
18-
Li(Class("justify-self-start align-start"),
19-
A(Class("inline-block p-2"), Href("/"), Text("Home")),
20-
A(Class("inline-block p-2"), Title("RSS feed"), Href("/feed"), rssIcon()),
21-
A(Class("inline-block p-2"), Title("iCal URL"), ID("ical-link"), calendarIcon()),
22-
A(Class("inline-block p-2"), Title("Add to Google Calendar"), ID("google-calendar-link"), Target("_blank"), calendarIcon()),
23-
Script(Raw(`document.getElementById("ical-link").setAttribute("href", "webcal://" + window.location.host + "/calendar.ics")`)),
24-
Script(Raw(`document.getElementById("google-calendar-link").setAttribute("href", "https://calendar.google.com/calendar/render?cid=" + "http://" + window.location.host + "/calendar.ics")`)),
25-
),
26-
27-
Iff(user != nil, func() Node {
28-
return Group{
29-
Li(Class("justify-self-end"),
30-
A(Class("inline-block p-2"), Href("/edit/0"), Text("Add event")),
31-
If(user.Role == domain.Admin, Group{
32-
A(Class("inline-block p-2"), Href("/stopwords"), Text("Stop words"), Title("Configure tag cloud stop words")),
33-
A(Class("inline-block p-2"), Href("/users"), Text("Users"), Title("Manage users")),
34-
}),
35-
A(Class("inline-block p-2"), Href("/logout"), Text("Logout")),
36-
),
37-
}
38-
}),
39-
40-
Iff(user == nil, func() Node {
41-
return Li(Class("justify-self-end"),
42-
A(Class("inline-block p-2"), Href("/login"), Text("Login")),
43-
)
44-
}),
45-
),
46-
children,
47-
)
48-
}
49-
50-
type eventNavLink struct {
51-
Text string
52-
URL string
53-
Active bool
54-
}
55-
5613
// EventNav renders the event navigation.
5714
func EventNav(user *domain.User, currentPath, csrf string) Node {
15+
type eventNavLink struct {
16+
Text string
17+
URL string
18+
Active bool
19+
}
20+
5821
links := []eventNavLink{
5922
{
6023
Text: "Upcoming",
@@ -150,7 +113,7 @@ func EventNav(user *domain.User, currentPath, csrf string) Node {
150113
})))),
151114
),
152115
Div(ID("search-spinner"), Class("opacity-0 absolute top-0 right-0 h-full flex items-center mr-2 htmx-indicator"),
153-
spinner(2),
116+
Spinner(2),
154117
),
155118
),
156119
),

html/components/UserNav.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package components
2+
3+
import (
4+
"github.com/mgnsk/calendar/domain"
5+
. "maragu.dev/gomponents"
6+
. "maragu.dev/gomponents/html"
7+
)
8+
9+
// UserNav renders the user navigation.
10+
func UserNav(user *domain.User, children Node) Node {
11+
return Nav(Class("sticky top-0 bg-white max-w-3xl mx-auto z-1"),
12+
Ul(Class("flex justify-between font-semibold flex-row space-x-8 mb-5"),
13+
// TODO: find better icons
14+
Li(Class("justify-self-start align-start"),
15+
A(Class("inline-block p-2"), Href("/"), Text("Home")),
16+
A(Class("inline-block p-2"), Title("RSS feed"), Href("/feed"), rssIcon()),
17+
A(Class("inline-block p-2"), Title("iCal URL"), ID("ical-link"), calendarIcon()),
18+
A(Class("inline-block p-2"), Title("Add to Google Calendar"), ID("google-calendar-link"), Target("_blank"), calendarIcon()),
19+
Script(Raw(`document.getElementById("ical-link").setAttribute("href", "webcal://" + window.location.host + "/calendar.ics")`)),
20+
Script(Raw(`document.getElementById("google-calendar-link").setAttribute("href", "https://calendar.google.com/calendar/render?cid=" + "http://" + window.location.host + "/calendar.ics")`)),
21+
),
22+
23+
Iff(user != nil, func() Node {
24+
return Group{
25+
Li(Class("justify-self-end"),
26+
A(Class("inline-block p-2"), Href("/edit/0"), Text("Add event")),
27+
If(user.Role == domain.Admin, Group{
28+
A(Class("inline-block p-2"), Href("/stopwords"), Text("Stop words"), Title("Configure tag cloud stop words")),
29+
A(Class("inline-block p-2"), Href("/users"), Text("Users"), Title("Manage users")),
30+
}),
31+
A(Class("inline-block p-2"), Href("/logout"), Text("Logout")),
32+
),
33+
}
34+
}),
35+
36+
Iff(user == nil, func() Node {
37+
return Li(Class("justify-self-end"),
38+
A(Class("inline-block p-2"), Href("/login"), Text("Login")),
39+
)
40+
}),
41+
),
42+
children,
43+
)
44+
}

html/components/form.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package components
2+
3+
import (
4+
"strconv"
5+
6+
. "maragu.dev/gomponents"
7+
. "maragu.dev/gomponents/html"
8+
)
9+
10+
// ButtonElement is a button.
11+
func ButtonElement(text string, children ...Node) Node {
12+
nodes := []Node{
13+
buttonClasses(),
14+
Type("button"),
15+
Text(text),
16+
}
17+
nodes = append(nodes, children...)
18+
19+
// TODO: Group can't use attrs?
20+
return Button(
21+
nodes...,
22+
)
23+
}
24+
25+
// SubmitButtonElement is a submit button.
26+
func SubmitButtonElement(text string, children ...Node) Node {
27+
nodes := []Node{
28+
buttonClasses(),
29+
Type("submit"),
30+
Text(text),
31+
}
32+
nodes = append(nodes, children...)
33+
34+
// TODO: Group can't use attrs?
35+
return Button(
36+
nodes...,
37+
)
38+
}
39+
40+
// InputElement is an input element.
41+
// TODO: struct arguments
42+
func InputElement(name, typ, placeholder string, value, err string, required, autocomplete bool) Node {
43+
return withErrors(err,
44+
Input(baseInputClasses(err != ""),
45+
Name(name),
46+
Type(typ),
47+
If(required, Placeholder(placeholder+"*")),
48+
If(!required, Placeholder(placeholder)),
49+
Value(value),
50+
If(required, Required()),
51+
If(!autocomplete, AutoComplete("off")),
52+
),
53+
)
54+
}
55+
56+
// TextareaElement is a textarea element.
57+
func TextareaElement(name string, value, err string, rows uint64, required, autocomplete bool) Node {
58+
return withErrors(err,
59+
Textarea(baseInputClasses(err != ""),
60+
Name(name),
61+
Text(value),
62+
Rows(strconv.FormatUint(rows, 10)),
63+
If(required, Required()),
64+
If(!autocomplete, AutoComplete("off")),
65+
),
66+
)
67+
}
68+
69+
// DateTimeLocalInput is a datetime-local element.
70+
func DateTimeLocalInput(name string, value, err string, required, autocomplete bool) Node {
71+
return withErrors(err,
72+
Input(baseInputClasses(err != ""),
73+
Name(name),
74+
Type("datetime-local"),
75+
Value(value),
76+
If(required, Required()),
77+
If(!autocomplete, AutoComplete("off")),
78+
),
79+
)
80+
}

html/icons.go renamed to html/components/icons.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
package html
1+
package components
22

33
import (
44
. "maragu.dev/gomponents"
55
. "maragu.dev/gomponents/html"
66
)
77

8-
func loadingSpinner() Node {
8+
// LoadingSpinner is a HTMX-aware loading spinner element.
9+
func LoadingSpinner() Node {
910
return Div(ID("loading-spinner"), Class("absolute left-0 right-0 my-5 htmx-indicator m-10 mx-auto flex justify-center"),
10-
spinner(8),
11+
Spinner(8),
1112
)
1213
}
1314

14-
func spinner(size int) Node {
15+
// Spinner is a spinner SVG element.
16+
func Spinner(size int) Node {
1517
return Rawf(`<svg class="w-%d h-%d text-gray-300 animate-spin" viewBox="0 0 64 64" fill="none"
1618
xmlns="http://www.w3.org/2000/svg" width="24" height="24">
1719
<path

html/components/util.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package components
2+
3+
import (
4+
"maps"
5+
6+
. "maragu.dev/gomponents"
7+
. "maragu.dev/gomponents/components"
8+
. "maragu.dev/gomponents/html"
9+
)
10+
11+
// BaseFormElementClasses is base form element classes.
12+
// TODO: refactor
13+
func BaseFormElementClasses() Classes {
14+
return Classes{
15+
"py-2": true,
16+
"px-3": true,
17+
"rounded": true,
18+
"mb-3": true,
19+
"block": true,
20+
"w-full": true,
21+
"mx-auto": true,
22+
}
23+
}
24+
25+
func baseInputClasses(hasError bool) Classes {
26+
classes := BaseFormElementClasses()
27+
maps.Copy(classes, Classes{
28+
"border": true,
29+
"border-gray-200": true,
30+
"bg-red-100": hasError,
31+
})
32+
33+
return classes
34+
}
35+
36+
func withErrors(err string, input Node) Node {
37+
return Group{
38+
If(err != "", P(Class("text-red-500 text-sm italic"), Text(err))),
39+
input,
40+
}
41+
}
42+
43+
func buttonClasses() Classes {
44+
classes := baseInputClasses(false)
45+
maps.Copy(classes, Classes{
46+
"mt-3": true,
47+
"font-bold": true,
48+
"hover:cursor-pointer": true,
49+
"hover:bg-amber-600/5": true,
50+
})
51+
52+
return classes
53+
}
54+
55+
func must[V any](v V, err error) V {
56+
if err != nil {
57+
panic(err)
58+
}
59+
return v
60+
}

html/editevent.go

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strconv"
77

88
"github.com/mgnsk/calendar/contract"
9+
"github.com/mgnsk/calendar/html/components"
910
. "maragu.dev/gomponents"
1011
. "maragu.dev/gomponents/html"
1112
)
@@ -17,7 +18,8 @@ func EditEventMain(form contract.EditEventForm, errs url.Values, csrf string) No
1718
Form(ID("edit-form"), Class("w-full px-3 py-4 mx-auto"),
1819
Method("POST"),
1920

20-
H1(baseFormElementClasses(),
21+
// TODO: refactor this usage of classes
22+
H1(components.BaseFormElementClasses(),
2123
Text("Status: "),
2224
B(Text(func() string {
2325
if form.IsDraft || form.EventID == 0 {
@@ -27,20 +29,20 @@ func EditEventMain(form contract.EditEventForm, errs url.Values, csrf string) No
2729
}())),
2830
),
2931

30-
input("title", "text", "Title", form.Title, errs.Get("title"), true, false),
31-
input("url", "url", "URL", form.URL, errs.Get("url"), false, false),
32-
dateTimeLocalInput("start_at", form.StartAt, errs.Get("start_at"), true, false),
32+
components.InputElement("title", "text", "Title", form.Title, errs.Get("title"), true, false),
33+
components.InputElement("url", "url", "URL", form.URL, errs.Get("url"), false, false),
34+
components.DateTimeLocalInput("start_at", form.StartAt, errs.Get("start_at"), true, false),
3335

3436
Div(Class("relative"),
35-
input("location", "text", "Location", form.Location, errs.Get("location"), true, false),
37+
components.InputElement("location", "text", "Location", form.Location, errs.Get("location"), true, false),
3638
Input(Type("hidden"), Name("osm_type"), Value(form.OSMType)),
3739
Input(Type("hidden"), Name("osm_id"), Value(strconv.FormatUint(form.OSMID, 10))),
3840
Div(ID("location-spinner"), Class("opacity-0 absolute top-0 right-0 h-full flex items-center mr-2"),
39-
spinner(2),
41+
components.Spinner(2),
4042
),
4143
),
4244

43-
textarea("desc", form.Description, errs.Get("desc"), true, false),
45+
components.TextareaElement("desc", form.Description, errs.Get("desc"), 3, true, false),
4446

4547
Input(Type("hidden"), Name("csrf"), Value(csrf)),
4648
Input(Type("hidden"), Name("easymde_cache_key"), Value(form.EventID.String())),
@@ -54,14 +56,10 @@ func EditEventMain(form contract.EditEventForm, errs url.Values, csrf string) No
5456
// Draft or new event.
5557
Iff(form.IsDraftOrNew(), func() Node {
5658
return Group{
57-
Button(buttonClasses(),
58-
Type("submit"),
59-
Text("Save Draft"),
59+
components.SubmitButtonElement("Save Draft",
6060
FormAction(fmt.Sprintf("/edit/%s?draft=1", form.EventID.String())),
6161
),
62-
Button(buttonClasses(),
63-
Type("submit"),
64-
Text("Publish"),
62+
components.SubmitButtonElement("Publish",
6563
Attr("onclick", "return confirm('Confirm publishing this event')"),
6664
FormAction(fmt.Sprintf("/edit/%s?draft=0", form.EventID.String())),
6765
),
@@ -71,14 +69,10 @@ func EditEventMain(form contract.EditEventForm, errs url.Values, csrf string) No
7169
// Already published event.
7270
Iff(!form.IsDraftOrNew(), func() Node {
7371
return Group{
74-
Button(buttonClasses(),
75-
Type("submit"),
76-
Text("Save"),
72+
components.SubmitButtonElement("Save",
7773
FormAction(fmt.Sprintf("/edit/%s?draft=0", form.EventID.String())),
7874
),
79-
Button(buttonClasses(),
80-
Type("submit"),
81-
Text("Unpublish"),
75+
components.SubmitButtonElement("Unpublish",
8276
Attr("onclick", "return confirm('Confirm unpublishing this event')"),
8377
FormAction(fmt.Sprintf("/edit/%s?draft=1", form.EventID.String())),
8478
),

0 commit comments

Comments
 (0)