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-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
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 01:24:44 +00:00
|
|
|
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)),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 01:24:44 +00:00
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 01:24:44 +00:00
|
|
|
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",
|
|
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 01:24:44 +00:00
|
|
|
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(
|
2024-09-26 19:39:34 +00:00
|
|
|
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-26 19:39:34 +00:00
|
|
|
}), h.Text("›"),
|
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
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 01:24:44 +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"),
|
2024-09-20 01:24:44 +00:00
|
|
|
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"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 01:24:44 +00:00
|
|
|
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(
|
2024-09-20 01:24:44 +00:00
|
|
|
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)
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 01:24:44 +00:00
|
|
|
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())),
|
2024-09-20 01:24:44 +00:00
|
|
|
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),
|
2024-09-20 01:24:44 +00:00
|
|
|
h.Attributes(&h.AttributeMap{
|
|
|
|
|
"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),
|
|
|
|
|
)),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 01:24:44 +00:00
|
|
|
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,
|
|
|
|
|
}),
|
2024-09-29 01:45:27 +00:00
|
|
|
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")))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")))
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 01:34:50 +00:00
|
|
|
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-19 23:13:04 +00:00
|
|
|
if name == "" {
|
|
|
|
|
return h.NewPartial(h.Div(h.Text("name is required")))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
service := tasks.NewService(ctx.ServiceLocator())
|
|
|
|
|
_, err := service.Create(tasks.CreateRequest{
|
|
|
|
|
Name: name,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return h.NewPartial(h.Div(h.Text("failed to create")))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list, _ := service.List()
|
|
|
|
|
|
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,
|
2024-09-22 19:37:24 +00:00
|
|
|
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
|
|
|
)
|
|
|
|
|
}
|