From 45ecbcbe77bc678aaf2a9313cdbd50660425a83a Mon Sep 17 00:00:00 2001 From: Cameron Reed Date: Mon, 19 Aug 2024 16:02:45 -0600 Subject: [PATCH] Sort elements as they are added --- main.go | 3 +- pages/fragments.go | 90 +---------------------- pages/templates/root.templ | 25 +++++-- pages/templates/root_templ.go | 135 +++++++++++++++++++++++++++++----- pages/webapi.go | 101 +++++++++++++++++++++++++ static/css/styles.css | 16 ++++ static/js/script.js | 85 +++++++++++++++++++++ 7 files changed, 341 insertions(+), 114 deletions(-) create mode 100644 pages/webapi.go create mode 100644 static/js/script.js diff --git a/main.go b/main.go index 4dc68fa..a91ec56 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( ) func main() { + db_path := flag.String("db", "./test.db", "Path to the sqlite3 database") bind_port := flag.Int("p", 8080, "Port to bind to") bind_addr := flag.String("a", "0.0.0.0", "Address to bind to") noFront := flag.Bool("no-frontend", false, "Disable the frontend endpoints") @@ -34,7 +35,7 @@ func main() { addBackendEndpoints(mux) } - db.Open("test.db") + db.Open(*db_path) defer db.Close() addr := fmt.Sprintf("%s:%d", *bind_addr, *bind_port) diff --git a/pages/fragments.go b/pages/fragments.go index ccb5fde..814eef4 100644 --- a/pages/fragments.go +++ b/pages/fragments.go @@ -1,14 +1,10 @@ package pages import ( - "fmt" "net/http" - "strconv" - "time" "github.com/Cameron-Reed1/todo-web/db" "github.com/Cameron-Reed1/todo-web/pages/templates" - "github.com/Cameron-Reed1/todo-web/types" ) func OverdueFragment(w http.ResponseWriter, r *http.Request) { @@ -18,7 +14,7 @@ func OverdueFragment(w http.ResponseWriter, r *http.Request) { return } - templates.TodoList(items).Render(r.Context(), w) + templates.TodoList("No overdue items", items).Render(r.Context(), w) } func TodayFragment(w http.ResponseWriter, r *http.Request) { @@ -28,7 +24,7 @@ func TodayFragment(w http.ResponseWriter, r *http.Request) { return } - templates.TodoList(items).Render(r.Context(), w) + templates.TodoList("No items for today", items).Render(r.Context(), w) } func UpcomingFragment(w http.ResponseWriter, r *http.Request) { @@ -38,85 +34,5 @@ func UpcomingFragment(w http.ResponseWriter, r *http.Request) { return } - templates.TodoList(items).Render(r.Context(), w) -} - -func CreateItem(w http.ResponseWriter, r *http.Request) { - var todo types.Todo - var err error - - todo.Text = r.FormValue("name") - start := r.FormValue("start") - due := r.FormValue("due") - - if start != "" { - start_time, err := time.Parse("2006-01-02T03:04", start) - if err != nil { - fmt.Printf("Bad start time: %s\n", start) - w.WriteHeader(http.StatusBadRequest) - return - } - todo.Start = start_time.Unix() - } else { - todo.Start = 0 - } - - if due != "" { - due_time, err := time.Parse("2006-01-02T15:04", due) - if err != nil { - fmt.Printf("Bad due time: %s\n", due) - w.WriteHeader(http.StatusBadRequest) - return - } - todo.Due = due_time.Unix() - } else { - todo.Due = 0 - } - - fmt.Printf("New item: %s: %d - %d\n", todo.Text, todo.Start, todo.Due) - - err = db.AddTodo(&todo) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write([]byte{}) -} - -func DeleteItem(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.Atoi(idStr) - - if idStr == "" || err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } - - err = db.DeleteTodo(id) - - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write([]byte{}) -} - -func SetItemCompleted(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.Atoi(idStr) - - if idStr == "" || err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } - - completed := r.FormValue("completed") == "on" - if err = db.SetCompleted(id, completed); err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write([]byte{}) + templates.TodoList("No upcoming items", items).Render(r.Context(), w) } diff --git a/pages/templates/root.templ b/pages/templates/root.templ index f9b240e..5cc2cd1 100644 --- a/pages/templates/root.templ +++ b/pages/templates/root.templ @@ -16,6 +16,7 @@ templ RootPage() { + @@ -38,18 +39,21 @@ templ RootPage() {
Overdue
+
Today
+
Upcoming
+
-
+
Create new Todo
@@ -60,17 +64,17 @@ templ RootPage() {

- +

- +
- + Close
@@ -80,7 +84,7 @@ templ RootPage() { } templ TodoItem(item types.Todo) { -
+
{ item.Text }
@@ -90,8 +94,15 @@ templ TodoItem(item types.Todo) {
} -templ TodoList(items []types.Todo) { -
+templ OobTodoItem(targetSelector string, item types.Todo) { +
+ @TodoItem(item) +
+} + +templ TodoList(fillerText string, items []types.Todo) { +
+
{ fillerText }
for _, item := range items { @TodoItem(item) } diff --git a/pages/templates/root_templ.go b/pages/templates/root_templ.go index 9786886..5a5b5e4 100644 --- a/pages/templates/root_templ.go +++ b/pages/templates/root_templ.go @@ -29,7 +29,7 @@ func RootPage() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Todo
Overdue
Today
Upcoming
Create new Todo





Close
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Todo
Overdue
Today
Upcoming
Create new Todo





Close
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -53,7 +53,33 @@ func TodoItem(item types.Todo) templ.Component { templ_7745c5c3_Var2 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = TodoItem(item).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func TodoList(fillerText string, items []types.Todo) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fillerText) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 105, Col: 45} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pages/webapi.go b/pages/webapi.go new file mode 100644 index 0000000..84648ef --- /dev/null +++ b/pages/webapi.go @@ -0,0 +1,101 @@ +package pages + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "github.com/Cameron-Reed1/todo-web/db" + "github.com/Cameron-Reed1/todo-web/pages/templates" + "github.com/Cameron-Reed1/todo-web/types" +) + +func CreateItem(w http.ResponseWriter, r *http.Request) { + var todo types.Todo + var err error + + todo.Text = r.FormValue("name") + start := r.FormValue("start") + due := r.FormValue("due") + + fmt.Printf("Create item request: %s: %s - %s\n", todo.Text, start, due) + + if start != "" { + todo.Start, err = strconv.ParseInt(start, 10, 64) + if err != nil { + fmt.Printf("Bad start time: %s\n", start) + w.WriteHeader(http.StatusBadRequest) + return + } + } else { + todo.Start = 0 + } + + if due != "" { + todo.Due, err = strconv.ParseInt(due, 10, 64) + if err != nil { + fmt.Printf("Bad due time: %s\n", due) + w.WriteHeader(http.StatusBadRequest) + return + } + } else { + todo.Due = 0 + } + + fmt.Printf("New item: %s: %d - %d\n", todo.Text, todo.Start, todo.Due) + + err = db.AddTodo(&todo) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + now := time.Now().Unix() + var targetSelector = "#today-list > .new-item" + if todo.Due != 0 && todo.Due < now { + targetSelector = "#overdue-list > .new-item" + } + if todo.Start > now { + targetSelector = "#upcoming-list > .new-item" + } + + templates.OobTodoItem(targetSelector, todo).Render(r.Context(), w) +} + +func DeleteItem(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + + if idStr == "" || err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + err = db.DeleteTodo(id) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write([]byte{}) +} + +func SetItemCompleted(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + + if idStr == "" || err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + completed := r.FormValue("completed") == "on" + if err = db.SetCompleted(id, completed); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write([]byte{}) +} diff --git a/static/css/styles.css b/static/css/styles.css index ece50cf..dde4db7 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -212,3 +212,19 @@ nav { color: blue; border-color: blue; } + +.new-item, .filler-item { + display: none; +} + +[data-item-count="0"] +.filler-item { + display: block; + width: calc(100% - 12px); + flex: 1; + text-align: center; + font-weight: bold; + font-size: 1.5rem; + line-height: 40px; + margin: 6px; +} diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..bb495c7 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,85 @@ +document.addEventListener("keyup", (event) => { + if (event.key === "Escape") { + window.location.hash = ''; + } +}); + +function on_load() { + let start_input = document.getElementById("create-item-form-start"); + let due_input = document.getElementById("create-item-form-due"); + + let create_form = document.getElementById("create-item"); + + create_form.addEventListener("htmx:configRequest", function(evt) { + evt.detail.parameters["start"] = start_input.value ? start_input.valueAsNumber / 1000 : 0; + evt.detail.parameters["due"] = due_input.value ? due_input.valueAsNumber / 1000 : 0; + }); + + create_form.addEventListener("htmx:afterRequest", function(evt) { + if (evt.detail.successful) { + window.location.hash = ''; + evt.detail.elt.reset(); + } + }); + + document.querySelector("#overdue-list > .new-item").addEventListener("htmx:oobBeforeSwap", function(evt) { + let overdue_items = document.querySelector("#overdue-list > .todo-list-items"); + let due = parseInt(evt.detail.fragment.firstElementChild.getAttribute("data-due")); + + let target = overdue_items.children[overdue_items.children.length - 1]; + for (let i = 1; i < overdue_items.children.length; i++) { + if (parseInt(overdue_items.children[i].getAttribute("data-due")) > due) { + target = overdue_items.children[i - 1]; + break; + } + } + + evt.detail.target = target; + + overdue_items.setAttribute("data-item-count", parseInt(overdue_items.getAttribute("data-item-count")) + 1); + }); + + document.querySelector("#today-list > .new-item").addEventListener("htmx:oobBeforeSwap", function(evt) { + let today_items = document.querySelector("#today-list > .todo-list-items"); + let due = parseInt(evt.detail.fragment.firstElementChild.getAttribute("data-due")); + + let target = today_items.children[today_items.children.length - 1]; + if (due !== 0) { + for (let i = 1; i < today_items.children.length; i++) { + if (parseInt(today_items.children[i].getAttribute("data-due")) > due) { + target = today_items.children[i - 1]; + break; + } + } + } + + evt.detail.target = target; + + today_items.setAttribute("data-item-count", parseInt(today_items.getAttribute("data-item-count")) + 1); + }); + + document.querySelector("#upcoming-list > .new-item").addEventListener("htmx:oobBeforeSwap", function(evt) { + let upcoming_items = document.querySelector("#upcoming-list > .todo-list-items"); + let start = parseInt(evt.detail.fragment.firstElementChild.getAttribute("data-start")); + + let target = upcoming_items.children[0]; + for (let i = 1; i < upcoming_items.children.length; i++) { + if (parseInt(upcoming_items.children[i].getAttribute("data-start")) > start) { + target = upcoming_items.children[i - 1]; + break; + } + } + + evt.detail.target = target; + + upcoming_items.setAttribute("data-item-count", parseInt(upcoming_items.getAttribute("data-item-count")) + 1); + }); +} + + +if (document.readyState === "completed") { + on_load(); +} else { + document.addEventListener("DOMContentLoaded", on_load); +} +