lots of stuff

This commit is contained in:
maddalax 2024-09-11 21:06:34 -05:00
parent fec5558f28
commit 0dea110ebc
16 changed files with 177 additions and 57 deletions

View file

@ -19,10 +19,6 @@ func (p *Partial) Render() *Node {
return p.Root return p.Root
} }
func (p *Partial) ToNode() *Node {
return p.Root
}
type Page struct { type Page struct {
Root Renderable Root Renderable
HttpMethod string HttpMethod string
@ -42,10 +38,6 @@ func NewPageWithHttpMethod(httpMethod string, root Renderable) *Page {
} }
} }
func WrapPartial(ctx *fiber.Ctx, cb func(ctx *fiber.Ctx) *Partial) *Node {
return cb(ctx).Root
}
func NewPartialWithHeaders(headers *Headers, root Renderable) *Partial { func NewPartialWithHeaders(headers *Headers, root Renderable) *Partial {
return &Partial{ return &Partial{
Headers: headers, Headers: headers,

View file

@ -25,7 +25,7 @@ func LiveReloadHandler(c *fiber.Ctx) error {
} }
func LiveReload() Renderable { func LiveReload() Renderable {
return Div(Get("/livereload"), Trigger("every 200ms")) return Div(Get("/livereload"), Trigger("every 2s"))
} }
func AddLiveReloadHandler(path string, app *fiber.App) { func AddLiveReloadHandler(path string, app *fiber.App) {

View file

@ -57,11 +57,12 @@ func (page Builder) renderNode(node *Node) {
for _, child := range node.children { for _, child := range node.children {
c := child.Render()
if child == nil { if child == nil {
continue continue
} }
c := child.Render()
if c.tag == "class" { if c.tag == "class" {
insertAttribute(node, "class", c.value) insertAttribute(node, "class", c.value)
c.tag = FlagSkip c.tag = FlagSkip
@ -140,6 +141,6 @@ func Render(node Renderable) string {
page.render() page.render()
d := page.builder.String() d := page.builder.String()
duration := time.Since(start) duration := time.Since(start)
fmt.Printf("render took %s\n", duration) fmt.Printf("render took %d\n", duration.Microseconds())
return d return d
} }

View file

@ -81,6 +81,10 @@ func Attribute(key string, value string) Renderable {
return Attributes(map[string]string{key: value}) return Attributes(map[string]string{key: value})
} }
func TriggerChildren() Renderable {
return Attribute("hx-trigger-children", "")
}
func Disabled() Renderable { func Disabled() Renderable {
return Attribute("disabled", "") return Attribute("disabled", "")
} }
@ -103,6 +107,7 @@ func CreateTriggers(triggers ...string) []string {
type ReloadParams struct { type ReloadParams struct {
Triggers []string Triggers []string
Target string
} }
func ViewOnLoad(partial func(ctx *fiber.Ctx) *Partial) Renderable { func ViewOnLoad(partial func(ctx *fiber.Ctx) *Partial) Renderable {
@ -115,10 +120,11 @@ func View(partial func(ctx *fiber.Ctx) *Partial, params ReloadParams) Renderable
return Div(Attributes(map[string]string{ return Div(Attributes(map[string]string{
"hx-get": GetPartialPath(partial), "hx-get": GetPartialPath(partial),
"hx-trigger": strings.Join(params.Triggers, ", "), "hx-trigger": strings.Join(params.Triggers, ", "),
"hx-target": params.Target,
})) }))
} }
func ViewWithTriggers(partial func(ctx *fiber.Ctx) *Partial, triggers ...string) Renderable { func PartialWithTriggers(partial func(ctx *fiber.Ctx) *Partial, triggers ...string) Renderable {
return Div(Attributes(map[string]string{ return Div(Attributes(map[string]string{
"hx-get": GetPartialPath(partial), "hx-get": GetPartialPath(partial),
"hx-trigger": strings.Join(triggers, ", "), "hx-trigger": strings.Join(triggers, ", "),
@ -126,20 +132,7 @@ func ViewWithTriggers(partial func(ctx *fiber.Ctx) *Partial, triggers ...string)
} }
func GetWithQs(path string, qs map[string]string) Renderable { func GetWithQs(path string, qs map[string]string) Renderable {
u, err := url.Parse(path) return Get(SetQueryParams(path, qs))
if err != nil {
return Empty()
}
q := u.Query()
for s := range qs {
q.Add(s, qs[s])
}
u.RawQuery = q.Encode()
return Get(u.String())
} }
func Post(url string) Renderable { func Post(url string) Renderable {
@ -239,6 +232,49 @@ func Div(children ...Renderable) Renderable {
return Tag("div", children...) return Tag("div", children...)
} }
func PushUrlHeader(url string) *Headers {
return NewHeaders("HX-Push-Url", url)
}
func CombineHeaders(headers ...*Headers) *Headers {
m := make(Headers)
for _, h := range headers {
for k, v := range *h {
m[k] = v
}
}
return &m
}
func CurrentPath(ctx *fiber.Ctx) string {
current := ctx.Get("Hx-Current-Url")
parsed, err := url.Parse(current)
if err != nil {
return ""
}
return parsed.Path
}
func PushQsHeader(ctx *fiber.Ctx, key string, value string) *Headers {
current := ctx.Get("Hx-Current-Url")
parsed, err := url.Parse(current)
if err != nil {
return NewHeaders()
}
return NewHeaders("HX-Push-Url", SetQueryParams(parsed.Path, map[string]string{
key: value,
}))
}
func NewHeaders(headers ...string) *Headers {
m := make(Headers)
for i := 0; i < len(headers); i++ {
m[headers[i]] = headers[i+1]
i++
}
return &m
}
func Input(inputType string, children ...Renderable) Renderable { func Input(inputType string, children ...Renderable) Renderable {
return &Node{ return &Node{
tag: "input", tag: "input",
@ -331,6 +367,10 @@ func BeforeRequestSetText(text string) Renderable {
return Attribute("hx-on::before-request", `this.innerText = '`+text+`'`) return Attribute("hx-on::before-request", `this.innerText = '`+text+`'`)
} }
func AfterRequestSetText(text string) Renderable {
return Attribute("hx-on::after-request", `this.innerText = '`+text+`'`)
}
func AfterRequestRemoveAttribute(key string, value string) Renderable { func AfterRequestRemoveAttribute(key string, value string) Renderable {
return Attribute("hx-on::after-request", `this.removeAttribute('`+key+`')`) return Attribute("hx-on::after-request", `this.removeAttribute('`+key+`')`)
} }

View file

@ -46,3 +46,20 @@ func GetQueryParam(ctx *fiber.Ctx, key string) string {
} }
return value return value
} }
func SetQueryParams(href string, qs map[string]string) string {
u, err := url.Parse(href)
if err != nil {
return href
}
q := u.Query()
for key, value := range qs {
if value == "" {
q.Del(key)
} else {
q.Set(key, value)
}
}
u.RawQuery = q.Encode()
return u.String()
}

View file

@ -1,12 +1,30 @@
window.onload = function () { window.onload = function () {
// htmx.logger = function(elt, event, data) { // htmx.logger = function(elt, event, data) {
// if(console) { // if(console) {
// console.log(event); // console.log(elt, event, data);
// } // }
// } // }
// onUrlChange(window.location.href); // onUrlChange(window.location.href);
function triggerChildren(event) {
const target = event.detail.target
const type = event.type
if(target && target.children && target.hasAttribute('hx-trigger-children')) {
Array.from(target.children).forEach(function(element) {
htmx.trigger(element, type);
});
}
}
const events = ['htmx:beforeRequest', 'htmx:afterRequest', 'htmx:responseError', 'htmx:sendError',
'htmx:timeout', 'htmx:xhr:abort',
'htmx:xhr:loadstart', 'htmx:xhr:loadend', 'htmx:xhr:progress']
events.forEach(function(event) {
document.addEventListener(event, triggerChildren)
})
window.history.pushState = new Proxy(window.history.pushState, { window.history.pushState = new Proxy(window.history.pushState, {
apply: (target, thisArg, argArray) => { apply: (target, thisArg, argArray) => {
if(argArray.length > 2) { if(argArray.length > 2) {

18
k6.js Normal file
View file

@ -0,0 +1,18 @@
import http from 'k6/http';
import { sleep } from 'k6';
export let options = {
stages: [
{ duration: '1m', target: 100 }, // Ramp-up to 100 RPS over 1 minute
{ duration: '10m', target: 100 }, // Stay at 100 RPS for 10 minutes
{ duration: '1m', target: 0 }, // Ramp-down to 0 RPS
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests should be below 500ms
},
};
export default function () {
http.get('http://localhost:3000/patients');
sleep(1 / 100); // Make 100 requests per second
}

12
main.go
View file

@ -36,19 +36,13 @@ func main() {
duration := time.Since(now) duration := time.Since(now)
ctx.Set("X-Response-Time", duration.String()) ctx.Set("X-Response-Time", duration.String())
// Log or print the request method, URL, and duration // Log or print the request method, URL, and duration
log.Printf("Request: %s %s took %v", ctx.Method(), ctx.OriginalURL(), duration) log.Printf("Request: %s %s took %dms", ctx.Method(), ctx.OriginalURL(), duration.Milliseconds())
return err return err
}) })
f.All("/mhtml/partials*", func(ctx *fiber.Ctx) error { partials.RegisterPartials(f)
partial := partials.GetPartialFromContext(ctx)
if partial == nil {
return ctx.SendStatus(404)
}
return h.PartialView(ctx, partial)
})
pages.RegisterPages(f) pages.RegisterPages(f)
h.Start(f, h.App{ h.Start(f, h.App{
LiveReload: true, LiveReload: true,
}) })

View file

@ -5,6 +5,7 @@ import (
"mhtml/h" "mhtml/h"
"mhtml/pages/base" "mhtml/pages/base"
"mhtml/partials/patient" "mhtml/partials/patient"
"mhtml/partials/sheet"
) )
func PatientsIndex(ctx *fiber.Ctx) *h.Page { func PatientsIndex(ctx *fiber.Ctx) *h.Page {
@ -17,7 +18,13 @@ func PatientsIndex(ctx *fiber.Ctx) *h.Page {
h.P("Manage Patients", h.Class("text-lg font-bold")), h.P("Manage Patients", h.Class("text-lg font-bold")),
patient.AddPatientButton(), patient.AddPatientButton(),
), ),
h.ViewWithTriggers(patient.List, "load", "patient-added from:body"), h.PartialWithTriggers(patient.List, "load", "patient-added from:body", "every 5s"),
h.If(
h.GetQueryParam(ctx, "adding") == "true",
h.View(patient.AddPatientSheetPartial, h.ReloadParams{
Triggers: h.CreateTriggers("load"),
Target: sheet.Id,
})),
), ),
), ),
)) ))

View file

@ -1 +1,16 @@
package partials package partials
import (
"github.com/gofiber/fiber/v2"
"mhtml/h"
)
func RegisterPartials(f *fiber.App) {
f.All("/mhtml/partials*", func(ctx *fiber.Ctx) error {
partial := GetPartialFromContext(ctx)
if partial == nil {
return ctx.SendStatus(404)
}
return h.PartialView(ctx, partial)
})
}

View file

@ -20,8 +20,8 @@ func GetPartialFromContext(ctx *fiber.Ctx) *h.Partial {
if path == "List" || path == "/mhtml/partials/patient.List" { if path == "List" || path == "/mhtml/partials/patient.List" {
return patient.List(ctx) return patient.List(ctx)
} }
if path == "AddPatientSheet" || path == "/mhtml/partials/patient.AddPatientSheet" { if path == "AddPatientSheetPartial" || path == "/mhtml/partials/patient.AddPatientSheetPartial" {
return patient.AddPatientSheet(ctx) return patient.AddPatientSheetPartial(ctx)
} }
if path == "Close" || path == "/mhtml/partials/sheet.Close" { if path == "Close" || path == "/mhtml/partials/sheet.Close" {
return sheet.Close(ctx) return sheet.Close(ctx)

View file

@ -21,9 +21,11 @@ func Create(ctx *fiber.Ctx) *h.Partial {
LocationName: location, LocationName: location,
}) })
headers := &map[string]string{ headers := h.CombineHeaders(h.PushQsHeader(ctx, "adding", ""), &map[string]string{
"HX-Trigger": "patient-added", "HX-Trigger": "patient-added",
} })
return h.NewPartialWithHeaders(headers, h.WrapPartial(ctx, sheet.Close)) return h.NewPartialWithHeaders(
headers,
sheet.Close(ctx))
} }

View file

@ -40,26 +40,36 @@ func List(ctx *fiber.Ctx) *h.Partial {
)) ))
} }
func AddPatientSheet(ctx *fiber.Ctx) *h.Partial { func AddPatientSheetPartial(ctx *fiber.Ctx) *h.Partial {
return h.NewPartial(sheet.Opened( return h.NewPartialWithHeaders(
h.PushQsHeader(ctx, "adding", "true"),
AddPatientSheet(h.CurrentPath(ctx)),
)
}
func AddPatientSheet(onClosePath string) h.Renderable {
return sheet.Opened(
sheet.Props{ sheet.Props{
ClassName: "w-[400px] bg-gray-100 p-4", OnClosePath: onClosePath,
ClassName: "w-[400px] bg-gray-100 p-4",
Root: h.Div( Root: h.Div(
h.Class("flex flex-col gap-4"), h.Class("flex flex-col gap-4"),
h.P("Add Patient", h.Class("text-lg font-bold")), h.P("Add Patient", h.Class("text-lg font-bold")),
addPatientForm(), addPatientForm(),
), ),
})) })
} }
func addPatientForm() h.Renderable { func addPatientForm() h.Renderable {
return h.Form( return h.Form(
h.TriggerChildren(),
h.Post(h.GetPartialPath(Create)), h.Post(h.GetPartialPath(Create)),
h.Class("flex flex-col gap-2"), h.Class("flex flex-col gap-2"),
ui.Input(ui.InputProps{ ui.Input(ui.InputProps{
Type: "text", Type: "text",
Label: "Name", Label: "Name",
Name: "name", Name: "name",
DefaultValue: "fart",
}), }),
ui.Input(ui.InputProps{ ui.Input(ui.InputProps{
Type: "text", Type: "text",
@ -98,6 +108,6 @@ func AddPatientButton() h.Renderable {
Text: "Add Patient", Text: "Add Patient",
Class: "bg-blue-700 text-white rounded p-2 h-12", Class: "bg-blue-700 text-white rounded p-2 h-12",
Target: sheet.Id, Target: sheet.Id,
Get: h.GetPartialPath(AddPatientSheet), Get: h.GetPartialPath(AddPatientSheetPartial),
}) })
} }

View file

@ -1,13 +1,15 @@
package sheet package sheet
import ( import (
"fmt"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"mhtml/h" "mhtml/h"
) )
type Props struct { type Props struct {
ClassName string ClassName string
Root h.Renderable Root h.Renderable
OnClosePath string
} }
var Id = "#active-modal" var Id = "#active-modal"
@ -16,7 +18,7 @@ func Opened(props Props) h.Renderable {
return h.Fragment(h.Div( return h.Fragment(h.Div(
h.Class(`fixed top-0 right-0 h-full shadow-lg z-50`, h.Class(`fixed top-0 right-0 h-full shadow-lg z-50`,
h.Ternary(props.ClassName != "", props.ClassName, "w-96 bg-gray-100")), h.Ternary(props.ClassName != "", props.ClassName, "w-96 bg-gray-100")),
closeButton(), closeButton(props),
h.Div( h.Div(
props.Root, props.Root,
))) )))
@ -27,17 +29,18 @@ func Closed() h.Renderable {
} }
func Close(ctx *fiber.Ctx) *h.Partial { func Close(ctx *fiber.Ctx) *h.Partial {
return h.NewPartial( return h.NewPartialWithHeaders(
h.Ternary(ctx.Query("path") != "", h.PushUrlHeader(ctx.Query("path")), h.NewHeaders()),
h.Swap(ctx, Closed()), h.Swap(ctx, Closed()),
) )
} }
func closeButton() h.Renderable { func closeButton(props Props) h.Renderable {
return h.Div( return h.Div(
h.Class("absolute top-0 right-0 p-3"), h.Class("absolute top-0 right-0 p-3"),
h.Button( h.Button(
h.Class("text-gray-500"), h.Class("text-gray-500"),
h.GetPartial(Close), h.GetPartialWithQs(Close, fmt.Sprintf("path=%s", props.OnClosePath)),
h.Text("X"), h.Text("X"),
), ),
) )

View file

@ -36,7 +36,10 @@ func Button(props ButtonProps) h.Renderable {
h.Class("flex gap-1 items-center border p-4 rounded cursor-hover", props.Class), 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.Get != "", h.Get(props.Get)),
h.If(props.Target != "", h.Target(props.Target)), h.If(props.Target != "", h.Target(props.Target)),
//h.Attribute("hx-indicator", "#spinner"),
h.IfElse(props.Type != "", h.Type(props.Type), h.Type("button")), h.IfElse(props.Type != "", h.Type(props.Type), h.Type("button")),
h.BeforeRequestSetText("Loading..."),
h.AfterRequestSetText(props.Text),
text, text,
) )

View file

@ -16,7 +16,7 @@ func Input(props InputProps) h.Renderable {
h.Class("border p-2 rounded"), h.Class("border p-2 rounded"),
h.If(props.Id != "", h.Id(props.Id)), h.If(props.Id != "", h.Id(props.Id)),
h.If(props.Name != "", h.Name(props.Name)), h.If(props.Name != "", h.Name(props.Name)),
h.If(props.DefaultValue != "", h.Attribute("defaultValue", props.DefaultValue)), h.If(props.DefaultValue != "", h.Attribute("value", props.DefaultValue)),
) )
if props.Label != "" { if props.Label != "" {
return h.Div( return h.Div(