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() {
-
@@ -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")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Todo")
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);
+}
+