~repos /gromer
git clone https://pyrossh.dev/repos/gromer.git
gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.
7a6f153d
—
Peter John 3 years ago
improve stuff
- _example/assets/icons/check-all.svg +3 -0
- _example/assets/icons/checked.svg +2 -3
- _example/assets/icons/close.svg +1 -1
- _example/assets/icons/list.svg +0 -1
- _example/assets/icons/unchecked.svg +1 -4
- _example/components/todo.go +8 -3
- _example/containers/TodoCount.go +7 -1
- _example/main.go +1 -0
- _example/routes/get.go +7 -4
- _example/routes/post.go +35 -4
- go.mod +1 -0
- go.sum +2 -0
- gsx/gsx.go +1 -1
- gsx/twx.go +21 -5
- http.go +21 -0
_example/assets/icons/check-all.svg
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48">
|
|
2
|
+
<path d="M11.1 37.3 4 30.2l2.1-2.1 5 4.95 8.95-8.95 2.1 2.15Zm0-16L4 14.2l2.1-2.1 5 4.95 8.95-8.95 2.1 2.15ZM26 33.5v-3h18v3Zm0-16v-3h18v3Z" />
|
|
3
|
+
</svg>
|
_example/assets/icons/checked.svg
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg"
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48">
|
|
2
|
-
<title>Checkmark Circle</title>
|
|
3
2
|
<path
|
|
4
|
-
d="
|
|
3
|
+
d="M21.05 33.1 35.2 18.95l-2.3-2.25-11.85 11.85-6-6-2.25 2.25ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 24q0-4.15 1.575-7.8 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24 4q4.15 0 7.8 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Z" />
|
|
5
4
|
</svg>
|
_example/assets/icons/close.svg
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg"
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="m12.45 37.65-2.1-2.1L21.9 24 10.35 12.45l2.1-2.1L24 21.9l11.55-11.55 2.1 2.1L26.1 24l11.55 11.55-2.1 2.1L24 26.1Z"/></svg>
|
_example/assets/icons/list.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>List</title><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M160 144h288M160 256h288M160 368h288"/><circle cx="80" cy="144" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="80" cy="256" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="80" cy="368" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>
|
_example/assets/icons/unchecked.svg
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg"
|
|
2
|
-
<title>Ellipse</title>
|
|
3
|
-
<path d="M256 464c-114.69 0-208-93.31-208-208S141.31 48 256 48s208 93.31 208 208-93.31 208-208 208z" />
|
|
4
|
-
</svg>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 24q0-4.15 1.575-7.8 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24 4q4.15 0 7.8 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Z"/></svg>
|
_example/components/todo.go
CHANGED
|
@@ -11,11 +11,16 @@ var TodoStyles = M{
|
|
|
11
11
|
"button-1": "ml-4 text-gray-400",
|
|
12
12
|
"label": "flex-1 min-w-0 flex items-center break-all ml-2 p-2 text-gray-800",
|
|
13
13
|
"striked": "text-gray-500 line-through",
|
|
14
|
-
"button-2": "mr-4 text-red-700
|
|
14
|
+
"button-2": "mr-4 text-red-700 opacity-0 group-hover-opacity-100",
|
|
15
15
|
"unchecked": "text-gray-200",
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
func Todo(c *Context, todo *todos.Todo) *Node {
|
|
19
|
+
checked := "/icons/unchecked.svg?fill=gray-400"
|
|
20
|
+
if todo.Completed {
|
|
21
|
+
checked = "/icons/checked.svg?fill=green-500"
|
|
22
|
+
}
|
|
23
|
+
c.Set("checked", checked)
|
|
19
24
|
return c.Render(`
|
|
20
25
|
<div id="todo-{todo.ID}" class="todo">
|
|
21
26
|
<div class="row">
|
|
@@ -23,7 +28,7 @@ func Todo(c *Context, todo *todos.Todo) *Node {
|
|
|
23
28
|
<input type="hidden" name="intent" value="complete" />
|
|
24
29
|
<input type="hidden" name="id" value="{todo.ID}" />
|
|
25
30
|
<button class="button-1">
|
|
26
|
-
<img src="{
|
|
31
|
+
<img src="{checked}" width="24" height="24" />
|
|
27
32
|
</button>
|
|
28
33
|
</form>
|
|
29
34
|
<label class="{ label: true, striked: todo.Completed }">
|
|
@@ -33,7 +38,7 @@ func Todo(c *Context, todo *todos.Todo) *Node {
|
|
|
33
38
|
<input type="hidden" name="intent" value="delete" />
|
|
34
39
|
<input type="hidden" name="id" value="{todo.ID}" />
|
|
35
40
|
<button class="button-2">
|
|
36
|
-
<img src="/
|
|
41
|
+
<img src="/icons/close.svg?fill=red-500" width="24" height="24" />
|
|
37
42
|
</button>
|
|
38
43
|
</form>
|
|
39
44
|
</div>
|
_example/containers/TodoCount.go
CHANGED
|
@@ -13,7 +13,13 @@ func TodoCount(c *Context, filter string) *Node {
|
|
|
13
13
|
if err != nil {
|
|
14
14
|
return Error(c, err)
|
|
15
15
|
}
|
|
16
|
+
count := 0
|
|
17
|
+
for _, t := range todos {
|
|
18
|
+
if !t.Completed {
|
|
19
|
+
count++
|
|
20
|
+
}
|
|
21
|
+
}
|
|
16
|
-
c.Set("count",
|
|
22
|
+
c.Set("count", count)
|
|
17
23
|
return c.Render(`
|
|
18
24
|
<span id="todo-count" class="todo-count" hx-swap-oob="true">
|
|
19
25
|
<strong>{count}</strong> item left
|
_example/main.go
CHANGED
|
@@ -35,6 +35,7 @@ func main() {
|
|
|
35
35
|
staticRouter.Use(gromer.CompressMiddleware)
|
|
36
36
|
gromer.StaticRoute(staticRouter, "/gromer/", gromer_assets.FS)
|
|
37
37
|
gromer.StaticRoute(staticRouter, "/assets/", assets.FS)
|
|
38
|
+
gromer.IconsRoute(staticRouter, "/icons/", assets.FS)
|
|
38
39
|
gromer.PageStylesRoute(staticRouter, "/styles.css")
|
|
39
40
|
gromer.ComponentStylesRoute(staticRouter, "/components.css")
|
|
40
41
|
|
_example/routes/get.go
CHANGED
|
@@ -57,12 +57,15 @@ func GET(c *Context, params GetParams) (*Node, int, error) {
|
|
|
57
57
|
</header>
|
|
58
58
|
<main class="main">
|
|
59
59
|
<div class="input-box">
|
|
60
|
-
<
|
|
60
|
+
<form hx-target="#todo-list" hx-post="/">
|
|
61
|
+
<input type="hidden" name="intent" value="select_all" />
|
|
62
|
+
<button id="check-all" class="button" hx-swap-oob="true">
|
|
61
|
-
|
|
63
|
+
<img src="/icons/check-all.svg?fill=gray-400" />
|
|
62
|
-
|
|
64
|
+
</button>
|
|
65
|
+
</form>
|
|
63
66
|
<form class="input-form" hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
|
|
64
67
|
<input type="hidden" name="intent" value="create" />
|
|
65
|
-
<input id="text" name="text" class="input" placeholder="What needs to be done?"
|
|
68
|
+
<input id="text" name="text" class="input" placeholder="What needs to be done?" autocomplete="off">
|
|
66
69
|
</form>
|
|
67
70
|
</div>
|
|
68
71
|
<TodoList id="todo-list" page="{params.Page}" filter="{params.Filter}"></TodoList>
|
_example/routes/post.go
CHANGED
|
@@ -15,7 +15,33 @@ type PostParams struct {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
func POST(c *Context, params PostParams) (*Node, int, error) {
|
|
18
|
+
if params.Intent == "select_all" {
|
|
19
|
+
allTodos, err := todos.GetAllTodo(c, todos.GetAllTodoParams{
|
|
20
|
+
Filter: "all",
|
|
21
|
+
Limit: 1000,
|
|
22
|
+
})
|
|
23
|
+
if err != nil {
|
|
24
|
+
return nil, 500, err
|
|
25
|
+
}
|
|
26
|
+
for _, t := range allTodos {
|
|
27
|
+
_, err := todos.UpdateTodo(c, t.ID, todos.UpdateTodoParams{
|
|
28
|
+
Text: t.Text,
|
|
29
|
+
Completed: true,
|
|
30
|
+
})
|
|
31
|
+
if err != nil {
|
|
32
|
+
return nil, 500, err
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return c.Render(`
|
|
36
|
+
<div>
|
|
37
|
+
<TodoList id="todo-list" filter="all" page="1"></TodoList>
|
|
38
|
+
<TodoCount filter="all" page="1"></TodoCount>
|
|
39
|
+
<button id="check-all" class="button" hx-swap-oob="true">
|
|
40
|
+
<img src="/icons/check-all.svg?fill=green-500" />
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
`), 200, nil
|
|
18
|
-
if params.Intent == "clear_completed" {
|
|
44
|
+
} else if params.Intent == "clear_completed" {
|
|
19
45
|
allTodos, err := todos.GetAllTodo(c, todos.GetAllTodoParams{
|
|
20
46
|
Filter: "all",
|
|
21
47
|
Limit: 1000,
|
|
@@ -44,8 +70,10 @@ func POST(c *Context, params PostParams) (*Node, int, error) {
|
|
|
44
70
|
}
|
|
45
71
|
c.Set("todo", todo)
|
|
46
72
|
return c.Render(`
|
|
73
|
+
<div>
|
|
47
|
-
|
|
74
|
+
<Todo></Todo>
|
|
48
|
-
|
|
75
|
+
<TodoCount filter="all" page="1"></TodoCount>
|
|
76
|
+
</div>
|
|
49
77
|
`), 200, nil
|
|
50
78
|
} else if params.Intent == "delete" {
|
|
51
79
|
_, err := todos.DeleteTodo(c, params.ID)
|
|
@@ -67,7 +95,10 @@ func POST(c *Context, params PostParams) (*Node, int, error) {
|
|
|
67
95
|
}
|
|
68
96
|
c.Set("todo", todo)
|
|
69
97
|
return c.Render(`
|
|
98
|
+
<div>
|
|
99
|
+
<Todo></Todo>
|
|
100
|
+
<TodoCount filter="all" page="1"></TodoCount>
|
|
70
|
-
<
|
|
101
|
+
</div>
|
|
71
102
|
`), 200, nil
|
|
72
103
|
}
|
|
73
104
|
return nil, 404, fmt.Errorf("Intent not specified: %s", params.Intent)
|
go.mod
CHANGED
|
@@ -20,6 +20,7 @@ require (
|
|
|
20
20
|
require (
|
|
21
21
|
cloud.google.com/go v0.94.0 // indirect
|
|
22
22
|
cloud.google.com/go/firestore v1.5.0 // indirect
|
|
23
|
+
github.com/JoshVarga/svgparser v0.0.0-20200804023048-5eaba627a7d1 // indirect
|
|
23
24
|
github.com/alecthomas/repr v0.1.0 // indirect
|
|
24
25
|
github.com/aymerick/douceur v0.2.0 // indirect
|
|
25
26
|
github.com/aymerick/raymond v2.0.2+incompatible // indirect
|
go.sum
CHANGED
|
@@ -98,6 +98,8 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
|
|
|
98
98
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
99
99
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
|
100
100
|
github.com/GoogleCloudPlatform/cloudsql-proxy v1.24.0/go.mod h1:3tx938GhY4FC+E1KT/jNjDw7Z5qxAEtIiERJ2sXjnII=
|
|
101
|
+
github.com/JoshVarga/svgparser v0.0.0-20200804023048-5eaba627a7d1 h1:RAQocNl+YQYGPt5yh4SR5zFUIHKrXnLhjIGhHO4Vwnc=
|
|
102
|
+
github.com/JoshVarga/svgparser v0.0.0-20200804023048-5eaba627a7d1/go.mod h1:tMmgUTWcco9d1ZmK7zjxuTv7XWZhyutXIsgu0uJ3gDw=
|
|
101
103
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
|
102
104
|
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
|
103
105
|
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
gsx/gsx.go
CHANGED
|
@@ -359,7 +359,7 @@ func populate(c *Context, n *html.Node) {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
} else if at.Val != "" && strings.Contains(at.Val, "{") {
|
|
362
|
-
if at.Key == "class"
|
|
362
|
+
if at.Key == "class" {
|
|
363
363
|
classes := []string{}
|
|
364
364
|
kvstrings := strings.Split(strings.TrimSpace(removeBrackets(at.Val)), ",")
|
|
365
365
|
for _, kv := range kvstrings {
|
gsx/twx.go
CHANGED
|
@@ -514,6 +514,10 @@ func mapApply(obj KeyValues) {
|
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
func GetColor(k string) string {
|
|
518
|
+
return colors.Values[k]
|
|
519
|
+
}
|
|
520
|
+
|
|
517
521
|
func getClassName(parent, k string) string {
|
|
518
522
|
if parent != "" {
|
|
519
523
|
if k == "container" {
|
|
@@ -530,8 +534,8 @@ func computeCss(classMap M, parent string) string {
|
|
|
530
534
|
for k, v := range classMap {
|
|
531
535
|
switch it := v.(type) {
|
|
532
536
|
case string:
|
|
533
|
-
|
|
537
|
+
className := getClassName(parent, k)
|
|
534
|
-
p += " {\n"
|
|
538
|
+
p += className + " {\n"
|
|
535
539
|
classes := strings.Split(it, " ")
|
|
536
540
|
for _, c := range classes {
|
|
537
541
|
if s, ok := twClassLookup[c]; ok {
|
|
@@ -542,9 +546,17 @@ func computeCss(classMap M, parent string) string {
|
|
|
542
546
|
for _, c := range classes {
|
|
543
547
|
if strings.Contains(c, ":") {
|
|
544
548
|
arr := strings.Split(c, ":")
|
|
549
|
+
prefix := arr[0]
|
|
550
|
+
class := arr[1]
|
|
545
|
-
if
|
|
551
|
+
if prefix == "placeholder" {
|
|
546
|
-
p += "\n" +
|
|
552
|
+
p += "\n" + className + "::placeholder" + " {\n"
|
|
547
|
-
if s, ok := twClassLookup[
|
|
553
|
+
if s, ok := twClassLookup[class]; ok {
|
|
554
|
+
p += " " + s + "\n"
|
|
555
|
+
}
|
|
556
|
+
p += "}\n"
|
|
557
|
+
} else if prefix == "group-hover" {
|
|
558
|
+
p += "\n" + className + ":hover" + " {\n"
|
|
559
|
+
if s, ok := twClassLookup[class]; ok {
|
|
548
560
|
p += " " + s + "\n"
|
|
549
561
|
}
|
|
550
562
|
p += "}\n"
|
|
@@ -607,4 +619,8 @@ img, video { max-width: 100%; height: auto; }
|
|
|
607
619
|
[hidden] { display: none; }
|
|
608
620
|
*, ::before, ::after { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); }
|
|
609
621
|
form { display: flex; }
|
|
622
|
+
|
|
623
|
+
.group:hover .group-hover-opacity-100 {
|
|
624
|
+
opacity: 1;
|
|
625
|
+
}
|
|
610
626
|
`
|
http.go
CHANGED
|
@@ -259,6 +259,27 @@ func StaticRoute(router *mux.Router, path string, fs embed.FS) {
|
|
|
259
259
|
router.PathPrefix(path).Methods("GET").Handler(http.StripPrefix(path, http.FileServer(http.FS(fs))))
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
func IconsRoute(router *mux.Router, path string, fs embed.FS) {
|
|
263
|
+
router.PathPrefix(path).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
264
|
+
err := r.ParseForm()
|
|
265
|
+
if err != nil {
|
|
266
|
+
RespondError(w, 400, err)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
data, err := fs.ReadFile(strings.TrimPrefix(r.URL.Path, "/"))
|
|
270
|
+
if err != nil {
|
|
271
|
+
RespondError(w, 404, err)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
fill := r.Form.Get("fill")
|
|
275
|
+
color := gsx.GetColor(fill)
|
|
276
|
+
svg := strings.ReplaceAll(string(data), "<svg", fmt.Sprintf(`<svg fill="%s" `, color))
|
|
277
|
+
w.Header().Set("Content-Type", "image/svg+xml")
|
|
278
|
+
w.WriteHeader(200)
|
|
279
|
+
w.Write([]byte(svg))
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
262
283
|
func PageStylesRoute(router *mux.Router, route string) {
|
|
263
284
|
router.Path(route).Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
264
285
|
err := r.ParseForm()
|