13 Commits

Author SHA1 Message Date
a6794cdfed feat(deps): update go compiler to 1.24
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m35s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m16s
2025-03-04 12:17:17 +01:00
38b3ad9326 fix(deps): update module golang.org/x/net to v0.35.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m4s
2025-03-03 10:02:17 +00:00
a6f5710521 chore(deps): update node.js to v22.14.0
Some checks failed
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-03-03 09:59:59 +00:00
cb0252e1af chore(deps): update tailwindcss monorepo to v4.0.9
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 2m48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-03-03 00:06:37 +00:00
f2937a762e chore(deps): update debian:12.9 docker digest to 3528682
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m28s
2025-03-02 18:24:21 +00:00
60daac48b4 fix(deps): update module github.com/prometheus/client_golang to v1.21.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 11m2s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m24s
2025-02-24 18:28:38 +00:00
b2a655f73a chore(deps): update tailwindcss monorepo to v4.0.8
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 9m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 10m23s
2025-02-24 16:18:54 +00:00
663081d719 chore(deps): update node.js to 5145c88
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m39s
2025-02-24 12:34:43 +00:00
28460a6bac fix: make tests more resilient
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m15s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 8m28s
2025-02-24 12:58:48 +01:00
3039d66295 feat(docs): update readme
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 2m54s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 2m16s
2025-02-23 21:46:31 +01:00
9b96e8f0a5 chore(deps): update debian:12.9 docker digest to 4abf773
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m6s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m58s
2025-02-11 14:09:06 +00:00
b86b737a82 chore(deps): update golang:1.23.5 docker digest to e213430
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m14s
2025-02-09 00:11:23 +00:00
f2951985c2 fix(deps): migrate tailwindcss to v4 and remove daisyui
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m37s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m47s
2025-02-03 23:18:51 +01:00
17 changed files with 1037 additions and 1487 deletions

View File

@@ -1,6 +1,6 @@
FROM golang:1.23.5@sha256:8c10f21bec412f08f73aa7b97ca5ac5f28a39d8a88030ad8a339fd0a781d72b4 AS builder_go
FROM golang:1.24.0 AS builder_go
WORKDIR /web-app-template
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.62.2
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
RUN go install github.com/a-h/templ/cmd/templ@latest
RUN go install github.com/vektra/mockery/v2@latest
COPY go.mod go.sum ./
@@ -13,7 +13,7 @@ RUN golangci-lint run ./...
RUN go build -o /web-app-template/web-app-template .
FROM node:22.13.1@sha256:ae2f3d4cc65d251352eca01ba668824f651a2ee4d2a37e2efb22649521a483fd AS builder_node
FROM node:22.14.0@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95 AS builder_node
WORKDIR /web-app-template
COPY package.json package-lock.json ./
RUN npm clean-install
@@ -21,7 +21,7 @@ COPY . ./
RUN npm run build
FROM debian:12.9@sha256:321341744acb788e251ebd374aecc1a42d60ce65da7bd4ee9207ff6be6686a62
FROM debian:12.9@sha256:35286826a88dc879b4f438b645ba574a55a14187b483d09213a024dc0c0a64ed
WORKDIR /web-app-template
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
COPY migration ./migration

View File

