htmgo/examples/todo-list/partials/task/task.go

347 lines
8.5 KiB
Go
Raw Normal View History

2024-09-19 23:13:04 +00:00
package task
import (
"fmt"
"github.com/google/uuid"
"github.com/maddalax/htmgo/framework/h"
2024-09-21 17:08:23 +00:00
"github.com/maddalax/htmgo/framework/hx"
2024-09-29 05:45:54 +00:00
"github.com/maddalax/htmgo/framework/js"
2024-09-19 23:13:04 +00:00
"todolist/ent"
"todolist/internal/tasks"
)
type Tab = string
const (
TabAll Tab = "All"
TabActive Tab = "Active"
TabComplete Tab = "Complete"
)
func getActiveTab(ctx *h.RequestContext) Tab {
if tab := h.GetQueryParam(ctx, "tab"); tab != "" {
return tab
}
return TabAll
}
func Card(ctx *h.RequestContext) *h.Element {
2024-09-19 23:13:04 +00:00
service := tasks.NewService(ctx.ServiceLocator())
list, _ := service.List()
return h.Div(
h.Id("task-card"),
h.Class("bg-white w-full rounded shadow-md"),
CardBody(list, getActiveTab(ctx)),
)
}
func CardBody(list []*ent.Task, tab Tab) *h.Element {
2024-09-19 23:13:04 +00:00
return h.Div(
h.Id("tasks-card-body"),
Input(list),
List(list, tab),
Footer(list, tab),
)
}
func Input(list []*ent.Task) *h.Element {
2024-09-19 23:13:04 +00:00
return h.Div(
h.Id("task-card-input"),
h.Class("border border-b-slate-100 relative"),
h.Input(
"text",
2024-09-29 05:45:54 +00:00
h.Attribute("required", "true"),
h.Attribute("maxlength", "150"),
2024-09-19 23:13:04 +00:00
h.Attribute("autocomplete", "off"),
h.Attribute("autofocus", "true"),
h.Attribute("name", "name"),
h.Class("pl-12 text-xl p-4 w-full outline-none focus:outline-2 focus:outline-rose-400"),
h.Placeholder("What needs to be done?"),
h.Post(h.GetPartialPath(Create)),
2024-09-21 17:08:23 +00:00
h.HxTrigger(hx.OnEvent(hx.TriggerKeyUpEnter)),
2024-09-19 23:13:04 +00:00
),
CompleteAllIcon(list),
)
}
func CompleteAllIcon(list []*ent.Task) *h.Element {
2024-09-19 23:13:04 +00:00
notCompletedCount := len(h.Filter(list, func(item *ent.Task) bool {
return item.CompletedAt == nil
}))
2024-09-24 19:55:07 +00:00
return h.Div(
h.ClassX("absolute top-1 left-5 p-2 rotate-90 text-3xl cursor-pointer", map[string]bool{
2024-09-24 19:55:07 +00:00
"text-slate-400": notCompletedCount > 0,
2024-09-29 02:02:57 +00:00
}), h.UnsafeRaw("›"),
2024-09-24 19:55:07 +00:00
h.PostPartialWithQs(CompleteAll, h.NewQs("complete", h.Ternary(notCompletedCount > 0, "true", "false"))),
2024-09-19 23:13:04 +00:00
)
}
func Footer(list []*ent.Task, activeTab Tab) *h.Element {
2024-09-19 23:13:04 +00:00
notCompletedCount := len(h.Filter(list, func(item *ent.Task) bool {
return item.CompletedAt == nil
}))
tabs := []Tab{TabAll, TabActive, TabComplete}
return h.Div(
h.Id("task-card-footer"),
h.Class("flex items-center justify-between p-4 border-t border-b-slate-100"),
h.Div(
h.TextF("%d items left", notCompletedCount),
),
h.Div(
h.Class("flex items-center gap-4"),
h.List(tabs, func(tab Tab, index int) *h.Element {
2024-09-19 23:13:04 +00:00
return h.P(
2024-09-21 17:08:23 +00:00
h.PostOnClick(h.GetPartialPathWithQs(ChangeTab, h.NewQs("tab", tab))),
2024-09-19 23:13:04 +00:00
h.ClassX("cursor-pointer px-2 py-1 rounded", map[string]bool{
"border border-rose-600": activeTab == tab,
}),
h.Text(tab),
)
}),
),
h.Div(
h.PostPartialOnClick(ClearCompleted),
h.ClassX("flex gap-2 cursor-pointer", map[string]bool{
"opacity-0": notCompletedCount == len(list),
}),
h.Text("Clear completed"),
),
)
}
func List(list []*ent.Task, tab Tab) *h.Element {
2024-09-19 23:13:04 +00:00
return h.Div(
h.Id("task-card-list"),
h.Class("bg-white w-full"),
h.Div(
h.List(list, func(item *ent.Task, index int) *h.Element {
2024-09-19 23:13:04 +00:00
if tab == TabActive && item.CompletedAt != nil {
return h.Empty()
}
if tab == TabComplete && item.CompletedAt == nil {
return h.Empty()
}
return Task(item, false)
}),
),
)
}
func Task(task *ent.Task, editing bool) *h.Element {
2024-09-19 23:13:04 +00:00
return h.Div(
h.Id(fmt.Sprintf("task-%s", task.ID.String())),
h.ClassX("h-[80px] max-h-[80px] max-w-2xl flex items-center p-4 gap-4 cursor-pointer", h.ClassMap{
2024-09-19 23:13:04 +00:00
"border border-b-slate-100": !editing,
}),
CompleteIcon(task),
h.IfElse(editing,
h.Div(
h.Class("flex-1 h-full"),
h.Form(
h.Class("h-full"),
h.Input("text",
h.Attribute("name", "task"),
h.Attribute("value", task.ID.String()),
h.Class("hidden"),
),
h.Input(
"text",
2024-09-21 17:08:23 +00:00
h.PostPartial(UpdateName, hx.TriggerBlur, hx.TriggerKeyUpEnter),
h.Attributes(&h.AttributeMap{
2024-09-29 05:45:54 +00:00
"maxLength": "150",
"required": "true",
"placeholder": "What needs to be done?",
"autofocus": "true",
"autocomplete": "off",
"name": "name",
"class": h.ClassX("", h.ClassMap{
"pl-1 h-full w-full text-xl outline-none outline-2 outline-rose-300": true,
}),
"value": task.Name,
}),
2024-09-19 23:13:04 +00:00
),
),
),
h.P(
2024-09-21 17:08:23 +00:00
h.GetPartialWithQs(EditNameForm, h.NewQs("id", task.ID.String()), hx.TriggerDblClick),
2024-09-19 23:13:04 +00:00
h.ClassX("text-xl break-all text-wrap truncate", map[string]bool{
"line-through text-slate-400": task.CompletedAt != nil,
}),
h.Text(task.Name),
)),
)
}
func CompleteIcon(task *ent.Task) *h.Element {
2024-09-19 23:13:04 +00:00
return h.Div(
2024-09-21 17:08:23 +00:00
h.HxTrigger(hx.OnClick()),
h.Post(h.GetPartialPathWithQs(ToggleCompleted, h.NewQs("id", task.ID.String()))),
2024-09-19 23:13:04 +00:00
h.Class("flex items-center justify-center cursor-pointer"),
h.Div(
h.ClassX("w-10 h-10 border rounded-full flex items-center justify-center", map[string]bool{
"border-green-500": task.CompletedAt != nil,
"border-slate-400": task.CompletedAt == nil,
}),
h.If(task.CompletedAt != nil, h.UnsafeRaw(`
2024-09-19 23:13:04 +00:00
<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path>
</svg>
`)),
),
)
}
func UpdateName(ctx *h.RequestContext) *h.Partial {
id, err := uuid.Parse(ctx.FormValue("task"))
if err != nil {
return h.NewPartial(h.Div(h.Text("invalid id")))
}
name := ctx.FormValue("name")
if name == "" {
return h.NewPartial(h.Div(h.Text("name is required")))
}
2024-09-29 05:45:54 +00:00
if len(name) > 150 {
return h.NewPartial(h.Div(h.Text("task must be less than 150 characters")))
}
2024-09-19 23:13:04 +00:00
service := tasks.NewService(ctx.ServiceLocator())
task, err := service.Get(id)
if task == nil {
return h.NewPartial(h.Div(h.Text("task not found")))
}
task, err = service.SetName(task.ID, name)
if err != nil {
return h.NewPartial(h.Div(h.Text("failed to update")))
}
return h.NewPartial(
h.OobSwap(ctx, Task(task, false)))
2024-09-19 23:13:04 +00:00
}
func EditNameForm(ctx *h.RequestContext) *h.Partial {
id, err := uuid.Parse(ctx.QueryParam("id"))
if err != nil {
return h.NewPartial(h.Div(h.Text("invalid id")))
}
service := tasks.NewService(ctx.ServiceLocator())
task, err := service.Get(id)
if task == nil {
return h.NewPartial(h.Div(h.Text("task not found")))
}
return h.NewPartial(
h.OobSwap(ctx, Task(task, true)),
)
}
func ToggleCompleted(ctx *h.RequestContext) *h.Partial {
id, err := uuid.Parse(ctx.QueryParam("id"))
if err != nil {
return h.NewPartial(h.Div(h.Text("invalid id")))
}
service := tasks.NewService(ctx.ServiceLocator())
task, err := service.Get(id)
if task == nil {
return h.NewPartial(h.Div(h.Text("task not found")))
}
task, err = service.SetCompleted(task.ID, h.
Ternary(task.CompletedAt == nil, true, false))
if err != nil {
return h.NewPartial(h.Div(h.Text("failed to update")))
}
list, _ := service.List()
2024-09-21 17:08:23 +00:00
return h.SwapManyPartial(ctx,
List(list, getActiveTab(ctx)),
Footer(list, getActiveTab(ctx)),
CompleteAllIcon(list),
)
2024-09-19 23:13:04 +00:00
}
func CompleteAll(ctx *h.RequestContext) *h.Partial {
service := tasks.NewService(ctx.ServiceLocator())
2024-09-24 19:55:07 +00:00
2024-09-19 23:13:04 +00:00
service.SetAllCompleted(ctx.QueryParam("complete") == "true")
2024-09-24 19:55:07 +00:00
list, _ := service.List()
return h.NewPartial(h.OobSwap(ctx, CardBody(list, getActiveTab(ctx))))
2024-09-19 23:13:04 +00:00
}
func ClearCompleted(ctx *h.RequestContext) *h.Partial {
service := tasks.NewService(ctx.ServiceLocator())
_ = service.ClearCompleted()
list, _ := service.List()
return h.NewPartial(h.OobSwap(ctx, CardBody(list, getActiveTab(ctx))))
}
func Create(ctx *h.RequestContext) *h.Partial {
name := ctx.FormValue("name")
2024-09-29 01:28:35 +00:00
2024-09-29 05:45:54 +00:00
if len(name) > 150 {
return h.NewPartial(
h.Div(
h.HxOnLoad(js.Alert("Task must be less than 150 characters")),
),
)
2024-09-19 23:13:04 +00:00
}
service := tasks.NewService(ctx.ServiceLocator())
2024-09-29 05:45:54 +00:00
list, _ := service.List()
if list != nil && len(list) >= 200 {
return h.NewPartial(
h.Div(
h.HxOnLoad(js.Alert("There are too many tasks, please complete and clear some.")),
),
)
}
2024-09-19 23:13:04 +00:00
_, err := service.Create(tasks.CreateRequest{
Name: name,
})
if err != nil {
return h.NewPartial(h.Div(h.Text("failed to create")))
}
2024-09-29 05:45:54 +00:00
list, err = service.List()
2024-09-19 23:13:04 +00:00
2024-09-21 17:08:23 +00:00
return h.SwapManyPartial(ctx,
CardBody(list, getActiveTab(ctx)),
)
2024-09-19 23:13:04 +00:00
}
func ChangeTab(ctx *h.RequestContext) *h.Partial {
service := tasks.NewService(ctx.ServiceLocator())
list, _ := service.List()
tab := ctx.QueryParam("tab")
2024-09-21 17:08:23 +00:00
return h.SwapManyPartialWithHeaders(ctx,
h.PushQsHeader(ctx, h.NewQs("tab", tab)),
2024-09-21 17:08:23 +00:00
List(list, tab),
Footer(list, tab),
2024-09-19 23:13:04 +00:00
)
}