Compare commits
2 Commits
main
...
feature/do
Author | SHA1 | Date | |
---|---|---|---|
94d556cb1b | |||
642a554050 |
@ -1 +0,0 @@
|
||||
DATABASE_URL=postgres://postgres@localhost:5432/todos
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -24,5 +24,3 @@ go.work
|
||||
.direnv/
|
||||
|
||||
tmp/
|
||||
|
||||
.env
|
||||
|
@ -2,8 +2,8 @@ steps:
|
||||
publish-latest:
|
||||
image: woodpeckerci/plugin-kaniko
|
||||
settings:
|
||||
repo: ${CI_REPO_OWNER}/go-todos-app
|
||||
registry: gitea.michaelthomson.dev
|
||||
repo: gitea.michaelthomson.dev/${CI_REPO_OWNER}/go-todos-app
|
||||
registry: https://gitea.michaelthomson.dev
|
||||
tags: latest
|
||||
username: ${CI_REPO_OWNER}
|
||||
password:
|
||||
|
@ -2,8 +2,8 @@ steps:
|
||||
publish-tag:
|
||||
image: woodpeckerci/plugin-kaniko
|
||||
settings:
|
||||
repo: ${CI_REPO_OWNER}/go-todos-app
|
||||
registry: gitea.michaelthomson.dev
|
||||
repo: gitea.michaelthomson.dev/${CI_REPO_OWNER}/go-todos-app
|
||||
registry: https://gitea.michaelthomson.dev
|
||||
tags: ${CI_COMMIT_TAG}
|
||||
username: ${CI_REPO_OWNER}
|
||||
password:
|
||||
|
15
Dockerfile
15
Dockerfile
@ -1,17 +1,10 @@
|
||||
# Build.
|
||||
FROM golang:1.22 AS build-stage
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM golang:1.22-alpine
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . /app
|
||||
COPY . ./
|
||||
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
|
||||
COPY --from=build-stage /app/sql /sql
|
||||
EXPOSE 3000
|
||||
USER nonroot:nonroot
|
||||
ENTRYPOINT ["/entrypoint"]
|
||||
|
@ -1,577 +0,0 @@
|
||||
/*
|
||||
! 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: ;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
5
dev.sh
5
dev.sh
@ -1,5 +0,0 @@
|
||||
air & \
|
||||
tailwindcss \
|
||||
-i './input.css' \
|
||||
-o 'assets/output.css' \
|
||||
--watch
|
@ -33,9 +33,6 @@
|
||||
vscode-langservers-extracted
|
||||
|
||||
air
|
||||
|
||||
tailwindcss
|
||||
tailwindcss-language-server
|
||||
];
|
||||
};
|
||||
});
|
||||
|
14
go.mod
14
go.mod
@ -3,17 +3,3 @@ module michaelthomson.dev/mthomson/go-todos-app
|
||||
go 1.22.1
|
||||
|
||||
require github.com/a-h/templ v0.2.707
|
||||
|
||||
require (
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gofrs/uuid/v5 v5.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
33
go.sum
33
go.sum
@ -1,35 +1,2 @@
|
||||
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
||||
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/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=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2 h1:QWdhlQz98hUe1xmjADOl2mr8ERLrOqj0KWLdkrnNsRQ=
|
||||
github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2/go.mod h1:Ti7pyNDU/UpXKmBTeFgxTvzYDM9xHLiYKMsLdt4b9cg=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -1,41 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"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) Done(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
id, err := uuid.FromString(r.PathValue("id"))
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
todo, err := h.ts.GetTodoById(id)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
todo, err = h.ts.UpdateTodo(id, todo.Name, true)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = partials.Todo(todo).Render(r.Context(), w)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TodoHandler) Undone(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
id, err := uuid.FromString(r.PathValue("id"))
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
todo, err := h.ts.GetTodoById(id)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
todo, err = h.ts.UpdateTodo(id, todo.Name, false)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = partials.Todo(todo).Render(r.Context(), w)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
id, err := uuid.FromString(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
|
||||
}
|
||||
}
|
127
main.go
127
main.go
@ -1,137 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
pgxuuid "github.com/jackc/pgx-gofrs-uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/joho/godotenv"
|
||||
"michaelthomson.dev/mthomson/go-todos-app/handlers"
|
||||
"michaelthomson.dev/mthomson/go-todos-app/middleware"
|
||||
"michaelthomson.dev/mthomson/go-todos-app/repositories"
|
||||
"michaelthomson.dev/mthomson/go-todos-app/services"
|
||||
"github.com/a-h/templ"
|
||||
"michaelthomson.dev/mthomson/go-todos-app/views"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// load environment
|
||||
err := godotenv.Load()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error loading .env file: %v", err)
|
||||
}
|
||||
|
||||
databaseUrl := os.Getenv("DATABASE_URL")
|
||||
|
||||
// connect to Db
|
||||
dbconfig, err := pgxpool.ParseConfig(databaseUrl)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to parse db config: %v", err)
|
||||
}
|
||||
|
||||
dbconfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
|
||||
pgxuuid.Register(conn.TypeMap())
|
||||
return nil
|
||||
}
|
||||
|
||||
dbpool, err := pgxpool.New(context.Background(), databaseUrl)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create connection pool: %v", err)
|
||||
}
|
||||
defer dbpool.Close()
|
||||
|
||||
// apply initial sql
|
||||
log.Print("Applying migrations table")
|
||||
content, err := os.ReadFile(filepath.Join("sql", "init.sql"))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = dbpool.Exec(context.Background(), string(content))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
//apply migrations
|
||||
log.Print("Starting migrations...")
|
||||
files, err := os.ReadDir("./sql/migrations")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
path := filepath.Join("sql", "migrations", file.Name())
|
||||
content, err := os.ReadFile(path)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var count int
|
||||
err = dbpool.QueryRow(context.Background(), "SELECT COUNT(1) FROM migration WHERE name=$1", file.Name()).Scan(&count)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
log.Printf("Running migration: %s", file.Name())
|
||||
_, err = dbpool.Exec(context.Background(), string(content))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = dbpool.Exec(context.Background(), "INSERT INTO migration(name) values($1)", file.Name())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Migration already exists: %s", file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
log.Print("Migrations complete")
|
||||
|
||||
// set up repositories
|
||||
todoRepository := repositories.NewPostgresTodoRepository(dbpool)
|
||||
|
||||
// set up services
|
||||
todoService := services.NewTodoService(&todoRepository)
|
||||
|
||||
// set up handlers
|
||||
homeHandler := handlers.NewHomeHandler(todoService)
|
||||
todoHandler := handlers.NewTodoHandler(todoService)
|
||||
|
||||
router := http.NewServeMux()
|
||||
|
||||
// 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)
|
||||
router.HandleFunc("PATCH /todos/{id}/done", todoHandler.Done)
|
||||
router.HandleFunc("PATCH /todos/{id}/undone", todoHandler.Undone)
|
||||
home := views.Home()
|
||||
router.Handle("GET /", templ.Handler(home))
|
||||
|
||||
server := http.Server{
|
||||
Addr: ":3000",
|
||||
Handler: middleware.LoggingMiddleware(router),
|
||||
Addr: "localhost:3000",
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
log.Fatal(server.ListenAndServe())
|
||||
|
@ -1,31 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type wrappedWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w * wrappedWriter) WriteHeader(statusCode int) {
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
w.statusCode = statusCode
|
||||
}
|
||||
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
wrapped := &wrappedWriter{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
next.ServeHTTP(wrapped, r)
|
||||
log.Println(wrapped.statusCode, r.Method, r.URL.Path, time.Since(start))
|
||||
})
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package models
|
||||
|
||||
import "github.com/gofrs/uuid/v5"
|
||||
|
||||
type Todo struct {
|
||||
Id uuid.UUID
|
||||
Name string
|
||||
Done bool
|
||||
}
|
||||
|
||||
func NewTodo(id uuid.UUID, name string, done bool) Todo {
|
||||
return Todo{
|
||||
Id: id,
|
||||
Name: name,
|
||||
Done: done,
|
||||
}
|
||||
}
|
||||
|
@ -1,61 +0,0 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"michaelthomson.dev/mthomson/go-todos-app/models"
|
||||
)
|
||||
|
||||
type InMemoryTodoRepository struct {
|
||||
Todos []models.Todo
|
||||
}
|
||||
|
||||
func (ts *InMemoryTodoRepository) List() (todo []models.Todo, err error) {
|
||||
return ts.Todos, nil
|
||||
}
|
||||
|
||||
func (ts *InMemoryTodoRepository) 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 *InMemoryTodoRepository) Add(todo models.Todo) (addedTodo models.Todo, err error) {
|
||||
ts.Todos = append(ts.Todos, todo)
|
||||
return todo, nil
|
||||
}
|
||||
|
||||
func (ts *InMemoryTodoRepository) 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 NewInMemoryTodoRepository() InMemoryTodoRepository {
|
||||
return InMemoryTodoRepository{
|
||||
Todos: []models.Todo{},
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"michaelthomson.dev/mthomson/go-todos-app/models"
|
||||
)
|
||||
|
||||
type PostgresTodoRepository struct {
|
||||
db *pgxpool.Pool
|
||||
}
|
||||
|
||||
func (ts *PostgresTodoRepository) List() ([]models.Todo, error) {
|
||||
var todos []models.Todo
|
||||
rows, err := ts.db.Query(context.Background(), "SELECT id, name, done FROM todo")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Database error: %v", err)
|
||||
return todos, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var r models.Todo
|
||||
err := rows.Scan(&r.Id, &r.Name, &r.Done)
|
||||
if err != nil {
|
||||
return todos, err
|
||||
}
|
||||
todos = append(todos, r)
|
||||
}
|
||||
|
||||
return todos, err
|
||||
}
|
||||
|
||||
func (ts *PostgresTodoRepository) Get(id uuid.UUID) (models.Todo, error) {
|
||||
var todo models.Todo
|
||||
err := ts.db.QueryRow(context.Background(), "SELECT id, name, done FROM todo WHERE id=$1", id).Scan(&todo.Id, &todo.Name, &todo.Done)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Database error: %v", err)
|
||||
return todo, err
|
||||
}
|
||||
|
||||
return todo, err
|
||||
}
|
||||
|
||||
func (ts *PostgresTodoRepository) Update(id uuid.UUID, name string, done bool) error {
|
||||
_, err := ts.db.Exec(context.Background(), "UPDATE todo SET name=$1, done=$2 WHERE id=$3", name, done, id)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Database error: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (ts *PostgresTodoRepository) Add(todo models.Todo) error {
|
||||
_, err := ts.db.Exec(context.Background(), "INSERT INTO todo(id, name, done) values($1, $2, $3)", todo.Id, todo.Name, todo.Done)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Database error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (ts *PostgresTodoRepository) Delete(id uuid.UUID) error {
|
||||
_, err := ts.db.Exec(context.Background(), "DELETE FROM todo where id=$1", id)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Database error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewPostgresTodoRepository(db *pgxpool.Pool) PostgresTodoRepository {
|
||||
return PostgresTodoRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"michaelthomson.dev/mthomson/go-todos-app/models"
|
||||
)
|
||||
|
||||
type Db interface {
|
||||
List() ([]models.Todo, error)
|
||||
Get(id uuid.UUID) (models.Todo, error)
|
||||
Add(todo models.Todo) error
|
||||
Delete(id uuid.UUID) error
|
||||
Update(id uuid.UUID, name string, done bool) error
|
||||
}
|
||||
|
||||
type TodoService struct {
|
||||
db Db
|
||||
}
|
||||
|
||||
func NewTodoService(db Db) *TodoService {
|
||||
return &TodoService{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TodoService) AddTodo(name string) (models.Todo, error) {
|
||||
if name == "" {
|
||||
return models.Todo{}, fmt.Errorf("Must provide a name")
|
||||
}
|
||||
|
||||
id, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return models.Todo{}, err
|
||||
}
|
||||
|
||||
err = ts.db.Add(models.NewTodo(id, name, false))
|
||||
|
||||
if err != nil {
|
||||
return models.Todo{}, err
|
||||
}
|
||||
|
||||
return ts.db.Get(id)
|
||||
}
|
||||
|
||||
func (ts *TodoService) UpdateTodo(id uuid.UUID, name string, done bool) (models.Todo, error) {
|
||||
err := ts.db.Update(id, name, done)
|
||||
|
||||
if err != nil {
|
||||
return models.Todo{}, err
|
||||
}
|
||||
|
||||
return ts.db.Get(id)
|
||||
}
|
||||
|
||||
func (ts *TodoService) GetTodos() ([]models.Todo, error) {
|
||||
return ts.db.List()
|
||||
}
|
||||
|
||||
func (ts *TodoService) GetTodoById(id uuid.UUID) (models.Todo, error) {
|
||||
return ts.db.Get(id)
|
||||
}
|
||||
|
||||
func (ts *TodoService) DeleteTodoById(id uuid.UUID) error {
|
||||
return ts.db.Delete(id)
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package services
|
||||
|
||||
type DbMock struct {}
|
@ -1,5 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS migration (
|
||||
id serial primary key not null,
|
||||
name text not null,
|
||||
applied_at timestamp not null default now()
|
||||
);
|
@ -1,5 +0,0 @@
|
||||
CREATE TABLE todo (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
done BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
@ -1,9 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [ "./**/*.html", "./**/*.templ" ],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
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 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>
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
// 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 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
|
||||
})
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package partials
|
||||
|
||||
import "michaelthomson.dev/mthomson/go-todos-app/models"
|
||||
import "fmt"
|
||||
|
||||
func todoId(todo models.Todo) string {
|
||||
return "todo-" + todo.Id.String()
|
||||
}
|
||||
|
||||
func patchUrl(todo models.Todo) string {
|
||||
if todo.Done {
|
||||
return string(templ.URL(fmt.Sprintf("/todos/%s/undone", todo.Id)))
|
||||
}
|
||||
return string(templ.URL(fmt.Sprintf("/todos/%s/done", todo.Id)))
|
||||
}
|
||||
|
||||
templ Todo(todo models.Todo) {
|
||||
<div id={ todoId(todo) }>
|
||||
{ todo.Name }
|
||||
<button hx-delete={ string(templ.URL(fmt.Sprintf("/todos/%s", todo.Id))) } hx-target={ "#" + todoId(todo) } hx-swap="delete">Delete</button>
|
||||
<input type="checkbox" checked?={ todo.Done } hx-target={ "#" + todoId(todo) } hx-patch={ patchUrl(todo) } hx-swap="outerHTML" />
|
||||
</div>
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
// 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"
|
||||
import "fmt"
|
||||
|
||||
func todoId(todo models.Todo) string {
|
||||
return "todo-" + todo.Id.String()
|
||||
}
|
||||
|
||||
func patchUrl(todo models.Todo) string {
|
||||
if todo.Done {
|
||||
return string(templ.URL(fmt.Sprintf("/todos/%s/undone", todo.Id)))
|
||||
}
|
||||
return string(templ.URL(fmt.Sprintf("/todos/%s/done", todo.Id)))
|
||||
}
|
||||
|
||||
func Todo(todo models.Todo) templ.Component {
|
||||
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: 18, 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: 19, 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(string(templ.URL(fmt.Sprintf("/todos/%s", todo.Id))))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 20, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
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: 20, Col: 109}
|
||||
}
|
||||
_, 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> <input type=\"checkbox\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if todo.Done {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("#" + todoId(todo))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 21, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-patch=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(patchUrl(todo))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 21, Col: 108}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"outerHTML\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
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>
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
// 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
|
||||
})
|
||||
}
|
5
views/home.templ
Normal file
5
views/home.templ
Normal file
@ -0,0 +1,5 @@
|
||||
package views
|
||||
|
||||
templ Home() {
|
||||
<div>Home page test again</div>
|
||||
}
|
35
views/home_templ.go
Normal file
35
views/home_templ.go
Normal file
@ -0,0 +1,35 @@
|
||||
// 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
|
||||
})
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user