@@ -7,11 +7,11 @@ A basic template with authentication to easily host on a VPC.
This template includes everything essential to build an app. It includes the following features:
- Authentication: Users can login, logout, register and reset their password. For increased security TOTP is available aswell.
- Authentication: Users can login, logout, register and reset their password. (for increased security TOTP is planned aswell.)
- Observability: The stack contains an Grafana+Prometheus instance for basic monitoring. You are able to add alerts and get notified on your phone.
- Mail: You are able to send mail with SMTP. You still need an external Mail Server, but a guide on how to set that up with a custom domain is included.
- SSL: This is included by using traefik as reverse proxy. It handles SSL certificates automatically. Furthermore all services are accessible through subdomains. Best thing is, you can add your more with 3 lines of code
- Actual Stack: Tailwindcss + HTMX + DaisyUI + GO Backend with templ and sqlite
- SSL: This is included by using traefik as reverse proxy. It handles SSL certificates automatically. Furthermore all services are accessible through subdomains.
- Stack: Tailwindcss + HTMX + GO Backend with templ and sqlite
## Architecture Design Decisions
@@ -51,13 +51,13 @@ Instead of implementing authentication from scratch, an external OAuth2 provider
Pros:
- The Systems of BigTech are probably safer. They have security experts employed.
- The other external system needs to prevent credential stuffing attacks, etc.
- The other external system is responsible to prevent credential stuffing attacks, etc.
- Users don't have to create new credentials
Cons:
- High dependency on those providers
- Single Point of failure (If your account is banned, your application access get's lost as well)
- It's possible that these providers ban the whole application
- There still needs to be implemented some logic server side
- Single Point of failure (If your account is banned, your application access get's lost as well.)
- It's possible that these providers ban the whole application (All users lose access)
- There still needs to be implemented some logic
- Full application integration can be difficult
#### 3. Using OAuth2 with Keycloak

14
go.mod
View File

@@ -10,10 +10,10 @@ require (
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.24
github.com/prometheus/client_golang v1.20.5
github.com/prometheus/client_golang v1.21.0
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.32.0
golang.org/x/net v0.34.0
golang.org/x/crypto v0.33.0
golang.org/x/net v0.35.0
)
require (
@@ -22,15 +22,15 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/sys v0.29.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/sys v0.30.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

28
go.sum
View File

@@ -19,8 +19,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -35,12 +35,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
@@ -51,14 +51,14 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

18
input.css Normal file
View File

@@ -0,0 +1,18 @@
@import 'tailwindcss';
@source './static/**/*.js';
@source './template/**/*.templ';
@theme {
--animate-fade: fadeOut 0.25s ease-in;
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}

View File

@@ -333,7 +333,7 @@ func TestIntegrationAuth(t *testing.T) {
resp, err = httpClient.Do(req)
timeEnd := time.Now()
assert.Nil(t, err)
if timeEnd.Sub(timeStart) > 253*time.Millisecond || timeEnd.Sub(timeStart) <= 250*time.Millisecond {
if timeEnd.Sub(timeStart) > 260*time.Millisecond || timeEnd.Sub(timeStart) <= 250*time.Millisecond {
t.Fail()
t.Logf("Time did not match: %v", timeEnd.Sub(timeStart))
}
@@ -367,7 +367,7 @@ func TestIntegrationAuth(t *testing.T) {
resp, err = httpClient.Do(req)
timeEnd = time.Now()
assert.Nil(t, err)
if timeEnd.Sub(timeStart) > 253*time.Millisecond || timeEnd.Sub(timeStart) <= 250*time.Millisecond {
if timeEnd.Sub(timeStart) > 260*time.Millisecond || timeEnd.Sub(timeStart) <= 250*time.Millisecond {
t.Fail()
t.Logf("Time did not match: %v", timeEnd.Sub(timeStart))
}
@@ -401,7 +401,7 @@ func TestIntegrationAuth(t *testing.T) {
resp, err = httpClient.Do(req)
timeEnd = time.Now()
assert.Nil(t, err)
if timeEnd.Sub(timeStart) > 253*time.Millisecond || timeEnd.Sub(timeStart) <= 250*time.Millisecond {
if timeEnd.Sub(timeStart) > 260*time.Millisecond || timeEnd.Sub(timeStart) <= 250*time.Millisecond {
t.Fail()
t.Logf("Time did not match: %v", timeEnd.Sub(timeStart))
}
@@ -571,7 +571,7 @@ func TestIntegrationAuth(t *testing.T) {
timeEnd := time.Now()
assert.Nil(t, err)
timeTaken := timeEnd.Sub(timeStart)
assert.LessOrEqual(t, timeTaken, 253*time.Millisecond)
assert.LessOrEqual(t, timeTaken, 260*time.Millisecond)
assert.GreaterOrEqual(t, timeTaken, 250*time.Millisecond)
assert.Equal(t, http.StatusOK, resp.StatusCode)
@@ -608,7 +608,7 @@ func TestIntegrationAuth(t *testing.T) {
timeEnd := time.Now()
assert.Nil(t, err)
timeTaken := timeEnd.Sub(timeStart)
assert.LessOrEqual(t, timeTaken, 253*time.Millisecond)
assert.LessOrEqual(t, timeTaken, 260*time.Millisecond)
assert.GreaterOrEqual(t, timeTaken, 250*time.Millisecond)
assert.Equal(t, http.StatusOK, resp.StatusCode)

2069
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,16 +4,15 @@
"description": "Your (almost) independent tech stack to host on a VPC.",
"main": "index.js",
"scripts": {
"build": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss build -o static/css/tailwind.css --minify",
"watch": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss build -o static/css/tailwind.css --watch",
"test": ""
"build": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --minify",
"watch": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"htmx.org": "2.0.4",
"tailwindcss": "3.4.17",
"daisyui": "4.12.23"
"tailwindcss": "4.0.9",
"@tailwindcss/cli": "4.0.9"
}
}

View File

@@ -1,26 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./template/**/*.templ", "./static/**/*.js"],
theme: {
extend: {
animation: {
fade: 'fadeOut 0.25s ease-in',
},
keyframes: _ => ({
fadeOut: {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
}),
},
},
plugins: [
require('daisyui'),
],
daisyui: {
themes: ["retro"],
},
}

View File

@@ -12,7 +12,7 @@ templ DeleteAccountComp() {
<p class="text-xl text-red-500 mb-4">
Are you sure you want to delete your account? This action is irreversible.
</p>
<label class="input input-bordered flex items-center gap-2">
<label class="flex items-center gap-2">
<input
type="password"
class="grow"
@@ -24,7 +24,7 @@ templ DeleteAccountComp() {
autocapitalize="off"
/>
</label>
<button class="btn btn-error self-end">
<button class="self-end">
Delete Account
</button>
</form>

View File

@@ -1,30 +1,30 @@
package auth
templ UserComp(user string) {
<div id="user-info" class="flex gap-5 items-center">
if user != "" {
<div class="group inline-block relative">
<button class="font-semibold py-2 px-4 inline-flex items-center">
<span class="mr-1">{ user }</span>
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"></path>
</svg>
</button>
<div class="absolute hidden group-hover:block w-full">
<ul class="menu bg-base-300 rounded-box w-fit float-right mr-4 p-3">
<li class="mb-1">
<a hx-post="/api/auth/signout" hx-target="#user-info">Sign Out</a>
</li>
<li class="mb-1">
<a href="/auth/change-password">Change Password</a>
</li>
<li><a href="/auth/delete-account" class="text-error">Delete Account</a></li>
</ul>
</div>
<div id="user-info" class="flex gap-5 items-center">
if user != "" {
<div class="inline-block relative">
<button class="font-semibold py-2 px-4 inline-flex items-center">
<span class="mr-1">{ user }</span>
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"></path>
</svg>
</button>
<div class="absolute hidden group-hover:block w-full">
<ul class="w-fit float-right mr-4 p-3">
<li class="mb-1">
<a hx-post="/api/auth/signout" hx-target="#user-info">Sign Out</a>
</li>
<li class="mb-1">
<a href="/auth/change-password">Change Password</a>
</li>
<li><a href="/auth/delete-account" class="">Delete Account</a></li>
</ul>
</div>
</div>
} else {
<a href="/auth/signup" class="">Sign Up</a>
<a href="/auth/signin" class="">Sign In</a>
}
</div>
} else {
<a href="/auth/signup" class="btn btn-sm">Sign Up</a>
<a href="/auth/signin" class="btn btn-sm">Sign In</a>
}
</div>
}

View File

@@ -12,7 +12,7 @@ templ VerifyComp() {
<p class="text-lg text-center">
Please check your inbox/spam and click on the link to verify your account.
</p>
<button class="btn mt-8" hx-get="/api/auth/verify-resend" hx-sync="this:drop" hx-swap="outerHTML">
<button class="mt-8" hx-get="/api/auth/verify-resend" hx-sync="this:drop" hx-swap="outerHTML">
resend verification email
</button>
</div>

View File

@@ -1,29 +1,29 @@
package auth
templ VerifyResponseComp(isVerified bool) {
<main>
<div class="flex flex-col items-center justify-center h-screen">
if isVerified {
<h2 class="text-6xl mb-10">
Your email has been verified
</h2>
<p class="text-lg text-center">
You have completed the verification process. Thank you!
</p>
<a class="btn btn-primary mt-8" href="/">
Go Home
</a>
} else {
<h2 class="text-6xl mb-10">
Error during verification
</h2>
<p class="text-lg text-center">
Please try again by sign up process
</p>
<a class="btn btn-primary mt-8" href="/auth/signup">
Sign Up
</a>
}
</div>
</main>
<main>
<div class="flex flex-col items-center justify-center h-screen">
if isVerified {
<h2 class="text-6xl mb-10">
Your email has been verified
</h2>
<p class="text-lg text-center">
You have completed the verification process. Thank you!
</p>
<a class="mt-8" href="/">
Go Home
</a>
} else {
<h2 class="text-6xl mb-10">
Error during verification
</h2>
<p class="text-lg text-center">
Please try again by sign up process
</p>
<a class="mt-8" href="/auth/signup">
Sign Up
</a>
}
</div>
</main>
}

View File

@@ -1,16 +1,16 @@
package template
templ Index() {
<div class="hero bg-base-200 h-full">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Next Level Workout Tracker</h1>
<p class="py-6">
Ever wanted to track your workouts and see your progress over time? web-app-template is the perfect
solution for you.
</p>
<a href="/workout" class="btn btn-primary">Get Started</a>
<div class="h-full">
<div class="text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Next Level Workout Tracker</h1>
<p class="py-6">
Ever wanted to track your workouts and see your progress over time? web-app-template is the perfect
solution for you.
</p>
<a href="/workout" class="">Get Started</a>
</div>
</div>
</div>
</div>
}

View File

@@ -1,45 +1,45 @@
package template
templ Layout(slot templ.Component, user templ.Component) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>web-app-template</title>
<link rel="icon" href="/static/favicon.svg" />
<link rel="stylesheet" href="/static/css/tailwind.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="htmx-config" content='{
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>web-app-template</title>
<link rel="icon" href="/static/favicon.svg"/>
<link rel="stylesheet" href="/static/css/tailwind.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta
name="htmx-config"
content='{
"includeIndicatorStyles": false,
"selfRequestsOnly": true,
"allowScriptTags": false
}' />
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/toast.js"></script>
</head>
<body hx-headers='{"csrf-token": "CSRF_TOKEN"}'>
<div class="h-screen flex flex-col">
<div class="flex justify-end items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2 shadow">
<a href="/" class="flex-1 flex gap-2">
<img src="/static/favicon.svg" alt="web-app-template logo" />
<span>web-app-template</span>
</a>
@user
</div>
<div class="flex-1">
if slot != nil {
@slot
}
</div>
</div>
<div class="toast" id="toasts">
<div class="hidden alert" id="toast">
New message arrived.
</div>
</div>
</body>
</html>
}'
/>
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/toast.js"></script>
</head>
<body hx-headers='{"csrf-token": "CSRF_TOKEN"}'>
<div class="h-screen flex flex-col">
<div class="flex justify-end items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2 shadow-sm">
<a href="/" class="flex-1 flex gap-2">
<img src="/static/favicon.svg" alt="web-app-template logo"/>
<span>web-app-template</span>
</a>
@user
</div>
<div class="flex-1">
if slot != nil {
@slot
}
</div>
</div>
<div class="" id="toasts">
<div class="hidden" id="toast">
New message arrived.
</div>
</div>
</body>
</html>
}

View File

@@ -1,11 +1,11 @@
package template
templ NotFound() {
<main class="flex h-full justify-center items-center ">
<div class="bg-error p-16 rounded-lg">
<h1 class="text-4xl text-error-content mb-5">Not Found</h1>
<p class="text-lg text-error-content mb-5">The page you are looking for does not exist.</p>
<a href="/" class="btn btn-lg btn-primary">Go back to home</a>
<main class="flex h-full justify-center items-center">
<div class="p-16 rounded-lg">
<h1 class="text-4xl mb-5">Not Found</h1>
<p class="text-lg mb-5">The page you are looking for does not exist.</p>
<a href="/" class="">Go back to home</a>
</div>
</main>
}

View File

@@ -1,69 +1,73 @@
package workout
templ WorkoutComp(currentDate string) {
<main class="mx-2">
<form class="max-w-xl mx-auto flex flex-col gap-4 justify-center mt-10" hx-post="/api/workout"
hx-target="#workout-placeholder" hx-swap="outerHTML">
<h2 class="text-4xl mb-8">Track your workout</h2>
<input id="date" type="date" class="input input-bordered" value={ currentDate } name="date" />
<select class="select select-bordered w-full" name="type">
<option>Push Ups</option>
<option>Pull Ups</option>
</select>
<input type="number" class="input input-bordered" placeholder="Sets" name="sets" />
<input type="number" class="input input-bordered" placeholder="Reps" name="reps" />
<button class="btn btn-primary self-end">Save</button>
</form>
<div hx-get="/api/workout" hx-trigger="load"></div>
</main>
<main class="mx-2">
<form
class="max-w-xl mx-auto flex flex-col gap-4 justify-center mt-10"
hx-post="/api/workout"
hx-target="#workout-placeholder"
hx-swap="outerHTML"
>
<h2 class="text-4xl mb-8">Track your workout</h2>
<input id="date" type="date" class="" value={ currentDate } name="date"/>
<select class="w-full" name="type">
<option>Push Ups</option>
<option>Pull Ups</option>
</select>
<input type="number" class="" placeholder="Sets" name="sets"/>
<input type="number" class="" placeholder="Reps" name="reps"/>
<button class="self-end">Save</button>
</form>
<div hx-get="/api/workout" hx-trigger="load"></div>
</main>
}
type Workout struct {
Id string
Date string
Type string
Sets string
Reps string
Id string
Date string
Type string
Sets string
Reps string
}
templ WorkoutListComp(workouts []Workout) {
<div class="overflow-x-auto mx-auto max-w-screen-lg">
<h2 class="text-4xl mt-14 mb-8">Workout history</h2>
<table class="table table-auto max-w-full">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Sets</th>
<th>Reps</th>
<th></th>
</tr>
</thead>
<tbody>
<tr class="hidden" id="workout-placeholder"></tr>
for _,w := range workouts {
@WorkoutItemComp(w, false)
}
</tbody>
</table>
</div>
<div class="overflow-x-auto mx-auto max-w-lg">
<h2 class="text-4xl mt-14 mb-8">Workout history</h2>
<table class="table table-auto max-w-full">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Sets</th>
<th>Reps</th>
<th></th>
</tr>
</thead>
<tbody>
<tr class="hidden" id="workout-placeholder"></tr>
for _,w := range workouts {
@WorkoutItemComp(w, false)
}
</tbody>
</table>
</div>
}
templ WorkoutItemComp(w Workout, includePlaceholder bool) {
if includePlaceholder {
<tr class="hidden" id="workout-placeholder"></tr>
}
<tr>
<th>{ w.Date }</th>
<th>{ w.Type }</th>
<th>{ w.Sets }</th>
<th>{ w.Reps }</th>
<th>
<div class="tooltip" data-tip="Delete Entry">
<button hx-delete={ "api/workout/" + w.Id } hx-target="closest tr" type="submit">
Delete
</button>
</div>
</th>
</tr>
if includePlaceholder {
<tr class="hidden" id="workout-placeholder"></tr>
}
<tr>
<th>{ w.Date }</th>
<th>{ w.Type }</th>
<th>{ w.Sets }</th>
<th>{ w.Reps }</th>
<th>
<div class="tooltip" data-tip="Delete Entry">
<button hx-delete={ "api/workout/" + w.Id } hx-target="closest tr" type="submit">
Delete
</button>
</div>
</th>
</tr>
}