public setup

This commit is contained in:
Alexilator committed 2024-08-19 14:56:20 +02:00
commit 4fac3fbfa3
18 files changed
+682

No files matched your search

+34
View 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
View 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
View 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
View 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
View File
@@ -0,0 +1,7 @@
services:
todos-app:
image: todos-app
volumes:
- ./data:/app/data
ports:
- "1337:1337"
+24
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File
@@ -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
+85
View 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
View File
@@ -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
+20
View 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 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 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>
}
}