This commit is contained in:
commit
4fac3fbfa3
18 changed files with 682 additions and 0 deletions
34
.air.toml
Normal file
34
.air.toml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
root = "."
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
bin = "./tmp/main"
|
||||||
|
# cmd = "templ generate && go build -o ./tmp/main ./cmd/."
|
||||||
|
cmd = "templ generate && go build -o ./tmp/main ."
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = [".*_templ.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "templ", "html"]
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = true
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
27
.github/workflows/build.yml
vendored
Normal file
27
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
name: Build Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GOPRIVATE: git.wittern.io/public/echo-todos
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '^1.22.5'
|
||||||
|
- run: go version
|
||||||
|
- name: Build the Docker image
|
||||||
|
run: |
|
||||||
|
go install github.com/a-h/templ/cmd/templ@latest
|
||||||
|
templ generate
|
||||||
|
docker build . --file Dockerfile --tag git.wittern.io/public/echo-todos
|
||||||
|
docker push git.wittern.io/public/echo-todos
|
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# Mac filesystem jank.
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# generated templ files
|
||||||
|
**/*_templ.go
|
||||||
|
tmp/
|
||||||
|
data/*
|
||||||
|
todos.db
|
||||||
|
todos-app
|
||||||
|
.env
|
27
Dockerfile
Normal file
27
Dockerfile
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Stage 1: Builder
|
||||||
|
FROM golang:1.22.5-alpine AS builder
|
||||||
|
RUN apk add --no-cache gcc g++
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
RUN go mod download
|
||||||
|
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||||
|
RUN templ generate
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN go build -o todos-app
|
||||||
|
|
||||||
|
# Stage 2: Final Image
|
||||||
|
FROM alpine:3.19
|
||||||
|
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
WORKDIR /app
|
||||||
|
VOLUME /app/data
|
||||||
|
|
||||||
|
COPY --from=builder /app/todos-app .
|
||||||
|
COPY --from=builder /app/static /app/static
|
||||||
|
|
||||||
|
CMD ["./todos-app"]
|
7
compose.yml
Normal file
7
compose.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
services:
|
||||||
|
todos-app:
|
||||||
|
image: todos-app
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
ports:
|
||||||
|
- "1337:1337"
|
24
go.mod
Normal file
24
go.mod
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
module git.wittern.io/public/echo-todos
|
||||||
|
|
||||||
|
go 1.22.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/a-h/templ v0.2.747
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
|
golang.org/x/net v0.28.0 // indirect
|
||||||
|
golang.org/x/sys v0.23.0 // indirect
|
||||||
|
golang.org/x/text v0.17.0 // indirect
|
||||||
|
golang.org/x/time v0.6.0 // indirect
|
||||||
|
)
|
43
go.sum
Normal file
43
go.sum
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
|
||||||
|
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||||
|
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||||
|
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
|
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
114
handler/handler.go
Normal file
114
handler/handler.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.wittern.io/public/echo-todos/model"
|
||||||
|
"git.wittern.io/public/echo-todos/pkg/db"
|
||||||
|
"git.wittern.io/public/echo-todos/view"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func render(ctx echo.Context, status int, t templ.Component) error {
|
||||||
|
ctx.Response().Writer.WriteHeader(status)
|
||||||
|
|
||||||
|
err := t.Render(context.Background(), ctx.Response().Writer)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.String(http.StatusInternalServerError, "failed to render response template")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DBHandler struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h DBHandler) Index(c echo.Context) error {
|
||||||
|
todos, err := db.ListTodos(h.DB)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to get todos", "Error", err)
|
||||||
|
render(c, http.StatusInternalServerError, view.ErrorPage("failed to get todos"))
|
||||||
|
return c.String(http.StatusInternalServerError, "failed to get todos")
|
||||||
|
}
|
||||||
|
render(c, http.StatusOK, view.Index(todos))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h DBHandler) getTodos(c echo.Context) error {
|
||||||
|
todos, err := db.ListTodos(h.DB)
|
||||||
|
if err != nil {
|
||||||
|
return c.String(http.StatusInternalServerError, "failed to get todos")
|
||||||
|
}
|
||||||
|
|
||||||
|
render(c, http.StatusOK, view.Index(todos))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h DBHandler) Post(c echo.Context) error {
|
||||||
|
title := c.FormValue("todo")
|
||||||
|
todo := model.Todo{Title: title, Done: false}
|
||||||
|
db.InsertTodo(h.DB, todo)
|
||||||
|
todos, _ := db.ListTodos(h.DB)
|
||||||
|
render(c, http.StatusCreated, view.TodosList(todos))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h DBHandler) Delete(c echo.Context) error {
|
||||||
|
todoIdParam := c.Param("id")
|
||||||
|
|
||||||
|
id := extractTodoID(todoIdParam)
|
||||||
|
|
||||||
|
err := db.DeleteTodoByID(h.DB, id)
|
||||||
|
if err != nil {
|
||||||
|
return c.String(http.StatusInternalServerError, "failed to delete todo")
|
||||||
|
}
|
||||||
|
|
||||||
|
todos, _ := db.ListTodos(h.DB)
|
||||||
|
render(c, http.StatusOK, view.TodosList(todos))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h DBHandler) Toggle(c echo.Context) error {
|
||||||
|
todoIdParam := c.Param("id")
|
||||||
|
id := extractTodoID(todoIdParam)
|
||||||
|
|
||||||
|
todos, err := db.ToggleTodoByID(h.DB, id)
|
||||||
|
if err != nil {
|
||||||
|
return c.String(http.StatusInternalServerError, "failed to toggle todo")
|
||||||
|
}
|
||||||
|
render(c, http.StatusOK, view.TodosList(todos))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddRoutes(h DBHandler, e *echo.Echo) {
|
||||||
|
|
||||||
|
e.Use(middleware.Static("static"))
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
|
||||||
|
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
||||||
|
slog.Error(err.Error())
|
||||||
|
render(c, http.StatusInternalServerError, view.ErrorPage(err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
e.GET("/todos", h.getTodos)
|
||||||
|
e.POST("/todos", h.Post)
|
||||||
|
e.PUT("/todos/:id", h.Toggle)
|
||||||
|
e.DELETE("/todos/:id", h.Delete)
|
||||||
|
e.GET("/", h.Index)
|
||||||
|
// e.POST("/", h.Post(e, h))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTodoID(str string) int {
|
||||||
|
parts := strings.Split(str, "-")
|
||||||
|
todoID, _ := strconv.Atoi(parts[1])
|
||||||
|
return todoID
|
||||||
|
}
|
21
helper.go
Normal file
21
helper.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func render(ctx echo.Context, status int, t templ.Component) error {
|
||||||
|
ctx.Response().Writer.WriteHeader(status)
|
||||||
|
|
||||||
|
err := t.Render(context.Background(), ctx.Response().Writer)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.String(http.StatusInternalServerError, "failed to render response template")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
36
main.go
Normal file
36
main.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.wittern.io/public/echo-todos/handler"
|
||||||
|
"git.wittern.io/public/echo-todos/pkg/db"
|
||||||
|
"git.wittern.io/public/echo-todos/view"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Username = "My"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
WEBURL := "0.0.0.0:1337"
|
||||||
|
e := echo.New()
|
||||||
|
err := godotenv.Load()
|
||||||
|
Username = os.Getenv("USERNAME")
|
||||||
|
|
||||||
|
view.Username = Username
|
||||||
|
database, err := db.InitDB()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to open database", "Error", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
dbhandler := handler.DBHandler{DB: database}
|
||||||
|
|
||||||
|
handler.AddRoutes(dbhandler, e)
|
||||||
|
e.Start(WEBURL)
|
||||||
|
}
|
9
model/model.go
Normal file
9
model/model.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type Todo struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Done bool `json:"done"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Todos []Todo
|
119
pkg/db/db.go
Normal file
119
pkg/db/db.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.wittern.io/public/echo-todos/model"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *sql.DB
|
||||||
|
|
||||||
|
func InitDB() (*sql.DB, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
todosDB := "./data/todos.db"
|
||||||
|
|
||||||
|
DB, err = sql.Open("sqlite3", todosDB)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("cant open db", "Error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tableExists(DB, "todos") {
|
||||||
|
DB, err = sql.Open("sqlite3", todosDB)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("cant open db", "Error", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CreateTable(DB)
|
||||||
|
}
|
||||||
|
return DB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableExists(db *sql.DB, tableName string) bool {
|
||||||
|
var err error
|
||||||
|
var result string
|
||||||
|
|
||||||
|
query := "SELECT name FROM sqlite_master WHERE type='table' AND name=?;"
|
||||||
|
err = db.QueryRow(query, tableName).Scan(&result)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
fmt.Println("Table does not exist, the database was likely just created.")
|
||||||
|
return false
|
||||||
|
} else if err != nil {
|
||||||
|
slog.Error("unexpected db error", "Error", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Table exists, the database was already initialized.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateTable(db *sql.DB) error {
|
||||||
|
query := `
|
||||||
|
CREATE TABLE IF NOT EXISTS todos (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT,
|
||||||
|
done BOOLEAN
|
||||||
|
);`
|
||||||
|
_, err := db.Exec(query)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertTodo(db *sql.DB, todo model.Todo) (error, int64) {
|
||||||
|
query := `
|
||||||
|
INSERT INTO todos (title, done )
|
||||||
|
VALUES (?, ?);`
|
||||||
|
result, err := db.Exec(query, todo.Title, 0)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to insert todo", "Error", err)
|
||||||
|
return err, 0
|
||||||
|
}
|
||||||
|
todoId, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to get last insert id", "Error", err)
|
||||||
|
return err, 0
|
||||||
|
}
|
||||||
|
return nil, todoId
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListTodos(db *sql.DB) ([]model.Todo, error) {
|
||||||
|
query := `SELECT id, title, done FROM todos;`
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var todos model.Todos
|
||||||
|
for rows.Next() {
|
||||||
|
var todo model.Todo
|
||||||
|
err := rows.Scan(&todo.ID, &todo.Title, &todo.Done)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
todos = append(todos, todo)
|
||||||
|
}
|
||||||
|
return todos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteTodoByID(db *sql.DB, todoID int) error {
|
||||||
|
query := `DELETE FROM todos WHERE id = ?;`
|
||||||
|
_, err := db.Exec(query, todoID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToggleTodoByID(db *sql.DB, id int) ([]model.Todo, error) {
|
||||||
|
query := `UPDATE todos SET done = NOT done WHERE id = ?;`
|
||||||
|
_, err := db.Exec(query, id)
|
||||||
|
if err != nil {
|
||||||
|
return []model.Todo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
todos, err := ListTodos(db)
|
||||||
|
if err != nil {
|
||||||
|
return []model.Todo{}, err
|
||||||
|
}
|
||||||
|
return todos, nil
|
||||||
|
}
|
4
static/css/pico.min.css
vendored
Normal file
4
static/css/pico.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
85
static/css/style.css
Normal file
85
static/css/style.css
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/* body { */
|
||||||
|
/* transform: scale(0.8); /* 150% zoom */ */
|
||||||
|
/* /* transform-origin: 0 0; /* Ensure the scale starts from the top left */ */ */
|
||||||
|
/* } */
|
||||||
|
:root {
|
||||||
|
--pico-font-family-sans-serif: Inter, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif, var(--pico-font-family-emoji);
|
||||||
|
--pico-font-size: 87.5%;
|
||||||
|
/* Original: 100% */
|
||||||
|
--pico-line-height: 1.25;
|
||||||
|
/* Original: 1.5 */
|
||||||
|
--pico-form-element-spacing-vertical: 0.5rem;
|
||||||
|
/* Original: 1rem */
|
||||||
|
--pico-form-element-spacing-horizontal: 1.0rem;
|
||||||
|
/* Original: 1.25rem */
|
||||||
|
--pico-border-radius: 0.375rem;
|
||||||
|
/* Original: 0.25rem */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
:root {
|
||||||
|
--pico-font-size: 87.5%;
|
||||||
|
/* Original: 106.25% */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
:root {
|
||||||
|
--pico-font-size: 87.5%;
|
||||||
|
/* Original: 112.5% */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
:root {
|
||||||
|
--pico-font-size: 87.5%;
|
||||||
|
/* Original: 118.75% */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
:root {
|
||||||
|
--pico-font-size: 87.5%;
|
||||||
|
/* Original: 125% */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1536px) {
|
||||||
|
:root {
|
||||||
|
--pico-font-size: 87.5%;
|
||||||
|
/* Original: 131.25% */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
--pico-font-weight: 600;
|
||||||
|
/* Original: 700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
border: 1px solid var(--pico-muted-border-color);
|
||||||
|
/* Original doesn't have a border */
|
||||||
|
border-radius: calc(var(--pico-border-radius) * 2);
|
||||||
|
/* Original: var(--pico-border-radius) */
|
||||||
|
}
|
||||||
|
|
||||||
|
article>footer {
|
||||||
|
border-radius: calc(var(--pico-border-radius) * 2);
|
||||||
|
/* Original: var(--pico-border-radius) */
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered-div {
|
||||||
|
color: black;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
|
}
|
1
static/js/htmx.min.js
vendored
Normal file
1
static/js/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
20
view/base.templ
Normal file
20
view/base.templ
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package view
|
||||||
|
|
||||||
|
var Username string
|
||||||
|
|
||||||
|
templ Base() {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{ Username } Todos</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<link href="/css/pico.min.css" rel="stylesheet"/>
|
||||||
|
<link href="/css/style.css" rel="stylesheet"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<script src="/js/htmx.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{ children... }
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
66
view/components.templ
Normal file
66
view/components.templ
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.wittern.io/public/echo-todos/model"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ TodoForm() {
|
||||||
|
<form hx-on::after-request="this.reset()" hx-post="/todos" hx-target="#todoslist">
|
||||||
|
<fieldset role="group">
|
||||||
|
<input type="text" name="todo" required/><input value="Clear" type="reset"/>
|
||||||
|
<button type="submit">Add</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TodosList(todos []model.Todo) {
|
||||||
|
<table class="striped" id="todoslist">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Done</th>
|
||||||
|
<th>Delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
for _, todo := range todos {
|
||||||
|
@TodoRow(todo)
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TodoRow(todo model.Todo) {
|
||||||
|
<tr id={ "todo-" + strconv.Itoa(todo.ID) }>
|
||||||
|
<td>{ todo.Title }</td>
|
||||||
|
if todo.Done {
|
||||||
|
<td><input hx-put={ "/todos/todo-" + strconv.Itoa(todo.ID) } hx-target="#todoslist" type="checkbox" name="done" checked/></td>
|
||||||
|
} else {
|
||||||
|
<td><input hx-put={ "/todos/todo-" + strconv.Itoa(todo.ID) } hx-target="#todoslist" type="checkbox" name="done"/></td>
|
||||||
|
}
|
||||||
|
<td><button hx-delete={ "/todos/todo-" + strconv.Itoa(todo.ID) } hx-target={ "#todo-" + strconv.Itoa(todo.ID) } hx-swap="delete">Delete</button></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ ErrorPage(message string) {
|
||||||
|
@Base() {
|
||||||
|
<container>
|
||||||
|
<h1>{ message }</h1>
|
||||||
|
</container>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Hero(AppTitle string) {
|
||||||
|
@Centered() {
|
||||||
|
<div class="hero">
|
||||||
|
<h1>{ AppTitle }</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Centered() {
|
||||||
|
<div id="centered-div" class="container centered">
|
||||||
|
{ children... }
|
||||||
|
</div>
|
||||||
|
}
|
13
view/index.templ
Normal file
13
view/index.templ
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package view
|
||||||
|
|
||||||
|
import "git.wittern.io/public/echo-todos/model"
|
||||||
|
|
||||||
|
templ Index(todos []model.Todo) {
|
||||||
|
@Base() {
|
||||||
|
<main class="container">
|
||||||
|
<h1>{ Username } Todos</h1>
|
||||||
|
@TodoForm()
|
||||||
|
@TodosList(todos)
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue