8 Commits

Author SHA1 Message Date
088365c411 update
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
2024-06-17 10:01:23 -04:00
09a957ca4b db, handlers, and interfaces
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
2024-06-15 21:33:14 -04:00
3d29dadaf3 update: service, handlers, and db separation
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
2024-06-14 17:49:56 -04:00
44feca12d2 tailwind and docker update
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
2024-06-09 16:21:41 -04:00
2733d2c19e update flake with tailwind
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
2024-06-09 14:47:43 -04:00
b45523f3d8 fix repo again
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
ci/woodpecker/tag/build Pipeline was successful
ci/woodpecker/tag/dryrun Pipeline was successful
ci/woodpecker/tag/publish-latest Pipeline was successful
ci/woodpecker/tag/publish-tag Pipeline was successful
2024-06-08 23:34:48 -04:00
8efc682844 fix registry url
Some checks failed
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 failed
2024-06-08 23:26:44 -04:00
23ac1417be docker and CI (#1)
Some checks failed
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 failed
Reviewed-on: #1
Co-authored-by: Michael Thomson <michael@michaelthomson.dev>
Co-committed-by: Michael Thomson <michael@michaelthomson.dev>
2024-06-09 03:23:09 +00:00
25 changed files with 1138 additions and 52 deletions

View File

@@ -2,8 +2,8 @@ steps:
publish-latest:
image: woodpeckerci/plugin-kaniko
settings:
repo: gitea.michaelthomson.dev/${CI_REPO_OWNER}/go-todos-app
registry: https://gitea.michaelthomson.dev
repo: ${CI_REPO_OWNER}/go-todos-app
registry: gitea.michaelthomson.dev
tags: latest
username: ${CI_REPO_OWNER}
password:

View File

@@ -2,8 +2,8 @@ steps:
publish-tag:
image: woodpeckerci/plugin-kaniko
settings:
repo: gitea.michaelthomson.dev/${CI_REPO_OWNER}/go-todos-app
registry: https://gitea.michaelthomson.dev
repo: ${CI_REPO_OWNER}/go-todos-app
registry: gitea.michaelthomson.dev
tags: ${CI_COMMIT_TAG}
username: ${CI_REPO_OWNER}
password:

View File

@@ -1,10 +1,16 @@
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine
# Build.
FROM golang:1.22 AS build-stage
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
COPY . /app
RUN CGO_ENABLED=0 GOOS=linux go build -o /entrypoint
# Deploy.
FROM gcr.io/distroless/static-debian11 AS release-stage
WORKDIR /
COPY --from=build-stage /entrypoint /entrypoint
COPY --from=build-stage /app/assets /assets
EXPOSE 3000
USER nonroot:nonroot
ENTRYPOINT ["/entrypoint"]

589
assets/output.css Normal file
View File

@@ -0,0 +1,589 @@
/*
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
.static {
position: static;
}
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.content-center {
align-content: center;
}
.items-center {
align-items: center;
}
.border {
border-width: 1px;
}
.border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0 / var(--tw-border-opacity));
}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}

61
db/db.go Normal file
View File

@@ -0,0 +1,61 @@
package db
import (
"fmt"
"github.com/google/uuid"
"michaelthomson.dev/mthomson/go-todos-app/models"
)
type TodosStore struct {
Todos []models.Todo
}
func (ts *TodosStore) List() (todo []models.Todo, err error) {
return ts.Todos, nil
}
func (ts *TodosStore) Get(id uuid.UUID) (todo models.Todo, err error) {
index := -1
for i, todo := range ts.Todos {
if id == todo.Id {
index = i
}
}
if index == -1 {
return models.Todo{}, fmt.Errorf("Could not find todo by id %q", id)
}
ts.Todos = append(ts.Todos[:index], ts.Todos[index + 1:]...)
return ts.Todos[index], nil
}
func (ts *TodosStore) Add(todo models.Todo) (addedTodo models.Todo, err error) {
ts.Todos = append(ts.Todos, todo)
return todo, nil
}
func (ts *TodosStore) Delete(id uuid.UUID) (err error) {
index := -1
for i, todo := range ts.Todos {
if id == todo.Id {
index = i
}
}
if index == -1 {
return fmt.Errorf("Could not find todo by id %q", id)
}
ts.Todos = append(ts.Todos[:index], ts.Todos[index + 1:]...)
return nil
}
func NewTodoStore() TodosStore {
return TodosStore{
Todos: []models.Todo{},
}
}

5
dev.sh Executable file
View File

@@ -0,0 +1,5 @@
air & \
tailwindcss \
-i './input.css' \
-o 'assets/output.css' \
--watch

View File

@@ -33,6 +33,9 @@
vscode-langservers-extracted
air
tailwindcss
tailwindcss-language-server
];
};
});

2
go.mod
View File

@@ -3,3 +3,5 @@ module michaelthomson.dev/mthomson/go-todos-app
go 1.22.1
require github.com/a-h/templ v0.2.707
require github.com/google/uuid v1.6.0 // indirect

4
go.sum
View File

@@ -1,2 +1,6 @@
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

41
handlers/homeHandler.go Normal file
View File

@@ -0,0 +1,41 @@
package handlers
import (
"net/http"
"michaelthomson.dev/mthomson/go-todos-app/services"
"michaelthomson.dev/mthomson/go-todos-app/templates/pages"
)
type HomeHandler struct {
ts *services.TodoService
}
func NewHomeHandler(ts *services.TodoService) HomeHandler {
return HomeHandler{ts: ts}
}
func (h *HomeHandler) Home(w http.ResponseWriter, r *http.Request) {
var err error
todos, err := h.ts.GetTodos()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
err = pages.Home(todos).Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *HomeHandler) Router() http.Handler {
router := http.NewServeMux()
router.HandleFunc("/", h.Home)
return router
}

61
handlers/todoHandler.go Normal file
View File

@@ -0,0 +1,61 @@
package handlers
import (
"net/http"
"github.com/google/uuid"
"michaelthomson.dev/mthomson/go-todos-app/services"
"michaelthomson.dev/mthomson/go-todos-app/templates/partials"
)
type TodoHandler struct {
ts *services.TodoService
}
func NewTodoHandler(ts *services.TodoService) TodoHandler {
return TodoHandler{ts: ts}
}
func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
var err error
err = r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
name := r.FormValue("name")
addedTodo, err := h.ts.AddTodo(name)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = partials.Todo(addedTodo).Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
var err error
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = h.ts.DeleteTodoById(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

3
input.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

20
main.go
View File

@@ -4,15 +4,27 @@ import (
"log"
"net/http"
"github.com/a-h/templ"
"michaelthomson.dev/mthomson/go-todos-app/views"
"michaelthomson.dev/mthomson/go-todos-app/db"
"michaelthomson.dev/mthomson/go-todos-app/handlers"
"michaelthomson.dev/mthomson/go-todos-app/services"
)
func main() {
todosStore := db.NewTodoStore()
ts := services.NewTodoService(&todosStore)
homeHandler := handlers.NewHomeHandler(ts)
todoHandler := handlers.NewTodoHandler(ts)
router := http.NewServeMux()
home := views.Home()
router.Handle("GET /", templ.Handler(home))
// Serve static files
fs := http.FileServer(http.Dir("./assets"))
router.Handle("GET /assets/", http.StripPrefix("/assets/", fs))
router.HandleFunc("GET /{$}", homeHandler.Home)
router.HandleFunc("POST /todos", todoHandler.Create)
router.HandleFunc("DELETE /todos/{id}", todoHandler.Delete)
server := http.Server{
Addr: "localhost:3000",

18
models/todo.go Normal file
View File

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

44
services/todoService.go Normal file
View File

@@ -0,0 +1,44 @@
package services
import (
"fmt"
"github.com/google/uuid"
"michaelthomson.dev/mthomson/go-todos-app/models"
)
type Db interface {
List() (todo []models.Todo, err error)
Get(id uuid.UUID) (todo models.Todo, err error)
Add(todo models.Todo) (addedTodo models.Todo, err error)
Delete(id uuid.UUID) (err error)
}
type TodoService struct {
db Db
}
func NewTodoService(db Db) *TodoService {
return &TodoService{
db: db,
}
}
func (ts *TodoService) AddTodo(name string) (todo models.Todo, err error) {
if name == "" {
return models.Todo{}, fmt.Errorf("Must provide a name")
}
return ts.db.Add(models.NewTodo(name, false))
}
func (ts *TodoService) GetTodos() (todos []models.Todo, err error) {
return ts.db.List()
}
func (ts *TodoService) GetTodoById(id uuid.UUID) (todo models.Todo, err error) {
return ts.db.Get(id)
}
func (ts *TodoService) DeleteTodoById(id uuid.UUID) (err error) {
return ts.db.Delete(id)
}

View File

@@ -0,0 +1,3 @@
package services
type DbMock struct {}

9
tailwind.config.js Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [ "./**/*.html", "./**/*.templ", "./**/*.go", ],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,22 @@
package pages
import "michaelthomson.dev/mthomson/go-todos-app/templates/shared"
import "michaelthomson.dev/mthomson/go-todos-app/templates/partials"
import "michaelthomson.dev/mthomson/go-todos-app/models"
templ Home(todos []models.Todo) {
@shared.Page("Todos") {
<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">
for _, todo := range todos {
@partials.Todo(todo)
}
</div>
</div>
}
}

View File

@@ -0,0 +1,64 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.663
package pages
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "michaelthomson.dev/mthomson/go-todos-app/templates/shared"
import "michaelthomson.dev/mthomson/go-todos-app/templates/partials"
import "michaelthomson.dev/mthomson/go-todos-app/models"
func Home(todos []models.Todo) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
_, 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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, todo := range todos {
templ_7745c5c3_Err = partials.Todo(todo).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = shared.Page("Todos").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@@ -0,0 +1,14 @@
package partials
import "michaelthomson.dev/mthomson/go-todos-app/models"
func todoId(todo models.Todo) string {
return "todo-" + todo.Id.String()
}
templ Todo(todo models.Todo) {
<div id={ todoId(todo) }>
{ todo.Name }
<button hx-delete={ "/todos/" + todo.Id.String() } hx-target={ "#" + todoId(todo) } hx-swap="delete">Delete</button>
</div>
}

View File

@@ -0,0 +1,93 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.663
package partials
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "michaelthomson.dev/mthomson/go-todos-app/models"
func todoId(todo models.Todo) string {
return "todo-" + todo.Id.String()
}
func Todo(todo models.Todo) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, 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: 10, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 11, Col: 15}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <button hx-delete=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("/todos/" + todo.Id.String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 12, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, 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: 12, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"delete\">Delete</button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@@ -0,0 +1,16 @@
package shared
templ Page(title string) {
<html>
<head>
<meta charset="UTF-8"></meta>
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<link rel="stylesheet" href="/assets/output.css"></link>
<title>{title}</title>
</head>
<body>
{ children... }
</body>
</html>
}

View File

@@ -0,0 +1,56 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.663
package shared
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func Page(title string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><script src=\"https://unpkg.com/htmx.org@1.9.11\"></script><link rel=\"stylesheet\" href=\"/assets/output.css\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/shared/page.templ`, Line: 10, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title></head><body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@@ -1,5 +0,0 @@
package views
templ Home() {
<div>Home page test again</div>
}

View File

@@ -1,35 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.663
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func Home() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div>Home page test again</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}