~repos /gromer

#golang#htmx#ssr

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 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" class="ionicon" viewBox="0 0 512 512" fill="rgba(16,185,129,1)">
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="48" width="48">
2
- <title>Checkmark Circle</title>
3
2
  <path
4
- d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm108.25 138.29l-134.4 160a16 16 0 01-12 5.71h-.27a16 16 0 01-11.89-5.3l-57.6-64a16 16 0 1123.78-21.4l45.29 50.32 122.59-145.91a16 16 0 0124.5 20.58z" />
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" class="ionicon" viewBox="0 0 512 512"><title>Close</title><path d="M289.94 256l95-95A24 24 0 00351 127l-95 95-95-95a24 24 0 00-34 34l95 95-95 95a24 24 0 1034 34l95-95 95 95a24 24 0 0034-34z"/></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" class="ionicon" viewBox="0 0 512 512" fill="rgba(156,163,175,1)">
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 c-0 group-hover:opacity-100",
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="{ /assets/icons/unchecked.svg: !todo.Completed, /assets/icons/checked.svg: todo.Completed }" width="24" height="24" />
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="/assets/icons/close.svg" width="24" height="24" />
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", len(todos))
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
- <button class="button">
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
- <img src="/assets/icons/list.svg" width="24" height="24" />
63
+ <img src="/icons/check-all.svg?fill=gray-400" />
62
- </button>
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?" autofocus="false" autocomplete="off">
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
- <Todo />
74
+ <Todo></Todo>
48
- <TodoCount filter="all" page="1" />
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
- <Todo />
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" || at.Key == "src" {
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
- p += getClassName(parent, k)
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 arr[0] == "placeholder" {
551
+ if prefix == "placeholder" {
546
- p += "\n" + getClassName(parent, k) + "::placeholder" + " {\n"
552
+ p += "\n" + className + "::placeholder" + " {\n"
547
- if s, ok := twClassLookup[arr[1]]; ok {
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()