From 3d18f2d89b7359b7a244e79052005515edda5f4c Mon Sep 17 00:00:00 2001 From: maddalax Date: Wed, 11 Sep 2024 12:31:40 -0500 Subject: [PATCH] so much stuff --- h/base.go | 7 +- h/livereload.go | 2 +- h/render.go | 18 ++- h/tag.go | 256 +++++++++++++++++++++++++++------------ h/util.go | 44 +++++++ js/mhtml.js | 66 ++++++++-- main.go | 23 +++- news/views.go | 7 +- pages/base/root.go | 11 +- pages/generated.go | 3 + pages/index.go | 11 +- pages/news.index.go | 23 +--- pages/patients.index.go | 21 ++++ partials/button.go | 26 ++++ partials/generated.go | 18 ++- partials/nav.go | 22 ++++ partials/news.go | 72 +++++++++++ partials/patient.go | 65 ++++++++++ partials/sheet.go | 56 --------- partials/sheet/sheet.go | 36 ++++++ tooling/astgen/entry.go | 81 +++++++++++-- tooling/astgen/writer.go | 2 +- ui/button.go | 19 ++- 23 files changed, 677 insertions(+), 212 deletions(-) create mode 100644 h/util.go create mode 100644 pages/patients.index.go create mode 100644 partials/button.go create mode 100644 partials/nav.go create mode 100644 partials/news.go create mode 100644 partials/patient.go delete mode 100644 partials/sheet.go create mode 100644 partials/sheet/sheet.go diff --git a/h/base.go b/h/base.go index b5fcc57..e5f4db2 100644 --- a/h/base.go +++ b/h/base.go @@ -2,6 +2,7 @@ package h import ( "github.com/gofiber/fiber/v2" + "html" "net/http" "reflect" "runtime" @@ -14,6 +15,10 @@ type Partial struct { Root *Node } +func (p *Partial) ToNode() *Node { + return p.Root +} + type Page struct { Root *Node HttpMethod string @@ -55,5 +60,5 @@ func GetPartialPath(partial func(ctx *fiber.Ctx) *Partial) string { } func GetPartialPathWithQs(partial func(ctx *fiber.Ctx) *Partial, qs string) string { - return GetPartialPath(partial) + "?" + qs + return html.EscapeString(GetPartialPath(partial) + "?" + qs) } diff --git a/h/livereload.go b/h/livereload.go index 26fa5ff..c029ed1 100644 --- a/h/livereload.go +++ b/h/livereload.go @@ -25,7 +25,7 @@ func LiveReloadHandler(c *fiber.Ctx) error { } func LiveReload() *Node { - return Div(Get("/livereload"), Trigger("every 100ms")) + return Div(Get("/livereload"), Trigger("every 2s")) } func AddLiveReloadHandler(path string, app *fiber.App) { diff --git a/h/render.go b/h/render.go index d8ff3d9..ed26c89 100644 --- a/h/render.go +++ b/h/render.go @@ -9,7 +9,8 @@ import ( const FlagSkip = "skip" const FlagText = "text" const FlagRaw = "raw" -const FlagAttributeList = "attribute-list" +const FlagAttributeList = "x-attribute-list" +const FlagChildrenList = "x-children-list" type Builder struct { builder *strings.Builder @@ -38,6 +39,21 @@ func (page Builder) renderNode(node *Node) { node.attributes = map[string]string{} } + flatChildren := make([]*Node, 0) + for _, child := range node.children { + flatChildren = append(flatChildren, child) + if child.tag == FlagChildrenList { + for _, gc := range child.children { + flatChildren = append(flatChildren, gc) + } + child.tag = FlagSkip + } + } + + if len(flatChildren) > 0 { + node.children = flatChildren + } + for _, child := range node.children { if child == nil { diff --git a/h/tag.go b/h/tag.go index 79bacc4..3d67e9c 100644 --- a/h/tag.go +++ b/h/tag.go @@ -2,6 +2,7 @@ package h import ( "encoding/json" + "fmt" "github.com/gofiber/fiber/v2" "html" "net/url" @@ -18,17 +19,6 @@ type Node struct { changed bool } -func NewNode(tag string) Node { - return Node{ - tag: tag, - attributes: nil, - children: nil, - text: "", - value: "", - id: "", - } -} - type Action struct { Type string Target *Node @@ -76,22 +66,39 @@ func MergeClasses(classes ...string) string { } func Id(value string) *Node { + if strings.HasPrefix(value, "#") { + value = value[1:] + } return Attribute("id", value) } -func Attribute(key string, value string) *Node { +func Attributes(attrs map[string]string) *Node { return &Node{ - tag: "attribute", - attributes: map[string]string{ - key: value, - }, + tag: "attribute", + attributes: attrs, } } +func Attribute(key string, value string) *Node { + return Attributes(map[string]string{key: value}) +} + +func Disabled() *Node { + return Attribute("disabled", "") +} + func Get(path string) *Node { return Attribute("hx-get", path) } +func GetPartial(partial func(ctx *fiber.Ctx) *Partial) *Node { + return Get(GetPartialPath(partial)) +} + +func GetPartialWithQs(partial func(ctx *fiber.Ctx) *Partial, qs string) *Node { + return Get(GetPartialPathWithQs(partial, qs)) +} + func CreateTriggers(triggers ...string) []string { return triggers } @@ -100,14 +107,24 @@ type ReloadParams struct { Triggers []string } +func ViewOnLoad(partial func(ctx *fiber.Ctx) *Partial) *Node { + return View(partial, ReloadParams{ + Triggers: CreateTriggers("load"), + }) +} + func View(partial func(ctx *fiber.Ctx) *Partial, params ReloadParams) *Node { - return &Node{ - tag: "attribute", - attributes: map[string]string{ - "hx-get": GetPartialPath(partial), - "hx-trigger": strings.Join(params.Triggers, ", "), - }, - } + return Div(Attributes(map[string]string{ + "hx-get": GetPartialPath(partial), + "hx-trigger": strings.Join(params.Triggers, ", "), + })) +} + +func ViewWithTriggers(partial func(ctx *fiber.Ctx) *Partial, triggers ...string) *Node { + return Div(Attributes(map[string]string{ + "hx-get": GetPartialPath(partial), + "hx-trigger": strings.Join(triggers, ", "), + })) } func GetWithQs(path string, qs map[string]string) *Node { @@ -142,6 +159,10 @@ func Text(text string) *Node { } } +func Pf(format string, args ...interface{}) *Node { + return P(fmt.Sprintf(format, args...)) +} + func Target(target string) *Node { return Attribute("hx-target", target) } @@ -166,37 +187,32 @@ func Placeholder(placeholder string) *Node { return Attribute("placeholder", placeholder) } -func Swap(swap string) *Node { - return Attribute("hx-swap", swap) +func OutOfBandSwap(selector string) *Node { + return Attribute("hx-swap-oob", + Ternary(selector == "", "true", selector)) } func Click(value string) *Node { return Attribute("onclick", value) } -func OnClickWs(handler string) *Node { - return Attribute("data-ws-click", handler) +func Tag(tag string, children ...*Node) *Node { + return &Node{ + tag: tag, + children: children, + } } func Html(children ...*Node) *Node { - return &Node{ - tag: "html", - children: children, - } + return Tag("html", children...) } func Head(children ...*Node) *Node { - return &Node{ - tag: "head", - children: children, - } + return Tag("head", children...) } func Body(children ...*Node) *Node { - return &Node{ - tag: "body", - children: children, - } + return Tag("body", children...) } func Script(url string) *Node { @@ -222,10 +238,7 @@ func RawScript(text string) *Node { } func Div(children ...*Node) *Node { - return &Node{ - tag: "div", - children: children, - } + return Tag("div", children...) } func Input(inputType string, children ...*Node) *Node { @@ -238,26 +251,6 @@ func Input(inputType string, children ...*Node) *Node { } } -func HStack(children ...*Node) *Node { - return &Node{ - tag: "div", - attributes: map[string]string{ - "class": "flex gap-2", - }, - children: children, - } -} - -func VStack(children ...*Node) *Node { - return &Node{ - tag: "div", - attributes: map[string]string{ - "class": "flex flex-col gap-2", - }, - children: children, - } -} - func List[T any](items []T, mapper func(item T) *Node) *Node { node := &Node{ tag: "", @@ -290,10 +283,7 @@ func AppendChildren(node *Node, children ...*Node) *Node { } func Button(children ...*Node) *Node { - return &Node{ - tag: "button", - children: children, - } + return Tag("button", children...) } func Indicator(tag string) *Node { @@ -316,6 +306,10 @@ func A(text string, children ...*Node) *Node { } } +func Nav(children ...*Node) *Node { + return Tag("nav", children...) +} + func Empty() *Node { return &Node{ tag: "", @@ -327,11 +321,59 @@ func BeforeRequestSetHtml(children ...*Node) *Node { return Attribute("hx-on::before-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`) } +func BeforeRequestSetAttribute(key string, value string) *Node { + return Attribute("hx-on::before-request", `this.setAttribute('`+key+`', '`+value+`')`) +} + +func BeforeRequestSetText(text string) *Node { + return Attribute("hx-on::before-request", `this.innerText = '`+text+`'`) +} + +func AfterRequestRemoveAttribute(key string, value string) *Node { + return Attribute("hx-on::after-request", `this.removeAttribute('`+key+`')`) +} + +func IfQueryParam(key string, node *Node) *Node { + return Fragment(Attribute("hx-if-qp:"+key, "true"), node) +} + +func Hidden() *Node { + return Attribute("style", "display:none") +} + +func MatchQueryParam(defaultValue string, active string, m map[string]*Node) *Node { + + rendered := make(map[string]string) + for s, node := range m { + rendered[s] = Render(node) + } + + root := Tag("span", + m[active], + Trigger("url"), + Attribute("hx-match-qp", "true"), + Attribute("hx-match-qp-default", defaultValue), + ) + + for s, node := range rendered { + root = AppendChildren(root, Attribute("hx-match-qp-mapping:"+s, ``+html.EscapeString(node)+``)) + } + + return root +} + func AfterRequestSetHtml(children ...*Node) *Node { serialized := Render(Fragment(children...)) return Attribute("hx-on::after-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`) } +func Children(children []*Node) *Node { + return &Node{ + tag: FlagChildrenList, + children: children, + } +} + func If(condition bool, node *Node) *Node { if condition { return node @@ -340,19 +382,6 @@ func If(condition bool, node *Node) *Node { } } -func JsIf(condition string, node *Node) *Node { - node1 := &Node{tag: "template"} - node1.AppendChild(Attribute("x-if", condition)) - node1.AppendChild(node) - return node -} - -func JsIfElse(condition string, node *Node, node2 *Node) *Node { - node1Template := Div(Attribute("x-show", condition), node) - node2Template := Div(Attribute("x-show", "!("+condition+")"), node2) - return Fragment(node1Template, node2Template) -} - func IfElse(condition bool, node *Node, node2 *Node) *Node { if condition { return node @@ -360,3 +389,70 @@ func IfElse(condition bool, node *Node, node2 *Node) *Node { return node2 } } + +func IfElseLazy(condition bool, cb1 func() *Node, cb2 func() *Node) *Node { + if condition { + return cb1() + } else { + return cb2() + } +} + +func IfHtmxRequest(ctx *fiber.Ctx, node *Node) *Node { + if ctx.Get("HX-Request") != "" { + return node + } + return Empty() +} + +type SwapArg struct { + Selector string + Content *Node +} + +func NewSwap(selector string, content *Node) SwapArg { + return SwapArg{ + Selector: selector, + Content: content, + } +} + +func Swap(ctx *fiber.Ctx, content *Node) *Node { + return SwapWithSelector(ctx, "", content) +} + +func SwapWithSelector(ctx *fiber.Ctx, selector string, content *Node) *Node { + if ctx == nil || ctx.Get("HX-Request") == "" { + return Empty() + } + return content.AppendChild(OutOfBandSwap(selector)) +} + +func SwapMany(ctx *fiber.Ctx, args ...SwapArg) *Node { + if ctx.Get("HX-Request") == "" { + return Empty() + } + for _, arg := range args { + arg.Content.AppendChild(OutOfBandSwap(arg.Selector)) + } + return Fragment(Map(args, func(arg SwapArg) *Node { + return arg.Content + })...) +} + +type OnRequestSwapArgs struct { + Target string + Get string + Default *Node + BeforeRequest *Node + AfterRequest *Node +} + +func OnRequestSwap(args OnRequestSwapArgs) *Node { + return Div(args.Default, + BeforeRequestSetHtml(args.BeforeRequest), + AfterRequestSetHtml(args.AfterRequest), + Get(args.Get), + Target(args.Target), + ) +} diff --git a/h/util.go b/h/util.go new file mode 100644 index 0000000..d4d7644 --- /dev/null +++ b/h/util.go @@ -0,0 +1,44 @@ +package h + +import ( + "encoding/json" + "github.com/gofiber/fiber/v2" + "net/url" +) + +func Ternary[T any](value bool, a T, b T) T { + if value { + return a + } + return b +} + +func Map[T any, U any](items []T, fn func(T) U) []U { + var result []U + for _, item := range items { + result = append(result, fn(item)) + } + return result +} + +func JsonSerialize(data any) string { + serialized, err := json.Marshal(data) + if err != nil { + return "" + } + return string(serialized) +} + +func GetQueryParam(ctx *fiber.Ctx, key string) string { + value := ctx.Query(key) + if value == "" { + current := ctx.Get("Hx-Current-Url") + if current != "" { + u, err := url.Parse(current) + if err == nil { + return u.Query().Get(key) + } + } + } + return value +} diff --git a/js/mhtml.js b/js/mhtml.js index ae03ebc..e492b3f 100644 --- a/js/mhtml.js +++ b/js/mhtml.js @@ -1,10 +1,62 @@ -// Replace 'ws://example.com/socket' with the URL of your WebSocket server - window.onload = function () { - document.querySelectorAll('[m\\:onclick]').forEach(element => { - const value = element.getAttribute('m:onclick') - element.addEventListener('click', () => { - fetch('/click/' + value).catch() - }) + // htmx.logger = function(elt, event, data) { + // if(console) { + // console.log(event); + // } + // } + // onUrlChange(window.location.href); + + + window.history.pushState = new Proxy(window.history.pushState, { + apply: (target, thisArg, argArray) => { + if(argArray.length > 2) { + onUrlChange(window.location.origin + argArray[2]); + } + return target.apply(thisArg, argArray); + }, }); +} + +function onUrlChange(newUrl) { + let url = new URL(newUrl); + + setTimeout(() => { + document.querySelectorAll('[hx-trigger]').forEach(function(element) { + const triggers = element.getAttribute('hx-trigger'); + const split = triggers.split(", "); + console.log(split) + if(split.find(s => s === 'url')) { + htmx.trigger(element, "url"); + } else { + for (let [key, values] of url.searchParams) { + let eventName = "qs:" + key + if (triggers.includes(eventName)) { + htmx.trigger(element, eventName); + break + } + } + } + }); + }, 50) + + document.querySelectorAll('[hx-match-qp]').forEach((el) => { + let hasMatch = false; + for (let name of el.getAttributeNames()) { + if(name.startsWith("hx-match-qp-mapping:")) { + let match = name.replace("hx-match-qp-mapping:", ""); + let value = url.searchParams.get(match); + if(value) { + htmx.swap(el, el.getAttribute(name), {swapStyle: 'innerHTML'}) + hasMatch = true; + break + } + } + } + if(!hasMatch) { + let defaultKey = el.getAttribute("hx-match-qp-default") + if(defaultKey) { + htmx.swap(el, el.getAttribute("hx-match-qp-mapping:" + defaultKey), {swapStyle: 'innerHTML'}) + } + } + }) } \ No newline at end of file diff --git a/main.go b/main.go index f6ffe60..721a243 100644 --- a/main.go +++ b/main.go @@ -3,9 +3,11 @@ package main import ( "github.com/gofiber/fiber/v2" "github.com/google/uuid" + "log" "mhtml/h" "mhtml/pages" "mhtml/partials" + "time" ) func main() { @@ -25,8 +27,25 @@ func main() { return ctx.Next() }) - f.Get("/mhtml/partials.*", func(ctx *fiber.Ctx) error { - return h.PartialView(ctx, partials.GetPartialFromContext(ctx)) + f.Use(func(ctx *fiber.Ctx) error { + if ctx.Path() == "/livereload" { + return ctx.Next() + } + now := time.Now() + err := ctx.Next() + duration := time.Since(now) + ctx.Set("X-Response-Time", duration.String()) + // Log or print the request method, URL, and duration + log.Printf("Request: %s %s took %v", ctx.Method(), ctx.OriginalURL(), duration) + return err + }) + + f.Get("/mhtml/partials*", func(ctx *fiber.Ctx) error { + partial := partials.GetPartialFromContext(ctx) + if partial == nil { + return ctx.SendStatus(404) + } + return h.PartialView(ctx, partial) }) pages.RegisterPages(f) diff --git a/news/views.go b/news/views.go index d91942d..18241be 100644 --- a/news/views.go +++ b/news/views.go @@ -4,6 +4,7 @@ import ( "fmt" "mhtml/database" "mhtml/h" + "time" ) func StoryList() *h.Node { @@ -13,12 +14,14 @@ func StoryList() *h.Node { return p }) + time.Sleep(200 * time.Millisecond) + if len(*posts) == 0 { return h.P("No results found") } return h.Fragment( - h.VStack(h.List(*posts, func(item Post) *h.Node { + h.Div(h.List(*posts, func(item Post) *h.Node { return StoryCard(item) })), ) @@ -26,7 +29,7 @@ func StoryList() *h.Node { func StoryCard(post Post) *h.Node { url := fmt.Sprintf("/news/%d", post.Id) - return h.VStack( + return h.Div( h.Class("items-center bg-indigo-200 p-4 rounded"), h.A(post.Title, h.Href(url)), ) diff --git a/pages/base/root.go b/pages/base/root.go index 03d7880..6e2f822 100644 --- a/pages/base/root.go +++ b/pages/base/root.go @@ -2,19 +2,22 @@ package base import ( "mhtml/h" + "mhtml/partials" + "mhtml/partials/sheet" ) func RootPage(children ...*h.Node) *h.Node { return h.Html( h.Head( h.Script("https://cdn.tailwindcss.com"), - h.Script("https://unpkg.com/htmx.org@1.9.12"), - h.Script("https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"), + h.Script("https://unpkg.com/htmx.org@2.0.2"), h.Script("/js/mhtml.js"), ), h.Body( - h.VStack( - h.Class("flex flex-col gap-2 bg-gray-100 h-full"), + partials.NavBar(), + sheet.Closed(), + h.Div( + h.Class("flex flex-col gap-2 bg-white h-full"), h.Fragment(children...), ), ), diff --git a/pages/generated.go b/pages/generated.go index 06b072d..f7c56d7 100644 --- a/pages/generated.go +++ b/pages/generated.go @@ -14,4 +14,7 @@ func RegisterPages(f *fiber.App) { f.Get("/news", func(ctx *fiber.Ctx) error { return h.HtmlView(ctx, ListPage(ctx)) }) + f.Get("/patients", func(ctx *fiber.Ctx) error { + return h.HtmlView(ctx, PatientsIndex(ctx)) + }) } diff --git a/pages/index.go b/pages/index.go index ee7b032..557d6af 100644 --- a/pages/index.go +++ b/pages/index.go @@ -7,14 +7,5 @@ import ( ) func IndexPage(c *fiber.Ctx) *h.Page { - return h.NewPage(base.RootPage( - h.Fragment( - h.Div( - h.Class("inline-flex flex-col gap-4 p-4"), - h.Div( - h.Class("max-w-md flex flex-col gap-4"), - h.P("Routes"), - ), - )), - )) + return h.NewPage(base.RootPage(h.P("this is cool"))) } diff --git a/pages/news.index.go b/pages/news.index.go index eae9ada..23e3a2b 100644 --- a/pages/news.index.go +++ b/pages/news.index.go @@ -5,38 +5,25 @@ import ( "mhtml/h" "mhtml/pages/base" "mhtml/partials" - "mhtml/ui" ) func ListPage(ctx *fiber.Ctx) *h.Page { return h.NewPage(base.RootPage( - list(), + list(ctx), )) } -func list() *h.Node { +func list(ctx *fiber.Ctx) *h.Node { return h.Fragment( - partials.SheetClosed(), + h.ViewOnLoad(partials.NewsSheet), h.Div( h.Class("inline-flex flex-col gap-4 p-4"), h.Div( h.Class("max-w-md flex flex-col gap-4 "), - openButton(), + partials.OpenSheetButton(h.GetQueryParam(ctx, "open") == "true"), ), h.Div( - h.View(partials.SheetOpenCount, h.ReloadParams{ - Triggers: h.CreateTriggers("load", "sheetOpened from:body"), - }), + h.ViewOnLoad(partials.NewsSheetOpenCount), h.Text("you opened sheet 0 times")), )) } - -func openButton() *h.Node { - return h.VStack( - ui.PrimaryButton(ui.ButtonProps{ - Text: "Open Sheet", - Target: "#sheet-partial", - Get: h.GetPartialPathWithQs(partials.Sheet, "open=true"), - }), - ) -} diff --git a/pages/patients.index.go b/pages/patients.index.go new file mode 100644 index 0000000..963c84b --- /dev/null +++ b/pages/patients.index.go @@ -0,0 +1,21 @@ +package pages + +import ( + "github.com/gofiber/fiber/v2" + "mhtml/h" + "mhtml/pages/base" + "mhtml/partials" +) + +func PatientsIndex(ctx *fiber.Ctx) *h.Page { + return h.NewPage(base.RootPage( + h.Div( + h.Class("flex flex-col p-4"), + h.Div( + h.Class("flex justify-between items-center"), + h.P("Manage Patients", h.Class("text-lg font-bold")), + partials.AddPatientButton()), + h.ViewWithTriggers(partials.PatientList, "load", "every 3s"), + ), + )) +} diff --git a/partials/button.go b/partials/button.go new file mode 100644 index 0000000..24796a7 --- /dev/null +++ b/partials/button.go @@ -0,0 +1,26 @@ +package partials + +import ( + "mhtml/h" + "mhtml/ui" +) + +func OpenSheetButton(open bool, children ...*h.Node) *h.Node { + if open { + return ui.PrimaryButton(ui.ButtonProps{ + Id: "open-sheet", + Text: "Close NewsSheet", + Target: "#sheet-partial", + Get: h.GetPartialPathWithQs(NewsSheet, "open=false"), + Children: children, + }) + } else { + return ui.PrimaryButton(ui.ButtonProps{ + Id: "open-sheet", + Text: "Open NewsSheet", + Target: "#sheet-partial", + Get: h.GetPartialPathWithQs(NewsSheet, "open=true"), + Children: children, + }) + } +} diff --git a/partials/generated.go b/partials/generated.go index f273aee..4cd0d06 100644 --- a/partials/generated.go +++ b/partials/generated.go @@ -3,14 +3,24 @@ package partials import "mhtml/h" import "github.com/gofiber/fiber/v2" +import "mhtml/partials/sheet" func GetPartialFromContext(ctx *fiber.Ctx) *h.Partial { path := ctx.Path() - if path == "SheetOpenCount" || path == "/mhtml/partials.SheetOpenCount" { - return SheetOpenCount(ctx) + if path == "NewsSheet" || path == "/mhtml/partials.NewsSheet" { + return NewsSheet(ctx) } - if path == "Sheet" || path == "/mhtml/partials.Sheet" { - return Sheet(ctx) + if path == "NewsSheetOpenCount" || path == "/mhtml/partials.NewsSheetOpenCount" { + return NewsSheetOpenCount(ctx) + } + if path == "PatientList" || path == "/mhtml/partials.PatientList" { + return PatientList(ctx) + } + if path == "AddPatientForm" || path == "/mhtml/partials.AddPatientForm" { + return AddPatientForm(ctx) + } + if path == "Close" || path == "/mhtml/partials/sheet.Close" { + return sheet.Close(ctx) } return nil } diff --git a/partials/nav.go b/partials/nav.go new file mode 100644 index 0000000..c08dfd5 --- /dev/null +++ b/partials/nav.go @@ -0,0 +1,22 @@ +package partials + +import "mhtml/h" + +type Link struct { + Name string + Path string +} + +func NavBar() *h.Node { + + links := []Link{ + {"Home", "/"}, + {"News", "/news"}, + {"Patients", "/patients"}, + } + + return h.Nav(h.Class("flex gap-4 items-center p-4 text-slate-600"), h.Children(h.Map(links, func(link Link) *h.Node { + return h.A(link.Name, h.Href(link.Path), h.Class("cursor-pointer hover:text-blue-400")) + }), + )) +} diff --git a/partials/news.go b/partials/news.go new file mode 100644 index 0000000..6617613 --- /dev/null +++ b/partials/news.go @@ -0,0 +1,72 @@ +package partials + +import ( + "fmt" + "github.com/gofiber/fiber/v2" + "mhtml/h" + "mhtml/news" + "mhtml/ui" +) + +func NewsSheet(ctx *fiber.Ctx) *h.Partial { + open := h.GetQueryParam(ctx, "open") == "true" + if open { + h.SessionIncr(ctx, "sheet-open-count") + } + return h.NewPartialWithHeaders( + &map[string]string{ + "hx-trigger": "sheetOpened", + "hx-push-url": fmt.Sprintf("/news%s", h.Ternary(open, "?open=true", "")), + }, + SheetWrapper( + h.IfElseLazy(open, SheetOpen, SheetClosed), + h.Swap(ctx, OpenSheetButton(open)), + h.Swap(ctx, NewsSheetOpenCount(ctx).Root), + ), + ) +} + +func NewsSheetOpenCount(ctx *fiber.Ctx) *h.Partial { + rnd := h.SessionGet[int64](ctx, "sheet-open-count") + if rnd == nil { + rnd = new(int64) + } + + open := h.GetQueryParam(ctx, "open") == "true" + + return h.NewPartial(h.Div( + h.Id("sheet-open-count"), + h.IfElse(open, + h.Text(fmt.Sprintf("you opened sheet %d times", *rnd)), + h.Text("sheet is not open")), + ), + ) +} + +func SheetWrapper(children ...*h.Node) *h.Node { + return h.Div(h.Id("sheet-partial"), h.Fragment(children...)) +} + +func SheetClosed() *h.Node { + return h.Div() +} + +func SheetOpen() *h.Node { + return h.Fragment(h.Div( + h.Class(`fixed top-0 right-0 h-full w-96 bg-gray-100 shadow-lg z-50`), + h.Div( + h.Class("p-4 overflow-y-auto h-full w-full flex flex-col gap-4"), + h.P("My NewsSheet", + h.Class("text-lg font-bold"), + ), + h.P("This is a sheet", + h.Class("text-sm mt-2"), + ), + ui.Button(ui.ButtonProps{ + Text: "Close NewsSheet", + Target: "#sheet-partial", + Get: h.GetPartialPathWithQs(NewsSheet, "open=false"), + }), + news.StoryList(), + ))) +} diff --git a/partials/patient.go b/partials/patient.go new file mode 100644 index 0000000..8d23901 --- /dev/null +++ b/partials/patient.go @@ -0,0 +1,65 @@ +package partials + +import ( + "github.com/gofiber/fiber/v2" + "mhtml/database" + "mhtml/h" + "mhtml/partials/sheet" + "mhtml/ui" + "time" +) + +type Patient struct { + Name string + ReasonForVisit string + AppointmentDate time.Time + LocationName string +} + +func PatientList(ctx *fiber.Ctx) *h.Partial { + patients, err := database.HList[Patient]("patients") + + if err != nil { + return h.NewPartial(h.Div( + h.Class("patient-list"), + h.P("Error loading patients"), + )) + } + + if len(patients) == 0 { + return h.NewPartial(h.Div( + h.Class("patient-list"), + h.P("No patients found"), + )) + } + + return h.NewPartial(h.Div( + h.Id("patient-list"), + h.List(patients, PatientRow), + )) +} + +func AddPatientForm(ctx *fiber.Ctx) *h.Partial { + return h.NewPartial(sheet.Opened(h.Div( + h.Class("flex flex-col gap-4"), + h.P("Add Patient", h.Class("text-lg font-bold")), + ))) +} + +func PatientRow(patient *Patient) *h.Node { + return h.Div( + h.Class("flex flex-col gap-2"), + h.Pf("Name: %s", patient.Name), + h.Pf("Reason for visit: %s", patient.ReasonForVisit), + ) +} + +func AddPatientButton() *h.Node { + return ui.Button(ui.ButtonProps{ + Id: "add-patient", + Text: "Add Patient", + Class: "bg-blue-700 text-white rounded p-2 h-12", + Target: "#active-modal", + Get: h.GetPartialPath(AddPatientForm), + }) +} diff --git a/partials/sheet.go b/partials/sheet.go deleted file mode 100644 index 8e8162d..0000000 --- a/partials/sheet.go +++ /dev/null @@ -1,56 +0,0 @@ -package partials - -import ( - "fmt" - "github.com/gofiber/fiber/v2" - "mhtml/h" - "mhtml/news" - "mhtml/ui" -) - -func SheetOpenCount(ctx *fiber.Ctx) *h.Partial { - rnd := h.SessionGet[int64](ctx, "sheet-open-count") - if rnd == nil { - rnd = new(int64) - } - return h.NewPartial(h.Div( - h.Text(fmt.Sprintf("you opened sheet %d times", *rnd)), - )) -} - -func SheetClosed() *h.Node { - return h.Div(h.Id("sheet-partial")) -} - -func Sheet(ctx *fiber.Ctx) *h.Partial { - open := ctx.Query("open") - if open == "true" { - h.SessionIncr(ctx, "sheet-open-count") - } - return h.NewPartialWithHeaders( - &map[string]string{ - "hx-trigger": "sheetOpened", - }, - h.IfElse(open == "true", SheetOpen(), SheetClosed()), - ) -} - -func SheetOpen() *h.Node { - return h.Div( - h.Class(`fixed top-0 right-0 h-full w-96 bg-gray-100 shadow-lg z-50`), - h.Div( - h.Class("p-4 overflow-y-auto h-full w-full flex flex-col gap-4"), - h.P("My Sheet", - h.Class("text-lg font-bold"), - ), - h.P("This is a sheet", - h.Class("text-sm mt-2"), - ), - ui.Button(ui.ButtonProps{ - Text: "Close Sheet", - Target: "#sheet-partial", - Get: h.GetPartialPathWithQs(Sheet, "open=false"), - }), - news.StoryList(), - )) -} diff --git a/partials/sheet/sheet.go b/partials/sheet/sheet.go new file mode 100644 index 0000000..a459433 --- /dev/null +++ b/partials/sheet/sheet.go @@ -0,0 +1,36 @@ +package sheet + +import ( + "github.com/gofiber/fiber/v2" + "mhtml/h" +) + +func Opened(children ...*h.Node) *h.Node { + return h.Fragment(h.Div( + h.Class(`fixed top-0 right-0 h-full w-96 bg-gray-100 shadow-lg z-50`), + CloseButton(), + h.Div( + children..., + ))) +} + +func Closed() *h.Node { + return h.Div(h.Id("active-modal")) +} + +func Close(ctx *fiber.Ctx) *h.Partial { + return h.NewPartial( + h.Swap(ctx, Closed()), + ) +} + +func CloseButton() *h.Node { + return h.Div( + h.Class("absolute top-0 right-0 p-3"), + h.Button( + h.Class("text-gray-500"), + h.GetPartial(Close), + h.Text("X"), + ), + ) +} diff --git a/tooling/astgen/entry.go b/tooling/astgen/entry.go index ac3fac8..3aefc18 100644 --- a/tooling/astgen/entry.go +++ b/tooling/astgen/entry.go @@ -17,8 +17,45 @@ type Page struct { Import string } -func findPublicFuncsReturningHPartial(dir string) ([]string, error) { - var functions []string +type Partial struct { + FuncName string + Package string + Import string +} + +func sliceCommonPrefix(dir1, dir2 string) string { + // Use filepath.Clean to normalize the paths + dir1 = filepath.Clean(dir1) + dir2 = filepath.Clean(dir2) + + // Find the common prefix + commonPrefix := dir1 + if len(dir1) > len(dir2) { + commonPrefix = dir2 + } + + for !strings.HasPrefix(dir1, commonPrefix) { + commonPrefix = filepath.Dir(commonPrefix) + } + + // Slice off the common prefix + slicedDir1 := strings.TrimPrefix(dir1, commonPrefix) + slicedDir2 := strings.TrimPrefix(dir2, commonPrefix) + + // Remove leading slashes + slicedDir1 = strings.TrimPrefix(slicedDir1, string(filepath.Separator)) + slicedDir2 = strings.TrimPrefix(slicedDir2, string(filepath.Separator)) + + // Return the longer one + if len(slicedDir1) > len(slicedDir2) { + return slicedDir1 + } + return slicedDir2 +} + +func findPublicFuncsReturningHPartial(dir string) ([]Partial, error) { + var partials []Partial + cwd, _ := os.Getwd() // Walk through the directory to find all Go files. err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { @@ -42,7 +79,7 @@ func findPublicFuncsReturningHPartial(dir string) ([]string, error) { ast.Inspect(node, func(n ast.Node) bool { // Check if the node is a function declaration. if funcDecl, ok := n.(*ast.FuncDecl); ok { - // Only consider exported (public) functions. + // Only consider exported (public) partials. if funcDecl.Name.IsExported() { // Check the return type. if funcDecl.Type.Results != nil { @@ -53,7 +90,11 @@ func findPublicFuncsReturningHPartial(dir string) ([]string, error) { // Check if the package name is 'h' and type is 'Partial'. if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" { if selectorExpr.Sel.Name == "Partial" { - functions = append(functions, funcDecl.Name.Name) + partials = append(partials, Partial{ + Package: node.Name.Name, + Import: sliceCommonPrefix(cwd, filepath.Dir(path)), + FuncName: funcDecl.Name.Name, + }) break } } @@ -73,7 +114,7 @@ func findPublicFuncsReturningHPartial(dir string) ([]string, error) { return nil, err } - return functions, nil + return partials, nil } func findPublicFuncsReturningHPage(dir string) ([]Page, error) { @@ -114,7 +155,7 @@ func findPublicFuncsReturningHPage(dir string) ([]Page, error) { if selectorExpr.Sel.Name == "Page" { pages = append(pages, Page{ Package: node.Name.Name, - Import: fmt.Sprintf("mhtml/%s", filepath.Dir(path)), + Import: filepath.Dir(path), Path: path, FuncName: funcDecl.Name.Name, }) @@ -140,22 +181,30 @@ func findPublicFuncsReturningHPage(dir string) ([]Page, error) { return pages, nil } -func buildGetPartialFromContext(builder *CodeBuilder, funcs []string) { +func buildGetPartialFromContext(builder *CodeBuilder, partials []Partial) { fName := "GetPartialFromContext" body := ` path := ctx.Path() ` - for _, f := range funcs { - if f == fName { + for _, f := range partials { + if f.FuncName == fName { continue } + caller := fmt.Sprintf("%s.%s", f.Package, f.FuncName) + path := fmt.Sprintf("/mhtml/%s.%s", f.Import, f.FuncName) + + if f.Package == "partials" { + caller = f.FuncName + path = fmt.Sprintf("/mhtml/partials.%s", f.FuncName) + } + body += fmt.Sprintf(` - if path == "%s" || path == "/mhtml/partials.%s" { + if path == "%s" || path == "%s" { return %s(ctx) } - `, f, f, f) + `, f.FuncName, path, caller) } body += "return nil" @@ -177,7 +226,7 @@ func buildGetPartialFromContext(builder *CodeBuilder, funcs []string) { func writePartialsFile() { cwd, _ := os.Getwd() partialPath := filepath.Join(cwd, "partials") - funcs, err := findPublicFuncsReturningHPartial(partialPath) + partials, err := findPublicFuncsReturningHPartial(partialPath) if err != nil { fmt.Println(err) return @@ -189,7 +238,13 @@ func writePartialsFile() { builder.AddImport("mhtml/h") builder.AddImport("github.com/gofiber/fiber/v2") - buildGetPartialFromContext(builder, funcs) + for _, partial := range partials { + if partial.Import != "partials" { + builder.AddImport(fmt.Sprintf(`mhtml/%s`, partial.Import)) + } + } + + buildGetPartialFromContext(builder, partials) WriteFile(filepath.Join("partials", "generated.go"), func(content *ast.File) string { return builder.String() diff --git a/tooling/astgen/writer.go b/tooling/astgen/writer.go index b20e076..2e758f2 100644 --- a/tooling/astgen/writer.go +++ b/tooling/astgen/writer.go @@ -43,7 +43,7 @@ func WriteFile(path string, cb func(content *ast.File) string) { } bytes = []byte(cb(f)) - formatEnabled := true + formatEnabled := false if formatEnabled { bytes, err = format.Source(bytes) diff --git a/ui/button.go b/ui/button.go index 80435a4..0342f1f 100644 --- a/ui/button.go +++ b/ui/button.go @@ -5,11 +5,13 @@ import ( ) type ButtonProps struct { + Id string Text string Target string + Trigger string Get string Class string - Children *h.Node + Children []*h.Node } func PrimaryButton(props ButtonProps) *h.Node { @@ -24,22 +26,15 @@ func SecondaryButton(props ButtonProps) *h.Node { func Button(props ButtonProps) *h.Node { - text := h.P(props.Text) + text := h.Text(props.Text) button := h.Button( - h.If(props.Children != nil, props.Children), + h.If(props.Id != "", h.Id(props.Id)), + h.If(props.Children != nil, h.Children(props.Children)), + h.If(props.Trigger != "", h.Trigger(props.Trigger)), h.Class("flex gap-1 items-center border p-4 rounded cursor-hover", props.Class), h.If(props.Get != "", h.Get(props.Get)), h.If(props.Target != "", h.Target(props.Target)), - //h.BeforeRequestSetHtml( - // h.Div( - // h.Class("flex gap-1"), - // h.Text("Loading..."), - // ), - //), - //h.AfterRequestSetHtml(h.Text(props.Text)), - // Note, i really like this idea of being able to reference elements just by the instance, - //and automatically adding id text, )