diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..e3ae1e5 --- /dev/null +++ b/db/db.go @@ -0,0 +1,19 @@ +package db + +import "github.com/google/uuid" + +type Todo struct { + Id uuid.UUID + Name string + Done bool +} + +type TodosStore struct { + Todos []Todo +} + +func NewTodoStore() TodosStore { + return TodosStore{ + Todos: []Todo{}, + } +} diff --git a/go.mod b/go.mod index 00e9f8f..687b23c 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module michaelthomson.dev/mthomson/go-todos-app go 1.22.1 require github.com/a-h/templ v0.2.707 + +require github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index ce5d368..8eeab4e 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U= github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/handlers/homeHandler.go b/handlers/homeHandler.go new file mode 100644 index 0000000..bf06845 --- /dev/null +++ b/handlers/homeHandler.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "net/http" + + "michaelthomson.dev/mthomson/go-todos-app/services" + "michaelthomson.dev/mthomson/go-todos-app/templates/pages" +) + +type HomeHandler struct { + ts services.TodoService +} + +func NewHomeHandler(ts services.TodoService) HomeHandler { + return HomeHandler{ts: ts} +} + +func (h *HomeHandler) Home(w http.ResponseWriter, r *http.Request) { + var err error + + todos, err := h.ts.GetTodos() + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + err = pages.Home(todos).Render(r.Context(), w) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func (h HomeHandler) Router() http.Handler { + router := http.NewServeMux() + + router.HandleFunc("/", h.Home) + + return router +} diff --git a/handlers/todoHandler.go b/handlers/todoHandler.go new file mode 100644 index 0000000..dd9716f --- /dev/null +++ b/handlers/todoHandler.go @@ -0,0 +1,61 @@ +package handlers + +import ( + "net/http" + + "github.com/google/uuid" + "michaelthomson.dev/mthomson/go-todos-app/services" + "michaelthomson.dev/mthomson/go-todos-app/templates/partials" +) + +type TodoHandler struct { + ts services.TodoService +} + +func NewTodoHandler(ts services.TodoService) TodoHandler { + return TodoHandler{ts: ts} +} + +func (h TodoHandler) Create(w http.ResponseWriter, r *http.Request) { + var err error + err = r.ParseForm() + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + name := r.FormValue("name") + + addedTodo, err := h.ts.AddTodo(name) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = partials.Todo(addedTodo).Render(r.Context(), w) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func (h TodoHandler) Delete(w http.ResponseWriter, r *http.Request) { + var err error + + id, err := uuid.Parse(r.PathValue("id")) + + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = h.ts.DeleteTodoById(id) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/main.go b/main.go index 36a146a..5d445c9 100644 --- a/main.go +++ b/main.go @@ -3,24 +3,28 @@ package main import ( "log" "net/http" - "path/filepath" - "github.com/a-h/templ" - "michaelthomson.dev/mthomson/go-todos-app/views" + "michaelthomson.dev/mthomson/go-todos-app/db" + "michaelthomson.dev/mthomson/go-todos-app/handlers" + "michaelthomson.dev/mthomson/go-todos-app/services" ) func main() { + todosStore := db.NewTodoStore() + ts := services.NewTodoService(&todosStore) + + homeHandler := handlers.NewHomeHandler(*ts) + todoHandler := handlers.NewTodoHandler(*ts) + router := http.NewServeMux() // Serve static files - router.HandleFunc("GET /assets/", func(w http.ResponseWriter, r *http.Request) { - filePath := r.URL.Path[len("/assets/"):] - fullPath := filepath.Join(".", "assets", filePath) - http.ServeFile(w, r, fullPath) - }) + fs := http.FileServer(http.Dir("./assets")) + router.Handle("GET /assets/", http.StripPrefix("/assets/", fs)) - home := views.Home() - router.Handle("GET /", templ.Handler(home)) + router.HandleFunc("GET /{$}", homeHandler.Home) + router.HandleFunc("POST /todos", todoHandler.Create) + router.HandleFunc("DELETE /todos/{id}", todoHandler.Delete) server := http.Server{ Addr: "localhost:3000", diff --git a/services/todoService.go b/services/todoService.go new file mode 100644 index 0000000..6ced1ad --- /dev/null +++ b/services/todoService.go @@ -0,0 +1,61 @@ +package services + +import ( + "fmt" + + "github.com/google/uuid" + "michaelthomson.dev/mthomson/go-todos-app/db" +) + +type TodoService struct { + store *db.TodosStore +} + +func NewTodoService(store *db.TodosStore) *TodoService { + return &TodoService{ + store: store, + } +} + +func (ts *TodoService) AddTodo(name string) (todo db.Todo, err error) { + addedTodo := db.Todo{ + Id: uuid.New(), + Name: name, + Done: false, + } + + ts.store.Todos = append(ts.store.Todos, addedTodo) + + return addedTodo, nil +} + +func (ts *TodoService) GetTodos() (todos []db.Todo, err error) { + return ts.store.Todos, nil +} + +func (ts *TodoService) GetTodoById(id uuid.UUID) (todo db.Todo, err error) { + for _, todo := range ts.store.Todos { + if id == todo.Id { + return todo, nil + } + } + + return db.Todo{}, fmt.Errorf("Could not find todo by id %q", id) +} + +func (ts *TodoService) DeleteTodoById(id uuid.UUID) (err error) { + index := -1 + for i, todo := range ts.store.Todos { + if id == todo.Id { + index = i + } + } + + if index == -1 { + return fmt.Errorf("Could not find todo by id %q", id) + } + + ts.store.Todos = append(ts.store.Todos[:index], ts.store.Todos[index + 1:]...) + + return nil +} diff --git a/templates/pages/home.templ b/templates/pages/home.templ new file mode 100644 index 0000000..629036d --- /dev/null +++ b/templates/pages/home.templ @@ -0,0 +1,20 @@ +package pages + +import "michaelthomson.dev/mthomson/go-todos-app/templates/shared" +import "michaelthomson.dev/mthomson/go-todos-app/db" +import "michaelthomson.dev/mthomson/go-todos-app/templates/partials" + +templ Home(todos []db.Todo) { + @shared.Page("Todos") { +
Todos
+
+ + +
+
+ for _, todo := range todos { + @partials.Todo(todo) + } +
+ } +} diff --git a/views/home_templ.go b/templates/pages/home_templ.go similarity index 64% rename from views/home_templ.go rename to templates/pages/home_templ.go index 6cd1e56..ef47fac 100644 --- a/views/home_templ.go +++ b/templates/pages/home_templ.go @@ -1,7 +1,7 @@ // Code generated by templ - DO NOT EDIT. // templ: version: v0.2.663 -package views +package pages //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -10,9 +10,11 @@ import "context" import "io" import "bytes" -import "michaelthomson.dev/mthomson/go-todos-app/views/template" +import "michaelthomson.dev/mthomson/go-todos-app/templates/shared" +import "michaelthomson.dev/mthomson/go-todos-app/db" +import "michaelthomson.dev/mthomson/go-todos-app/templates/partials" -func Home() templ.Component { +func Home(todos []db.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 { @@ -31,7 +33,17 @@ func Home() templ.Component { templ_7745c5c3_Buffer = templ.GetBuffer() defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Home page
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Todos
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, todo := range todos { + templ_7745c5c3_Err = partials.Todo(todo).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 } @@ -40,7 +52,7 @@ func Home() templ.Component { } return templ_7745c5c3_Err }) - templ_7745c5c3_Err = template.Base("Todos app").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = shared.Page("Todos").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/partials/todo.templ b/templates/partials/todo.templ new file mode 100644 index 0000000..e72c038 --- /dev/null +++ b/templates/partials/todo.templ @@ -0,0 +1,14 @@ +package partials + +import "michaelthomson.dev/mthomson/go-todos-app/db" + +func todoId(todo db.Todo) string { + return "todo-" + todo.Id.String() +} + +templ Todo(todo db.Todo) { +
+ { todo.Id.String() }: { todo.Name } + +
+} diff --git a/templates/partials/todo_templ.go b/templates/partials/todo_templ.go new file mode 100644 index 0000000..35177dd --- /dev/null +++ b/templates/partials/todo_templ.go @@ -0,0 +1,106 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.663 +package partials + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import "michaelthomson.dev/mthomson/go-todos-app/db" + +func todoId(todo db.Todo) string { + return "todo-" + todo.Id.String() +} + +func Todo(todo db.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_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = 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_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Id.String()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 11, Col: 22} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + 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 + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 11, Col: 37} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + 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 + }) +} diff --git a/views/template/template.templ b/templates/shared/page.templ similarity index 90% rename from views/template/template.templ rename to templates/shared/page.templ index 50cbc3f..31ad4e1 100644 --- a/views/template/template.templ +++ b/templates/shared/page.templ @@ -1,6 +1,6 @@ -package template +package shared -templ Base(title string) { +templ Page(title string) { diff --git a/views/template/template_templ.go b/templates/shared/page_templ.go similarity index 92% rename from views/template/template_templ.go rename to templates/shared/page_templ.go index fe9b2a9..d300e89 100644 --- a/views/template/template_templ.go +++ b/templates/shared/page_templ.go @@ -1,7 +1,7 @@ // Code generated by templ - DO NOT EDIT. // templ: version: v0.2.663 -package template +package shared //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -10,7 +10,7 @@ import "context" import "io" import "bytes" -func Base(title string) templ.Component { +func Page(title string) 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 { @@ -30,7 +30,7 @@ func Base(title string) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/template/template.templ`, Line: 10, Col: 25} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/shared/page.templ`, Line: 10, Col: 25} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { diff --git a/views/home.templ b/views/home.templ deleted file mode 100644 index 478457c..0000000 --- a/views/home.templ +++ /dev/null @@ -1,9 +0,0 @@ -package views - -import "michaelthomson.dev/mthomson/go-todos-app/views/template" - -templ Home() { - @template.Base("Todos app") { -
Home page
- } -}