postgres and such
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/dryrun Pipeline was successful
ci/woodpecker/push/publish-tag Pipeline was successful
ci/woodpecker/push/publish-latest Pipeline was successful

This commit is contained in:
Michael Thomson 2024-06-17 23:37:43 -04:00
parent 088365c411
commit 6f7bcc9503
17 changed files with 423 additions and 36 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=postgres://postgres@localhost:5432/todos

2
.gitignore vendored
View File

@ -24,3 +24,5 @@ go.work
.direnv/ .direnv/
tmp/ tmp/
.env

View File

@ -562,6 +562,10 @@ video {
display: flex; display: flex;
} }
.table {
display: table;
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }

14
go.mod
View File

@ -4,4 +4,16 @@ go 1.22.1
require github.com/a-h/templ v0.2.707 require github.com/a-h/templ v0.2.707
require github.com/google/uuid v1.6.0 // indirect require (
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gofrs/uuid/v5 v5.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/joho/godotenv v1.5.1 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/text v0.14.0 // indirect
)

29
go.sum
View File

@ -1,6 +1,35 @@
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U= 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/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2 h1:QWdhlQz98hUe1xmjADOl2mr8ERLrOqj0KWLdkrnNsRQ=
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2/go.mod h1:Ti7pyNDU/UpXKmBTeFgxTvzYDM9xHLiYKMsLdt4b9cg=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -3,7 +3,7 @@ package handlers
import ( import (
"net/http" "net/http"
"github.com/google/uuid" "github.com/gofrs/uuid/v5"
"michaelthomson.dev/mthomson/go-todos-app/services" "michaelthomson.dev/mthomson/go-todos-app/services"
"michaelthomson.dev/mthomson/go-todos-app/templates/partials" "michaelthomson.dev/mthomson/go-todos-app/templates/partials"
) )
@ -42,10 +42,74 @@ func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
} }
} }
func (h *TodoHandler) Done(w http.ResponseWriter, r *http.Request) {
var err error
id, err := uuid.FromString(r.PathValue("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
todo, err := h.ts.GetTodoById(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
todo, err = h.ts.UpdateTodo(id, todo.Name, true)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = partials.Todo(todo).Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *TodoHandler) Undone(w http.ResponseWriter, r *http.Request) {
var err error
id, err := uuid.FromString(r.PathValue("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
todo, err := h.ts.GetTodoById(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
todo, err = h.ts.UpdateTodo(id, todo.Name, false)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = partials.Todo(todo).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) { func (h *TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
var err error var err error
id, err := uuid.Parse(r.PathValue("id")) id, err := uuid.FromString(r.PathValue("id"))
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)

113
main.go
View File

@ -1,20 +1,121 @@
package main package main
import ( import (
"context"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath"
"michaelthomson.dev/mthomson/go-todos-app/db" pgxuuid "github.com/jackc/pgx-gofrs-uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/joho/godotenv"
"michaelthomson.dev/mthomson/go-todos-app/handlers" "michaelthomson.dev/mthomson/go-todos-app/handlers"
"michaelthomson.dev/mthomson/go-todos-app/repositories"
"michaelthomson.dev/mthomson/go-todos-app/services" "michaelthomson.dev/mthomson/go-todos-app/services"
) )
func main() { func main() {
todosStore := db.NewTodoStore() // load environment
ts := services.NewTodoService(&todosStore) err := godotenv.Load()
homeHandler := handlers.NewHomeHandler(ts) if err != nil {
todoHandler := handlers.NewTodoHandler(ts) log.Fatalf("Error loading .env file: %v", err)
os.Exit(1)
}
databaseUrl := os.Getenv("DATABASE_URL")
// connect to Db
dbconfig, err := pgxpool.ParseConfig(databaseUrl)
if err != nil {
log.Fatalf("Unable to parse db config: %v", err)
os.Exit(1)
}
dbconfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
pgxuuid.Register(conn.TypeMap())
return nil
}
dbpool, err := pgxpool.New(context.Background(), databaseUrl)
if err != nil {
log.Fatalf("Unable to create connection pool: %v", err)
os.Exit(1)
}
defer dbpool.Close()
// apply initial sql
log.Print("Applying migrations table")
content, err := os.ReadFile(filepath.Join("sql", "init.sql"))
if err != nil {
log.Fatal(err)
os.Exit(1)
}
_, err = dbpool.Exec(context.Background(), string(content))
if err != nil {
log.Fatal(err)
os.Exit(1)
}
//apply migrations
log.Print("Starting migrations...")
files, err := os.ReadDir("./sql/migrations")
if err != nil {
log.Fatal(err)
os.Exit(1)
}
for _, file := range files {
path := filepath.Join("sql", "migrations", file.Name())
content, err := os.ReadFile(path)
if err != nil {
log.Fatal(err)
os.Exit(1)
}
var count int
err = dbpool.QueryRow(context.Background(), "SELECT COUNT(1) FROM migration WHERE name=$1", file.Name()).Scan(&count)
if err != nil {
log.Fatal(err)
os.Exit(1)
}
if count == 0 {
log.Printf("Running migration: %s", file.Name())
_, err = dbpool.Exec(context.Background(), string(content))
if err != nil {
log.Fatal(err)
os.Exit(1)
}
_, err = dbpool.Exec(context.Background(), "INSERT INTO migration(name) values($1)", file.Name())
if err != nil {
log.Fatal(err)
os.Exit(1)
}
} else {
log.Printf("Migration already exists: %s", file.Name())
}
}
// set up repositories
todoRepository := repositories.NewPostgresTodoRepository(dbpool)
// set up services
todoService := services.NewTodoService(&todoRepository)
// set up handlers
homeHandler := handlers.NewHomeHandler(todoService)
todoHandler := handlers.NewTodoHandler(todoService)
router := http.NewServeMux() router := http.NewServeMux()
@ -25,6 +126,8 @@ func main() {
router.HandleFunc("GET /{$}", homeHandler.Home) router.HandleFunc("GET /{$}", homeHandler.Home)
router.HandleFunc("POST /todos", todoHandler.Create) router.HandleFunc("POST /todos", todoHandler.Create)
router.HandleFunc("DELETE /todos/{id}", todoHandler.Delete) router.HandleFunc("DELETE /todos/{id}", todoHandler.Delete)
router.HandleFunc("PATCH /todos/{id}/done", todoHandler.Done)
router.HandleFunc("PATCH /todos/{id}/undone", todoHandler.Undone)
server := http.Server{ server := http.Server{
Addr: "localhost:3000", Addr: "localhost:3000",

View File

@ -1,6 +1,6 @@
package models package models
import "github.com/google/uuid" import "github.com/gofrs/uuid/v5"
type Todo struct { type Todo struct {
Id uuid.UUID Id uuid.UUID
@ -8,9 +8,9 @@ type Todo struct {
Done bool Done bool
} }
func NewTodo(name string, done bool) Todo { func NewTodo(id uuid.UUID, name string, done bool) Todo {
return Todo{ return Todo{
Id: uuid.New(), Id: id,
Name: name, Name: name,
Done: done, Done: done,
} }

View File

@ -1,21 +1,21 @@
package db package repositories
import ( import (
"fmt" "fmt"
"github.com/google/uuid" "github.com/gofrs/uuid/v5"
"michaelthomson.dev/mthomson/go-todos-app/models" "michaelthomson.dev/mthomson/go-todos-app/models"
) )
type TodosStore struct { type InMemoryTodoRepository struct {
Todos []models.Todo Todos []models.Todo
} }
func (ts *TodosStore) List() (todo []models.Todo, err error) { func (ts *InMemoryTodoRepository) List() (todo []models.Todo, err error) {
return ts.Todos, nil return ts.Todos, nil
} }
func (ts *TodosStore) Get(id uuid.UUID) (todo models.Todo, err error) { func (ts *InMemoryTodoRepository) Get(id uuid.UUID) (todo models.Todo, err error) {
index := -1 index := -1
for i, todo := range ts.Todos { for i, todo := range ts.Todos {
if id == todo.Id { if id == todo.Id {
@ -32,12 +32,12 @@ func (ts *TodosStore) Get(id uuid.UUID) (todo models.Todo, err error) {
return ts.Todos[index], nil return ts.Todos[index], nil
} }
func (ts *TodosStore) Add(todo models.Todo) (addedTodo models.Todo, err error) { func (ts *InMemoryTodoRepository) Add(todo models.Todo) (addedTodo models.Todo, err error) {
ts.Todos = append(ts.Todos, todo) ts.Todos = append(ts.Todos, todo)
return todo, nil return todo, nil
} }
func (ts *TodosStore) Delete(id uuid.UUID) (err error) { func (ts *InMemoryTodoRepository) Delete(id uuid.UUID) (err error) {
index := -1 index := -1
for i, todo := range ts.Todos { for i, todo := range ts.Todos {
if id == todo.Id { if id == todo.Id {
@ -54,8 +54,8 @@ func (ts *TodosStore) Delete(id uuid.UUID) (err error) {
return nil return nil
} }
func NewTodoStore() TodosStore { func NewInMemoryTodoRepository() InMemoryTodoRepository {
return TodosStore{ return InMemoryTodoRepository{
Todos: []models.Todo{}, Todos: []models.Todo{},
} }
} }

View File

@ -0,0 +1,86 @@
package repositories
import (
"context"
"log"
"github.com/gofrs/uuid/v5"
"github.com/jackc/pgx/v5/pgxpool"
"michaelthomson.dev/mthomson/go-todos-app/models"
)
type PostgresTodoRepository struct {
db *pgxpool.Pool
}
func (ts *PostgresTodoRepository) List() ([]models.Todo, error) {
var todos []models.Todo
rows, err := ts.db.Query(context.Background(), "SELECT id, name, done FROM todo")
if err != nil {
log.Fatal(err)
return todos, err
}
for rows.Next() {
var r models.Todo
err := rows.Scan(&r.Id, &r.Name, &r.Done)
if err != nil {
log.Fatal(err)
return todos, err
}
todos = append(todos, r)
}
return todos, err
}
func (ts *PostgresTodoRepository) Get(id uuid.UUID) (models.Todo, error) {
var todo models.Todo
err := ts.db.QueryRow(context.Background(), "SELECT id, name, done FROM todo WHERE id=$1", id).Scan(&todo.Id, &todo.Name, &todo.Done)
if err != nil {
log.Fatal(err)
return todo, err
}
return todo, err
}
func (ts *PostgresTodoRepository) Update(id uuid.UUID, name string, done bool) error {
_, err := ts.db.Exec(context.Background(), "UPDATE todo SET name=$1, done=$2 WHERE id=$3", name, done, id)
if err != nil {
log.Fatal(err)
}
return err
}
func (ts *PostgresTodoRepository) Add(todo models.Todo) error {
_, err := ts.db.Exec(context.Background(), "INSERT INTO todo(id, name, done) values($1, $2, $3)", todo.Id, todo.Name, todo.Done)
if err != nil {
log.Fatal(err)
return err
}
return err
}
func (ts *PostgresTodoRepository) Delete(id uuid.UUID) error {
_, err := ts.db.Exec(context.Background(), "DELETE FROM todo where id=$1", id)
if err != nil {
log.Fatal(err)
return err
}
return err
}
func NewPostgresTodoRepository(db *pgxpool.Pool) PostgresTodoRepository {
return PostgresTodoRepository{
db: db,
}
}

View File

@ -3,15 +3,16 @@ package services
import ( import (
"fmt" "fmt"
"github.com/google/uuid" "github.com/gofrs/uuid/v5"
"michaelthomson.dev/mthomson/go-todos-app/models" "michaelthomson.dev/mthomson/go-todos-app/models"
) )
type Db interface { type Db interface {
List() (todo []models.Todo, err error) List() ([]models.Todo, error)
Get(id uuid.UUID) (todo models.Todo, err error) Get(id uuid.UUID) (models.Todo, error)
Add(todo models.Todo) (addedTodo models.Todo, err error) Add(todo models.Todo) error
Delete(id uuid.UUID) (err error) Delete(id uuid.UUID) error
Update(id uuid.UUID, name string, done bool) error
} }
type TodoService struct { type TodoService struct {
@ -24,11 +25,33 @@ func NewTodoService(db Db) *TodoService {
} }
} }
func (ts *TodoService) AddTodo(name string) (todo models.Todo, err error) { func (ts *TodoService) AddTodo(name string) (models.Todo, error) {
if name == "" { if name == "" {
return models.Todo{}, fmt.Errorf("Must provide a name") return models.Todo{}, fmt.Errorf("Must provide a name")
} }
return ts.db.Add(models.NewTodo(name, false))
id, err := uuid.NewV4()
if err != nil {
return models.Todo{}, err
}
err = ts.db.Add(models.NewTodo(id, name, false))
if err != nil {
return models.Todo{}, err
}
return ts.db.Get(id)
}
func (ts *TodoService) UpdateTodo(id uuid.UUID, name string, done bool) (models.Todo, error) {
err := ts.db.Update(id, name, done)
if err != nil {
return models.Todo{}, err
}
return ts.db.Get(id)
} }
func (ts *TodoService) GetTodos() (todos []models.Todo, err error) { func (ts *TodoService) GetTodos() (todos []models.Todo, err error) {

5
sql/init.sql Normal file
View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS migration (
id serial primary key not null,
name text not null,
applied_at timestamp not null default now()
);

View File

@ -0,0 +1,5 @@
CREATE TABLE todo (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
done BOOLEAN NOT NULL DEFAULT false
);

View File

@ -6,7 +6,7 @@ import "michaelthomson.dev/mthomson/go-todos-app/models"
templ Home(todos []models.Todo) { templ Home(todos []models.Todo) {
@shared.Page("Todos") { @shared.Page("Todos") {
<div class="flex content-center items-center flex-col"> <div class="flex flex-col">
<h1 class="text-3xl">Todos</h1> <h1 class="text-3xl">Todos</h1>
<form hx-post="/todos" hx-target="#todos" hx-swap="beforeend"> <form hx-post="/todos" hx-target="#todos" hx-swap="beforeend">
<input name="name" type="text" class="border border-black" /> <input name="name" type="text" class="border border-black" />

View File

@ -33,7 +33,7 @@ func Home(todos []models.Todo) templ.Component {
templ_7745c5c3_Buffer = templ.GetBuffer() templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex content-center items-center flex-col\"><h1 class=\"text-3xl\">Todos</h1><form hx-post=\"/todos\" hx-target=\"#todos\" hx-swap=\"beforeend\"><input name=\"name\" type=\"text\" class=\"border border-black\"> <button type=\"submit\">Submit</button></form><div id=\"todos\">") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex flex-col\"><h1 class=\"text-3xl\">Todos</h1><form hx-post=\"/todos\" hx-target=\"#todos\" hx-swap=\"beforeend\"><input name=\"name\" type=\"text\" class=\"border border-black\"> <button type=\"submit\">Submit</button></form><div id=\"todos\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -1,14 +1,23 @@
package partials package partials
import "michaelthomson.dev/mthomson/go-todos-app/models" import "michaelthomson.dev/mthomson/go-todos-app/models"
import "fmt"
func todoId(todo models.Todo) string { func todoId(todo models.Todo) string {
return "todo-" + todo.Id.String() return "todo-" + todo.Id.String()
} }
func patchUrl(todo models.Todo) string {
if todo.Done {
return string(templ.URL(fmt.Sprintf("/todos/%s/undone", todo.Id)))
}
return string(templ.URL(fmt.Sprintf("/todos/%s/done", todo.Id)))
}
templ Todo(todo models.Todo) { templ Todo(todo models.Todo) {
<div id={ todoId(todo) }> <div id={ todoId(todo) }>
{ todo.Name } { todo.Name }
<button hx-delete={ "/todos/" + todo.Id.String() } hx-target={ "#" + todoId(todo) } hx-swap="delete">Delete</button> <button hx-delete={ string(templ.URL(fmt.Sprintf("/todos/%s", todo.Id))) } hx-target={ "#" + todoId(todo) } hx-swap="delete">Delete</button>
<input type="checkbox" checked?={ todo.Done } hx-target={ "#" + todoId(todo) } hx-patch={ patchUrl(todo) } hx-swap="outerHTML" />
</div> </div>
} }

View File

@ -11,11 +11,19 @@ import "io"
import "bytes" import "bytes"
import "michaelthomson.dev/mthomson/go-todos-app/models" import "michaelthomson.dev/mthomson/go-todos-app/models"
import "fmt"
func todoId(todo models.Todo) string { func todoId(todo models.Todo) string {
return "todo-" + todo.Id.String() return "todo-" + todo.Id.String()
} }
func patchUrl(todo models.Todo) string {
if todo.Done {
return string(templ.URL(fmt.Sprintf("/todos/%s/undone", todo.Id)))
}
return string(templ.URL(fmt.Sprintf("/todos/%s/done", todo.Id)))
}
func Todo(todo models.Todo) templ.Component { func Todo(todo models.Todo) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { 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) templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
@ -36,7 +44,7 @@ func Todo(todo models.Todo) templ.Component {
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(todoId(todo)) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(todoId(todo))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 10, Col: 24} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 18, Col: 24}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -49,7 +57,7 @@ func Todo(todo models.Todo) templ.Component {
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Name) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 11, Col: 15} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 19, Col: 15}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -60,9 +68,9 @@ func Todo(todo models.Todo) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("/todos/" + todo.Id.String()) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/todos/%s", todo.Id))))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 12, Col: 52} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 20, Col: 76}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -75,13 +83,49 @@ func Todo(todo models.Todo) templ.Component {
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("#" + todoId(todo)) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("#" + todoId(todo))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 12, Col: 85} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 20, Col: 109}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"delete\">Delete</button></div>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"delete\">Delete</button> <input type=\"checkbox\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if todo.Done {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" checked")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" hx-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("#" + todoId(todo))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 21, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-patch=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(patchUrl(todo))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 21, Col: 108}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"outerHTML\"></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }