package task import ( "fmt" "github.com/google/uuid" "github.com/maddalax/htmgo/framework/h" "github.com/maddalax/htmgo/framework/hx" "github.com/maddalax/htmgo/framework/js" "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 { 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 { return h.Div( h.Id("tasks-card-body"), Input(list), List(list, tab), Footer(list, tab), ) } func Input(list []*ent.Task) *h.Element { return h.Div( h.Id("task-card-input"), h.Class("border border-b-slate-100 relative"), h.Input( "text", h.Required(), h.Disabled(), h.MaxLength(150), h.AutoComplete("off"), h.AutoFocus(), h.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)), h.HxTrigger(hx.OnEvent(hx.TriggerKeyUpEnter)), ), CompleteAllIcon(list), ) } func CompleteAllIcon(list []*ent.Task) *h.Element { notCompletedCount := len(h.Filter(list, func(item *ent.Task) bool { return item.CompletedAt == nil })) return h.Div( h.ClassX("absolute top-1 left-5 p-2 rotate-90 text-3xl cursor-pointer", map[string]bool{ "text-slate-400": notCompletedCount > 0, }), h.UnsafeRaw("›"), h.PostPartialWithQs(CompleteAll, h.NewQs("complete", h.Ternary(notCompletedCount > 0, "true", "false"))), ) } func Footer(list []*ent.Task, activeTab Tab) *h.Element { 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 { return h.P( h.PostOnClick(h.GetPartialPathWithQs(ChangeTab, h.NewQs("tab", tab))), 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 { 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 { 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 { 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{ "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.Name("task"), h.Value(task.ID.String()), h.Class("hidden"), ), h.Input( "text", h.PostPartial(UpdateName, hx.TriggerBlur, hx.TriggerKeyUpEnter), h.Attributes(&h.AttributeMap{ "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, }), ), ), ), h.P( h.GetPartialWithQs(EditNameForm, h.NewQs("id", task.ID.String()), hx.TriggerDblClick), 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 { return h.Div( h.HxTrigger(hx.OnClick()), h.Post(h.GetPartialPathWithQs(ToggleCompleted, h.NewQs("id", task.ID.String()))), 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(` `)), ), ) } 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"))) } if len(name) > 150 { return h.NewPartial(h.Div(h.Text("task must be less than 150 characters"))) } 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))) } 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() return h.SwapManyPartial(ctx, List(list, getActiveTab(ctx)), Footer(list, getActiveTab(ctx)), CompleteAllIcon(list), ) } func CompleteAll(ctx *h.RequestContext) *h.Partial { service := tasks.NewService(ctx.ServiceLocator()) service.SetAllCompleted(ctx.QueryParam("complete") == "true") list, _ := service.List() return h.NewPartial(h.OobSwap(ctx, CardBody(list, getActiveTab(ctx)))) } 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") if len(name) > 150 { return h.NewPartial( h.Div( h.HxOnLoad(js.Alert("Task must be less than 150 characters")), ), ) } service := tasks.NewService(ctx.ServiceLocator()) 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.")), ), ) } _, err := service.Create(tasks.CreateRequest{ Name: name, }) if err != nil { return h.NewPartial(h.Div(h.Text("failed to create"))) } list, err = service.List() return h.SwapManyPartial(ctx, CardBody(list, getActiveTab(ctx)), ) } func ChangeTab(ctx *h.RequestContext) *h.Partial { service := tasks.NewService(ctx.ServiceLocator()) list, _ := service.List() tab := ctx.QueryParam("tab") return h.SwapManyPartialWithHeaders(ctx, h.PushQsHeader(ctx, h.NewQs("tab", tab)), List(list, tab), Footer(list, tab), ) }