diff --git a/h/app.go b/h/app.go index c312a22..5e583f1 100644 --- a/h/app.go +++ b/h/app.go @@ -55,6 +55,27 @@ func HtmlView(c *fiber.Ctx, page *Page) error { ) } +func PartialViewWithHeaders(c *fiber.Ctx, headers *Headers, partial *Partial) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) + if partial.Headers != nil { + for s, a := range *partial.Headers { + c.Set(s, a) + } + } + + if headers != nil { + for s, a := range *headers { + c.Set(s, a) + } + } + + return c.SendString( + Render( + partial.Root, + ), + ) +} + func PartialView(c *fiber.Ctx, partial *Partial) error { c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) if partial.Headers != nil { diff --git a/h/tag.go b/h/tag.go index 3d67e9c..f5ac1df 100644 --- a/h/tag.go +++ b/h/tag.go @@ -251,13 +251,13 @@ func Input(inputType string, children ...*Node) *Node { } } -func List[T any](items []T, mapper func(item T) *Node) *Node { +func List[T any](items []T, mapper func(item T, index int) *Node) *Node { node := &Node{ tag: "", children: make([]*Node, len(items)), } for index, value := range items { - node.children[index] = mapper(value) + node.children[index] = mapper(value, index) } return node } @@ -298,6 +298,10 @@ func P(text string, children ...*Node) *Node { } } +func Form(children ...*Node) *Node { + return Tag("form", children...) +} + func A(text string, children ...*Node) *Node { return &Node{ tag: "a", @@ -374,6 +378,10 @@ func Children(children []*Node) *Node { } } +func Label(text string) *Node { + return Tag("label", Text(text)) +} + func If(condition bool, node *Node) *Node { if condition { return node diff --git a/main.go b/main.go index 721a243..badb0c3 100644 --- a/main.go +++ b/main.go @@ -4,9 +4,11 @@ import ( "github.com/gofiber/fiber/v2" "github.com/google/uuid" "log" + "mhtml/database" "mhtml/h" "mhtml/pages" "mhtml/partials" + "mhtml/partials/sheet" "time" ) @@ -50,6 +52,23 @@ func main() { pages.RegisterPages(f) + f.Post("/api/patients", func(ctx *fiber.Ctx) error { + name := ctx.FormValue("name") + reason := ctx.FormValue("reason-for-visit") + location := ctx.FormValue("location-name") + + database.HSet("patients", uuid.New().String(), partials.Patient{ + Name: name, + ReasonForVisit: reason, + AppointmentDate: time.Now(), + LocationName: location, + }) + + return h.PartialViewWithHeaders(ctx, &map[string]string{ + "HX-Trigger": "patient-added", + }, sheet.Close(ctx)) + }) + h.Start(f, h.App{ LiveReload: true, }) diff --git a/news/views.go b/news/views.go index 18241be..7da5836 100644 --- a/news/views.go +++ b/news/views.go @@ -21,7 +21,7 @@ func StoryList() *h.Node { } return h.Fragment( - h.Div(h.List(*posts, func(item Post) *h.Node { + h.Div(h.List(*posts, func(item Post, index int) *h.Node { return StoryCard(item) })), ) diff --git a/pages/patients.index.go b/pages/patients.index.go index 963c84b..0b5fc65 100644 --- a/pages/patients.index.go +++ b/pages/patients.index.go @@ -10,12 +10,15 @@ import ( func PatientsIndex(ctx *fiber.Ctx) *h.Page { return h.NewPage(base.RootPage( h.Div( - h.Class("flex flex-col p-4"), + h.Class("flex flex-col p-4 w-full"), 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"), + 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", "patient-added from:body"), + ), ), )) } diff --git a/partials/generated.go b/partials/generated.go index 4cd0d06..da8c4ba 100644 --- a/partials/generated.go +++ b/partials/generated.go @@ -16,8 +16,8 @@ func GetPartialFromContext(ctx *fiber.Ctx) *h.Partial { if path == "PatientList" || path == "/mhtml/partials.PatientList" { return PatientList(ctx) } - if path == "AddPatientForm" || path == "/mhtml/partials.AddPatientForm" { - return AddPatientForm(ctx) + if path == "AddPatientSheet" || path == "/mhtml/partials.AddPatientSheet" { + return AddPatientSheet(ctx) } if path == "Close" || path == "/mhtml/partials/sheet.Close" { return sheet.Close(ctx) diff --git a/partials/patient.go b/partials/patient.go index 8d23901..0edb503 100644 --- a/partials/patient.go +++ b/partials/patient.go @@ -34,21 +34,59 @@ func PatientList(ctx *fiber.Ctx) *h.Partial { } return h.NewPartial(h.Div( + h.Class("mt-8"), 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 AddPatientSheet(ctx *fiber.Ctx) *h.Partial { + return h.NewPartial(sheet.Opened( + sheet.Props{ + ClassName: "w-[400px] bg-gray-100 p-4", + Root: h.Div( + h.Class("flex flex-col gap-4"), + h.P("Add Patient", h.Class("text-lg font-bold")), + addPatientForm(), + ), + })) } -func PatientRow(patient *Patient) *h.Node { - return h.Div( +func addPatientForm() *h.Node { + return h.Form( + h.Post("/api/patients"), h.Class("flex flex-col gap-2"), + ui.Input(ui.InputProps{ + Type: "text", + Label: "Name", + Name: "name", + }), + ui.Input(ui.InputProps{ + Type: "text", + Label: "Reason for visit", + Name: "reason-for-visit", + }), + ui.Input(ui.InputProps{ + Type: "date", + Label: "Appointment Date", + Name: "appointment-date", + }), + ui.Input(ui.InputProps{ + Type: "text", + Label: "Location Name", + Name: "location-name", + }), + ui.PrimaryButton(ui.ButtonProps{ + Text: "Add Patient", + Class: "rounded p-2", + Type: "submit", + }), + ) +} + +func PatientRow(patient *Patient, index int) *h.Node { + return h.Div( + h.Class("flex flex-col gap-2 rounded p-4", h.Ternary(index%2 == 0, "bg-red-100", "")), h.Pf("Name: %s", patient.Name), h.Pf("Reason for visit: %s", patient.ReasonForVisit), ) @@ -59,7 +97,7 @@ func AddPatientButton() *h.Node { Id: "add-patient", Text: "Add Patient", Class: "bg-blue-700 text-white rounded p-2 h-12", - Target: "#active-modal", - Get: h.GetPartialPath(AddPatientForm), + Target: sheet.Id, + Get: h.GetPartialPath(AddPatientSheet), }) } diff --git a/partials/sheet/sheet.go b/partials/sheet/sheet.go index a459433..3bc5045 100644 --- a/partials/sheet/sheet.go +++ b/partials/sheet/sheet.go @@ -5,17 +5,25 @@ import ( "mhtml/h" ) -func Opened(children ...*h.Node) *h.Node { +type Props struct { + ClassName string + Root *h.Node +} + +var Id = "#active-modal" + +func Opened(props Props) *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.Class(`fixed top-0 right-0 h-full shadow-lg z-50`, + h.Ternary(props.ClassName != "", props.ClassName, "w-96 bg-gray-100")), + closeButton(), h.Div( - children..., + props.Root, ))) } func Closed() *h.Node { - return h.Div(h.Id("active-modal")) + return h.Div(h.Id(Id)) } func Close(ctx *fiber.Ctx) *h.Partial { @@ -24,7 +32,7 @@ func Close(ctx *fiber.Ctx) *h.Partial { ) } -func CloseButton() *h.Node { +func closeButton() *h.Node { return h.Div( h.Class("absolute top-0 right-0 p-3"), h.Button( diff --git a/ui/button.go b/ui/button.go index 0342f1f..b75410c 100644 --- a/ui/button.go +++ b/ui/button.go @@ -8,6 +8,7 @@ type ButtonProps struct { Id string Text string Target string + Type string Trigger string Get string Class string @@ -35,6 +36,7 @@ func Button(props ButtonProps) *h.Node { 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.IfElse(props.Type != "", h.Type(props.Type), h.Type("button")), text, ) diff --git a/ui/input.go b/ui/input.go new file mode 100644 index 0000000..5d4be71 --- /dev/null +++ b/ui/input.go @@ -0,0 +1,29 @@ +package ui + +import "mhtml/h" + +type InputProps struct { + Id string + Label string + Name string + Type string + DefaultValue string +} + +func Input(props InputProps) *h.Node { + input := h.Input( + props.Type, + h.Class("border p-2 rounded"), + h.If(props.Id != "", h.Id(props.Id)), + h.If(props.Name != "", h.Name(props.Name)), + h.If(props.DefaultValue != "", h.Attribute("defaultValue", props.DefaultValue)), + ) + if props.Label != "" { + return h.Div( + h.Class("flex flex-col gap-1"), + h.Label(props.Label), + input, + ) + } + return input +}