public setup
This commit is contained in:
commit
4fac3fbfa3
18 files changed
+682
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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"]
|
||||
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
todos-app:
|
||||
image: todos-app
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
ports:
|
||||
- "1337:1337"
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
@charset "UTF-8";/*!
|
||||
* Pico CSS ✨ v2.0.6 (https://picocss.com)
|
||||
* Copyright 2019-2024 - Licensed under MIT
|
||||
*/:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:root{--pico-font-size:106.25%}}@media (min-width:768px){:root{--pico-font-size:112.5%}}@media (min-width:1024px){:root{--pico-font-size:118.75%}}@media (min-width:1280px){:root{--pico-font-size:125%}}@media (min-Line truncated
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:F,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Br,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.12"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t){return new RegExp("<"+e+"(\\s[^>]*>|>)([\\s\\S]*?)<\\/"+e+">",!!t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function s(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/<body/.test(e)}function l(e){var t=!N(e);var r=A(e);var n=e;if(r==="head"){n=n.replace(S,"")}if(Q.config.useTemplateFragments&&t){var i=s("<body><template>"+n+"</template></body>",0);var a=i.querySelector("template").content;if(Q.config.allowScriptTags){oe(a.querySelectorAll("script"),function(e){if(Q.config.inlineScriptNonce){e.nonce=Q.config.inlineScriptNonce}e.htmxExecuted=navigator.userAgent.indexOf("Firefox")===-1})}else{oe(a.querySelectorAll("script"),function(e){_(e)})}return a}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return s("<table>"+n+"</table>",1);case"col":return s("<table><colgroup>"+n+"</colgroup></table>",2);case"tr":return s("<table><tbody>"+n+"</tbody></table>",2);case"td":case"th":return s("<table><tbody><tr>"+n+"</tr></tbody></table>",3);case"script":case"style":return s("<div>"+n+"</div>",1);default:return s(n,0)}}function ie(e){if(e){e()}}function I(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return I(e,"Function")}function P(e){return I(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function M(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function oe(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function X(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function se(e){ifLine truncated
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user