Compare commits

80 Commits

Author SHA1 Message Date
061d63a8ad feat(ui): #111 draft for (unfinished) mobile transaction-ui
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m43s
2025-08-24 15:35:00 +02:00
d0faee2950 chore(deps): update golang:1.25.0 docker digest to 4859242
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m24s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m39s
2025-08-22 21:05:43 +00:00
01d459e913 fix: add "?" to validateString
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m52s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m36s
2025-08-20 04:41:29 +02:00
cee533694c chore(deps): update golang:1.25.0 docker digest to 91e2cd4
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m40s
2025-08-18 12:05:26 +00:00
f6283c6ab3 fix(deps): update module github.com/a-h/templ to v0.3.943
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m33s
2025-08-15 16:05:55 +00:00
e48d11b818 chore(deps): update node.js to v24.6.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m38s
2025-08-14 22:06:46 +00:00
b9150334ee chore(deps): update node.js to 3266bc9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m41s
2025-08-14 16:06:16 +00:00
d20981beaa fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.32
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m0s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m34s
2025-08-14 15:06:41 +00:00
3c95abe59c fix(deps): update module github.com/a-h/templ to v0.3.937
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m6s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m30s
2025-08-14 14:10:03 +00:00
fad2bd3928 chore(deps): update tailwindcss monorepo to v4.1.12
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m34s
2025-08-14 13:06:29 +00:00
c75b99ea9d chore(deps): update node.js to 73297e2
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m31s
2025-08-14 07:05:57 +00:00
56737a4156 chore(deps): update golang:1.25.0 docker digest to 9e56f0d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m46s
2025-08-14 06:09:06 +00:00
ab425d759c chore(deps): update node.js to 58a2604
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m42s
2025-08-14 01:16:40 +00:00
283679fc4f chore(deps): update golang:1.25.0 docker digest to 10a15b9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m38s
2025-08-14 00:06:49 +00:00
e0802cf232 chore(deps): update dependency go to v1.25.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m43s
2025-08-13 23:10:18 +00:00
6577dbb297 chore(deps): update golang docker tag to v1.25.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m41s
2025-08-13 22:11:10 +00:00
7e37c24b07 chore(deps): update golang:1.24.6 docker digest to 746a0e9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m41s
2025-08-13 21:06:28 +00:00
3f3edbb8ad chore(deps): update golang:1.24.6 docker digest to 86a999d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m37s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m22s
2025-08-13 18:06:20 +00:00
16429f1950 chore(deps): update debian docker tag to v13
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m55s
2025-08-13 15:25:14 +00:00
82a9fd8220 chore(deps): update golang:1.24.6 docker digest to 370491a
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 4m50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-08-13 15:08:39 +00:00
192e6b7f50 chore(deps): update node.js to 5cc5271
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m37s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m33s
2025-08-13 02:06:22 +00:00
57377f9c27 chore(deps): update debian:12.11 docker digest to 731dd13
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m58s
2025-08-13 01:16:08 +00:00
66227c5818 chore(deps): update golang:1.24.6 docker digest to 0348485
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m44s
2025-08-13 00:08:49 +00:00
6e51e3c8b3 chore(deps): update debian:12.11 docker digest to 5626655
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m57s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m39s
2025-08-12 22:06:39 +00:00
6c916aecb4 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.31
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m0s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m39s
2025-08-11 15:06:14 +00:00
8575fbf56e chore(deps): update actions/checkout digest to 08eba0b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m38s
2025-08-11 11:05:53 +00:00
f820fcdfeb fix(deps): update module golang.org/x/net to v0.43.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m15s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m8s
2025-08-09 20:35:22 +00:00
f6e58b7afc fix(deps): update module golang.org/x/crypto to v0.41.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m2s
2025-08-09 02:06:26 +00:00
93e669b038 chore(deps): update golang docker tag to v1.24.6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m55s
2025-08-09 01:10:39 +00:00
d037317aab chore(deps): update dependency go to v1.24.6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m27s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m50s
2025-08-08 23:09:13 +00:00
0517e7ec89 feat(transaction): #243 add pagination to transactions
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m34s
2025-08-09 00:36:50 +02:00
867c0ca1cd chore(deps): update node.js to 3218f0d
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 4m3s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 2m50s
2025-08-04 16:07:53 +00:00
ddcbfaa075 chore(deps): update node.js to 0d98a9f
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m41s
2025-08-04 13:04:59 +00:00
4583c0a70e chore(deps): update node.js to v22.18.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m14s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m43s
2025-08-04 10:05:10 +00:00
380854272a chore(deps): update dependency echarts to v6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m9s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m48s
2025-08-01 22:42:20 +02:00
6bc9e0666b feat(layout): #211 optimize the overall layout for mobile
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m10s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m18s
move navigation to aside
proper mobile handling
update logo.svg
remove pirata-one/only use it for the logo
2025-08-01 22:26:17 +02:00
9fa554c60a chore(deps): update node.js to v24.5.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m37s
2025-07-31 22:08:19 +00:00
763c952cbe fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.30
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m7s
2025-07-30 13:06:28 +00:00
fce669146f fix(deps): update module github.com/a-h/templ to v0.3.924
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m16s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m5s
2025-07-26 16:05:39 +00:00
06219d1fd3 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.29
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m42s
2025-07-25 23:05:50 +00:00
9fac68d7ae chore(deps): update node.js to 37ff334
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 7m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m47s
2025-07-25 22:07:02 +00:00
8afd48b981 chore(deps): update golang:1.24.5 docker digest to ef5b4be
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 7m35s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m47s
2025-07-25 21:05:10 +00:00
25568591fd fix: build
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m39s
2025-07-25 22:47:07 +02:00
e1551c1fa3 chore(deps): update debian:12.11 docker digest to b6507e3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m49s
2025-07-22 04:06:22 +00:00
e8b3d3e16c fix(deps): update module github.com/a-h/templ to v0.3.920
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m5s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m26s
2025-07-20 19:06:40 +00:00
59288d4544 chore(deps): update node.js to 9e6918e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m59s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m27s
2025-07-17 01:06:01 +00:00
a2d1f22d46 chore(deps): update node.js to 414e20e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m40s
2025-07-16 22:09:53 +00:00
0e150b3d7d chore(deps): update node.js to v22.17.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m24s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m54s
2025-07-16 19:08:44 +00:00
19567313bd chore(deps): update node.js to v24.4.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m36s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m14s
2025-07-15 23:08:46 +00:00
42f1cfc07f fix(deps): update module golang.org/x/net to v0.42.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m3s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m30s
2025-07-10 20:06:21 +00:00
0276bc6a4c fix(deps): update module golang.org/x/crypto to v0.40.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m10s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m28s
2025-07-10 18:06:59 +00:00
d6c8559d4c chore(deps): update golang:1.24.5 docker digest to 14fd8a5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m2s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m2s
2025-07-09 21:05:29 +00:00
38cdd96b6f chore(deps): update golang docker tag to v1.24.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m3s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m0s
2025-07-09 19:05:32 +00:00
cb49494e60 chore(deps): update node.js to v24.4.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m57s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m33s
2025-07-09 13:08:35 +00:00
3ffe7514e2 chore(deps): update dependency go to v1.24.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m0s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m34s
2025-07-08 17:15:13 +00:00
d13a387303 chore(deps): update node.js to 2fa6c97
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m32s
2025-07-08 04:05:16 +00:00
a398d275f5 chore(deps): update node.js to 5307f5f
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m28s
2025-07-08 01:39:32 +00:00
23b97a9cac chore(deps): update node.js to df39165
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m32s
2025-07-07 22:06:05 +00:00
4b74a9b6d4 chore(deps): update golang:1.24.4 docker digest to 20a022e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m6s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m6s
2025-07-02 12:04:58 +00:00
c67f232e9b chore(deps): update golang:1.24.4 docker digest to 764d7e0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m4s
2025-07-02 00:06:32 +00:00
93727ee49a chore(deps): update golang:1.24.4 docker digest to a92f3b1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m1s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m7s
2025-07-01 21:05:23 +00:00
1a79df9423 chore(deps): update golang:1.24.4 docker digest to 1aa97dd
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m48s
2025-07-01 18:05:24 +00:00
582d265fd5 chore(deps): update golang:1.24.4 docker digest to 1bb140b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m56s
2025-07-01 15:05:25 +00:00
f094767582 chore(deps): update golang:1.24.4 docker digest to 270cd53
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m39s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m18s
2025-07-01 06:54:12 +00:00
440fed9ed1 chore(deps): update debian:12.11 docker digest to d42b86d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m55s
2025-07-01 04:06:08 +00:00
6e1d24eef7 feat(dashboard): #191 add development of treasurechests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 12m24s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m53s
2025-06-30 00:39:14 +02:00
f37b50515b fix(deps): update opentelemetry-go-contrib monorepo
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m25s
2025-06-27 19:06:53 +00:00
b2a512d186 fix(deps): update opentelemetry-go monorepo
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m58s
2025-06-27 18:28:27 +00:00
4a28fb5ca4 chore(deps): update node.js to v24.3.0
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 5m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-06-27 18:11:32 +00:00
e5f98c1fb0 chore(deps): update node.js to v22.17.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m13s
2025-06-27 17:48:00 +00:00
3072df6507 fix(deps): update module github.com/a-h/templ to v0.3.906
Some checks failed
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-06-27 17:42:29 +00:00
9f35ca7476 chore(deps): update tailwindcss monorepo to v4.1.11
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 5m3s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-06-27 17:24:55 +00:00
472ab68986 chore(deps): update dependency htmx.org to v2.0.6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m21s
2025-06-27 16:33:51 +00:00
2fd2200ac2 fix: enable dot in stings
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m59s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m57s
2025-06-22 19:40:59 +02:00
a58ddb7a1d chore(deps): update dependency htmx.org to v2.0.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m34s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m5s
2025-06-20 22:06:46 +00:00
147d57f6e5 feat: #194 enable sqlite wal mode for better performace
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m57s
2025-06-20 22:55:59 +02:00
d064626197 feat: #193 disable session handling for static content
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m41s
2025-06-20 22:46:13 +02:00
72869e5c68 feat(dashboard): #192 include treemap of treasure chests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m36s
2025-06-20 21:31:37 +02:00
3120c19669 feat(dashboard): #82 add chart for sum of account and savings
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m21s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m33s
2025-06-19 18:05:36 +02:00
c9bf320611 chore: include node version manager
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 4m51s
2025-06-19 12:31:55 +02:00
45 changed files with 1368 additions and 579 deletions

View File

@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- run: docker build . -t spend-sparrow-test
- run: docker rmi spend-sparrow-test

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- run: docker login git.wundenbergs.de -u tim -p ${{ secrets.DOCKER_GITEA_TOKEN }}
- run: docker build . -t git.wundenbergs.de/x/spend-sparrow:latest -t git.wundenbergs.de/x/spend-sparrow:$GITHUB_SHA
- run: docker push git.wundenbergs.de/x/spend-sparrow:latest

2
.gitignore vendored
View File

@@ -25,7 +25,7 @@ go.work.sum
# env file
.env
*.db
data/
secrets/
node_modules/

View File

@@ -24,6 +24,8 @@ linters:
- cyclop
- contextcheck
- bodyclose # i don't care in the tests, the implementation itself doesn't do http requests
- wsl_v5
- noinlineerr
settings:
nestif:
min-complexity: 6

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
24.6.0

View File

@@ -1,4 +1,4 @@
FROM golang:1.24.4@sha256:10c131810f80a4802c49cab0961bbe18a16f4bb2fb99ef16deaa23e4246fc817 AS builder_go
FROM golang:1.25.0@sha256:4859242e2c392ddc9d3225fd41181c00a443d9cc005b8e5131ce164106fbc676 AS builder_go
WORKDIR /spend-sparrow
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
RUN go install github.com/a-h/templ/cmd/templ@latest
@@ -13,7 +13,7 @@ RUN golangci-lint run ./...
RUN go build -o /spend-sparrow/spend-sparrow .
FROM node:22.16.0@sha256:71bcbb3b215b3fa84b5b167585675072f4c270855e37a599803f1a58141a0716 AS builder_node
FROM node:22.18.0@sha256:3266bc9e8bee1acc8a77386eefaf574987d2729b8c5ec35b0dbd6ddbc40b0ce2 AS builder_node
WORKDIR /spend-sparrow
COPY package.json package-lock.json ./
RUN npm clean-install
@@ -21,7 +21,7 @@ COPY . ./
RUN npm run build
FROM debian:12.11@sha256:0d8498a0e9e6a60011df39aab78534cfe940785e7c59d19dfae1eb53ea59babe
FROM debian:13.0@sha256:6d87375016340817ac2391e670971725a9981cfc24e221c47734681ed0f6c0f5
WORKDIR /spend-sparrow
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
COPY migration ./migration

View File

@@ -3,6 +3,15 @@
SpendSparrow is a web app to keep track of expenses and income. It is very opinionated by keeping an keen eye on disciplin of it's users. Every Expense needs to be mapped to a Piggy Bank. For emergencies, funds can be moved between Piggy Banks.
## Prerequisites
```bash
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
go install github.com/a-h/templ/cmd/templ@latest
go install github.com/vektra/mockery/v2@latest
```
## Design priciples
The State of the application can always be calculated on the fly. Even though it is not an Event Streaming Application, it is still important to be able to recalculate historic data.
@@ -10,3 +19,4 @@ It may be applicable to do some sort of monthly snapshots to speed up calculatio
This applications uses as little dependencies as feasible, especially on the front end.

View File

@@ -0,0 +1,188 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="logo-inkscape.svg"
inkscape:export-filename="static/logo.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="2.5914723"
inkscape:cx="385.10927"
inkscape:cy="275.712"
inkscape:window-width="2252"
inkscape:window-height="1450"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer2" />
<defs
id="defs1">
<rect
x="115.37843"
y="80.263254"
width="470.38898"
height="197.18521"
id="rect2" />
<rect
x="175.18448"
y="463.06726"
width="253.29221"
height="303.50433"
id="rect6" />
</defs>
<g
inkscape:label="Faviocon"
inkscape:groupmode="layer"
id="layer1">
<path
d="m 59.240389,97.978247 c 1.775354,-0.394229 4.087813,-2.156354 4.439709,-3.024187 0.206375,-0.508 -0.822855,-1.30175 -1.098021,-1.621896 -0.629709,-0.73025 -0.375709,-1.090083 -0.132292,-1.960562 0.277813,-0.989542 -0.381,-2.082271 -1.314979,-2.510896 -0.933979,-0.428625 -2.050521,-0.293688 -2.989792,0.124354 -0.939271,0.418042 -1.740958,1.090083 -2.526771,1.751542 -0.574145,-0.36248 -1.489604,-1.963209 -2.97127,-0.923396 -1.023938,0.717021 -1.116542,2.278062 -0.98425,3.52425 0.309562,2.876021 1.018645,4.368271 2.354791,4.770437 1.688042,0.508 3.556,0.240771 5.222875,-0.129646"
style="fill:#ffca28;stroke-width:0.264583"
id="path1-3"
inkscape:export-filename="static/favicon.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<path
d="m 62.124348,89.704727 c -0.224896,3.876145 -4.005792,6.447895 -5.799667,7.580312 l 1.164167,1.000125 c 0,0 0.738187,0.01588 1.748895,-0.306917 1.733021,-0.550333 4.265084,-2.106083 4.439709,-3.024187 0.256646,-1.336146 -1.113896,-1.045104 -1.423459,-2.100792 -0.161395,-0.558271 0.785813,-1.613958 -0.129645,-3.148541 m -6.503459,1.03452 c 0,0 -0.674687,-0.690562 -1.17475,-1.005416 -0.248708,0.468312 -0.425979,0.976312 -0.513291,1.500187 -0.156105,0.92075 0,2.227792 0.36777,3.201459 0.05821,0.150812 0.275167,0.127 0.29898,-0.03175 0.3175,-2.092855 1.021291,-3.66448 1.021291,-3.66448"
style="fill:#e2a610;stroke-width:0.264583"
id="path2-6" />
<path
d="m 50.906014,97.636935 c 0,0 -8.252354,0.891646 -11.975042,8.056565 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490104,-1.30175 6.863292,-4.0005 6.606646,-6.60664 -0.373062,-3.80471 -3.907896,-6.14363 -3.907896,-6.14363 0,0 0.140229,-4.699 -3.505729,-7.749647 -3.235854,-2.709333 -7.289271,-1.486958 -7.289271,-1.486958"
style="fill:#ffca28;stroke-width:0.264583"
id="path3-0" />
<path
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
style="fill:#6d4c41;stroke-width:0.264583"
id="path6-6" />
<path
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
style="fill:#6b4b46;stroke-width:0.264583"
id="path7-2" />
<path
d="m 60.042077,103.11381 c 0.280458,-0.19314 1.222375,0.14023 1.486958,1.98438 0.129646,0.90223 0.169333,1.77535 0.169333,1.77535 0,0 -1.11125,-0.99748 -1.47902,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
style="fill:#e2a610;stroke-width:0.264583"
id="path8-6" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="82.355011"
y="90.66716"
id="text4-9"
inkscape:label="$"
transform="rotate(20.578693)"><tspan
sodipodi:role="line"
id="tspan4-2"
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
x="82.355011"
y="90.66716">$</tspan></text>
</g>
<g
inkscape:label="Logo"
inkscape:groupmode="layer"
id="g7"
transform="translate(1.4293676,-48.496402)">
<g
id="g8"
inkscape:label="Favicon"
transform="translate(-38.797122,-28.178962)"
inkscape:export-filename="../static/logo.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<path
d="m 98.874384,115.02659 c 1.775356,-0.39423 4.087816,-2.15635 4.439706,-3.02419 0.20638,-0.508 -0.82285,-1.30175 -1.09802,-1.62189 -0.62971,-0.73025 -0.37571,-1.09009 -0.13229,-1.96057 0.27781,-0.98954 -0.381,-2.08227 -1.31498,-2.51089 -0.933978,-0.42863 -2.05052,-0.29369 -2.989791,0.12435 -0.939271,0.41804 -1.740958,1.09009 -2.526771,1.75154 -0.574145,-0.36248 -1.489604,-1.96321 -2.97127,-0.92339 -1.023938,0.71702 -1.116542,2.27806 -0.98425,3.52425 0.309562,2.87602 1.018645,4.36827 2.354791,4.77043 1.688042,0.508 3.556,0.24078 5.222875,-0.12964"
style="fill:#ffca28;stroke-width:0.264583"
id="path1-3-3" />
<path
d="m 101.75834,106.75307 c -0.22489,3.87614 -4.005789,6.44789 -5.799664,7.58031 l 1.164167,1.00013 c 0,0 0.738187,0.0159 1.748895,-0.30692 1.733022,-0.55033 4.265082,-2.10608 4.439712,-3.02419 0.25664,-1.33614 -1.1139,-1.0451 -1.42346,-2.10079 -0.1614,-0.55827 0.78581,-1.61396 -0.12965,-3.14854 m -6.503456,1.03452 c 0,0 -0.674687,-0.69056 -1.17475,-1.00542 -0.248708,0.46832 -0.425979,0.97632 -0.513291,1.50019 -0.156105,0.92075 0,2.22779 0.36777,3.20146 0.05821,0.15081 0.275167,0.127 0.29898,-0.0317 0.3175,-2.09286 1.021291,-3.66448 1.021291,-3.66448"
style="fill:#e2a610;stroke-width:0.264583"
id="path2-6-6" />
<path
d="m 90.540009,114.68528 c 0,0 -8.252354,0.89164 -11.975042,8.05656 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490101,-1.30175 6.863291,-4.0005 6.606641,-6.60664 -0.37306,-3.80471 -3.90789,-6.14363 -3.90789,-6.14363 0,0 0.14023,-4.699 -3.50573,-7.74964 -3.235854,-2.70934 -7.289271,-1.48696 -7.289271,-1.48696"
style="fill:#ffca28;stroke-width:0.264583"
id="path3-0-1" />
<path
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
style="fill:#6d4c41;stroke-width:0.264583"
id="path6-6-2" />
<path
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
style="fill:#6b4b46;stroke-width:0.264583"
id="path7-2-9" />
<path
d="m 99.676072,120.16215 c 0.280458,-0.19314 1.222378,0.14023 1.486958,1.98438 0.12965,0.90223 0.16933,1.77535 0.16933,1.77535 0,0 -1.11125,-0.99748 -1.479017,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
style="fill:#e2a610;stroke-width:0.264583"
id="path8-6-3" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="125.45235"
y="92.696564"
id="text4-9-1"
inkscape:label="$"
transform="rotate(20.578693)"><tspan
sodipodi:role="line"
id="tspan4-2-9"
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
x="125.45235"
y="92.696564">$</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Text"
transform="translate(-1.4293676,48.496402)">
<text
xml:space="preserve"
style="font-size:17.6389px;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="57.635151"
y="55.655094"
id="text1"
inkscape:label="SpendSparrow"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.6389px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:-0.529167px;fill:#4d4d4d;stroke:none;stroke-width:0.264583"
x="57.635151"
y="55.655094">pendSparrow</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#1a1a1a;stroke-width:0.264583"
x="93.314896"
y="91.227318"
id="text5"><tspan
sodipodi:role="line"
id="tspan5"
style="stroke-width:0.264583"
x="93.314896"
y="91.227318" /></text>
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text6"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:74.6667px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-2px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect6);display:inline;fill:#1a1a1a;stroke:none" />
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text2"
style="fill:#4d4d4d;text-orientation:auto;-inkscape-font-specification:'Pirata One, Normal';font-family:'Pirata One';font-size:74.66666667px;letter-spacing:-2px;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect2)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

6
dev.sh
View File

@@ -1,3 +1,9 @@
#!/bin/sh
set -e
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
go install github.com/a-h/templ/cmd/templ@latest
go install github.com/vektra/mockery/v2@latest
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ." &
npm run watch

54
go.mod
View File

@@ -2,54 +2,54 @@ module spend-sparrow
go 1.23.0
toolchain go1.24.4
toolchain go1.25.0
require (
github.com/a-h/templ v0.3.898
github.com/a-h/templ v0.3.943
github.com/golang-migrate/migrate/v4 v4.18.3
github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.28
github.com/mattn/go-sqlite3 v1.14.32
github.com/stretchr/testify v1.10.0
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2
github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0
go.opentelemetry.io/otel/log v0.12.2
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/sdk/log v0.12.2
go.opentelemetry.io/otel/sdk/metric v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
golang.org/x/crypto v0.39.0
golang.org/x/net v0.41.0
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0
go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
go.opentelemetry.io/otel/log v0.13.0
go.opentelemetry.io/otel/sdk v1.37.0
go.opentelemetry.io/otel/sdk/log v0.13.0
go.opentelemetry.io/otel/sdk/metric v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
golang.org/x/crypto v0.41.0
golang.org/x/net v0.43.0
)
require (
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/grpc v1.72.1 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

104
go.sum
View File

@@ -1,7 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=
github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -9,8 +9,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
@@ -23,8 +23,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -41,8 +41,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -57,54 +57,54 @@ github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2 h1:zA9ZXfdtowo0EKt+t7u
github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2/go.mod h1:ySXmuW9JLCm/TjsQksuMY/7MNiWqfHnhH2xeT34uOLU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0 h1:EMIiYTms4Z4m3bBuKp1VmMNRLZcl6j4YbvOPL1IhlWo=
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0/go.mod h1:DIEZmUR7tzuOOVUTDKvkGWtYWSHFV18Qg8+GMb8wPJw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0=
go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY=
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0=
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 h1:lFM7SZo8Ce01RzRfnUFQZEYeWRf/MtOA3A5MobOqk2g=
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0/go.mod h1:Dw05mhFtrKAYu72Tkb3YBYeQpRUJ4quDgo2DQw3No5A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 h1:z6lNIajgEBVtQZHjfw2hAccPEBDs+nx58VemmXWa2ec=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0/go.mod h1:+kyc3bRx/Qkq05P6OCu3mTEIOxYRYzoIg+JsUp5X+PM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 h1:zG8GlgXCJQd5BU98C0hZnBbElszTmUgCNCfYneaDL0A=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls=
go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ=
go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw=
go.opentelemetry.io/otel/sdk/log/logtest v0.13.0 h1:9yio6AFZ3QD9j9oqshV1Ibm9gPLlHNxurno5BreMtIA=
go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -3,37 +3,34 @@
@source './static/**/*.js';
@source './template/**/*.templ';
body {
@apply font-garamond text-gray-700;
}
input:focus {
@apply outline-none ring-0;
}
@font-face {
font-family: "Pirata One";
src: url("/static/font/PirataOne-Regular.woff2") format("woff2");
}
@font-face {
font-family: "EB Garamond";
src: url("/static/font/EBGaramond-VariableFont_wght.woff2") format("woff2");
}
@theme {
--font-pirata: "Pirata One", serif;
--font-garamond: "EB Garamond", serif;
body {
font-family: "EB Garamond", serif;
@apply text-gray-700;
}
input:focus {
@apply outline-none ring-0;
}
button {
@apply cursor-pointer;
}
/* Button */
.button {
transition: all 150ms linear;
transition: all 150ms linear;
@apply cursor-pointer border-2 rounded-lg border-transparent;
}
.button-primary:hover,
.button-normal:hover {
transform: translate(-0.25rem, -0.25rem);
box-shadow: 3px 3px 3px var(--color-gray-200);
transform: translate(-0.25rem, -0.25rem);
box-shadow: 3px 3px 3px var(--color-gray-200);
}
.button-primary {
@@ -61,3 +58,5 @@ input:focus {
box-shadow: 0 0 0 2px var(--color-gray-200);
}

View File

@@ -121,7 +121,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
render := handler.NewRender()
indexHandler := handler.NewIndex(render, clockService)
dashboardHandler := handler.NewDashboard(render, dashboardService)
dashboardHandler := handler.NewDashboard(render, dashboardService, treasureChestService)
authHandler := handler.NewAuth(authService, render)
accountHandler := handler.NewAccount(accountService, render)
treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render)

View File

@@ -9,6 +9,8 @@ import (
"spend-sparrow/internal/template/dashboard"
"spend-sparrow/internal/utils"
"time"
"github.com/google/uuid"
)
type Dashboard interface {
@@ -16,20 +18,24 @@ type Dashboard interface {
}
type DashboardImpl struct {
r *Render
d *service.Dashboard
r *Render
d *service.Dashboard
treasureChest service.TreasureChest
}
func NewDashboard(r *Render, d *service.Dashboard) Dashboard {
func NewDashboard(r *Render, d *service.Dashboard, treasureChest service.TreasureChest) Dashboard {
return DashboardImpl{
r: r,
d: d,
r: r,
d: d,
treasureChest: treasureChest,
}
}
func (handler DashboardImpl) Handle(router *http.ServeMux) {
router.Handle("GET /dashboard", handler.handleDashboard())
router.Handle("GET /dashboard/dataset", handler.handleDashboardDataset())
router.Handle("GET /dashboard/main-chart", handler.handleDashboardMainChart())
router.Handle("GET /dashboard/treasure-chests", handler.handleDashboardTreasureChests())
router.Handle("GET /dashboard/treasure-chest", handler.handleDashboardTreasureChest())
}
func (handler DashboardImpl) handleDashboard() http.HandlerFunc {
@@ -42,12 +48,18 @@ func (handler DashboardImpl) handleDashboard() http.HandlerFunc {
return
}
comp := dashboard.Dashboard()
treasureChests, err := handler.treasureChest.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
return
}
comp := dashboard.Dashboard(treasureChests)
handler.r.RenderLayoutWithStatus(r, w, comp, user, http.StatusOK)
}
}
func (handler DashboardImpl) handleDashboardDataset() http.HandlerFunc {
func (handler DashboardImpl) handleDashboardMainChart() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
@@ -69,11 +81,15 @@ func (handler DashboardImpl) handleDashboardDataset() http.HandlerFunc {
account += fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100)
savings += fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Savings)/100)
}
account = account[:len(account)-1]
savings = savings[:len(savings)-1]
_, err = fmt.Fprintf(w, `
{
"aria": {
"enabled": true
},
"tooltip": {
"trigger": "axis",
"formatter": "<updated by client>"
@@ -105,3 +121,127 @@ func (handler DashboardImpl) handleDashboardDataset() http.HandlerFunc {
}
}
}
func (handler DashboardImpl) handleDashboardTreasureChests() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r)
treeList, err := handler.d.TreasureChests(r.Context(), user)
if err != nil {
handleError(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
data := ""
for _, item := range treeList {
children := ""
for _, child := range item.Children {
if child.Value < 0 {
children += fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, -child.Value)
} else {
children += fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, child.Value)
}
}
children = children[:len(children)-1]
data += fmt.Sprintf(`{"name":"%s","children":[%s]},`, item.Name, children)
}
data = data[:len(data)-1]
_, err = fmt.Fprintf(w, `
{
"aria": {
"enabled": true
},
"series": [
{
"data": [%s],
"type": "treemap",
"name": "Savings"
}
]
}
`, data)
if err != nil {
slog.InfoContext(r.Context(), "could not write response", "err", err)
}
}
}
func (handler DashboardImpl) handleDashboardTreasureChest() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r)
var treasureChestId *uuid.UUID
treasureChestStr := r.URL.Query().Get("id")
if treasureChestStr != "" {
id, err := uuid.Parse(treasureChestStr)
if err != nil {
handleError(w, r, fmt.Errorf("could not parse treasure chest: %w", service.ErrBadRequest))
return
}
treasureChestId = &id
}
series, err := handler.d.TreasureChest(r.Context(), user, treasureChestId)
if err != nil {
handleError(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
value := ""
for _, entry := range series {
value += fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100)
}
if len(value) > 0 {
value = value[:len(value)-1]
}
_, err = fmt.Fprintf(w, `
{
"aria": {
"enabled": true
},
"tooltip": {
"trigger": "axis",
"formatter": "<updated by client>"
},
"xAxis": {
"type": "time"
},
"yAxis": {
"axisLabel": {
"formatter": "{value} €"
}
},
"series": [
{
"data": [%s],
"type": "line",
"name": "Treasure Chest Value"
}
]
}
`, value)
if err != nil {
slog.InfoContext(r.Context(), "could not write response", "err", err)
}
}
}

View File

@@ -3,6 +3,7 @@ package middleware
import (
"context"
"net/http"
"strings"
"spend-sparrow/internal/service"
"spend-sparrow/internal/types"
@@ -16,6 +17,13 @@ var UserKey ContextKey = "user"
func Authenticate(service service.Auth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if strings.Contains(r.URL.Path, "/static/") {
next.ServeHTTP(w, r.WithContext(ctx))
return
}
sessionId := getSessionID(r)
session, user, _ := service.SignInSession(r.Context(), sessionId)
@@ -33,7 +41,6 @@ func Authenticate(service service.Auth) func(http.Handler) http.Handler {
http.SetCookie(w, &cookie)
}
ctx := r.Context()
ctx = context.WithValue(ctx, UserKey, user)
ctx = context.WithValue(ctx, SessionKey, session)

View File

@@ -10,6 +10,7 @@ import (
type csrfResponseWriter struct {
http.ResponseWriter
csrfToken string
}
@@ -32,9 +33,15 @@ func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := GetSession(r)
ctx := r.Context()
if strings.Contains(r.URL.Path, "/static/") {
next.ServeHTTP(w, r.WithContext(ctx))
return
}
session := GetSession(r)
if r.Method == http.MethodPost ||
r.Method == http.MethodPut ||
r.Method == http.MethodDelete ||

View File

@@ -8,6 +8,7 @@ import (
type WrappedWriter struct {
http.ResponseWriter
StatusCode int
}

View File

@@ -65,6 +65,8 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
func (handler IndexImpl) handleEmpty() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// do nothing
updateSpan(r)
// Return nothing
}
}

View File

@@ -58,6 +58,7 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
AccountId: r.URL.Query().Get("account-id"),
TreasureChestId: r.URL.Query().Get("treasure-chest-id"),
Error: r.URL.Query().Get("error"),
Page: r.URL.Query().Get("page"),
}
transactions, err := h.s.GetAll(r.Context(), user, filter)

View File

@@ -6,6 +6,7 @@ import (
"spend-sparrow/internal/types"
"time"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
@@ -39,7 +40,9 @@ func (s Dashboard) MainChart(
}
timeEntries := make([]types.DashboardMainChartEntry, 0)
var lastEntry *types.DashboardMainChartEntry
for _, t := range transactions {
if t.Error != nil {
continue
@@ -64,6 +67,7 @@ func (s Dashboard) MainChart(
if t.AccountId != nil {
lastEntry.Value += t.Value
}
if t.TreasureChestId != nil {
lastEntry.Savings += t.Value
}
@@ -75,3 +79,97 @@ func (s Dashboard) MainChart(
return timeEntries, nil
}
func (s Dashboard) TreasureChests(
ctx context.Context,
user *types.User,
) ([]*types.DashboardTreasureChest, error) {
if user == nil {
return nil, ErrUnauthorized
}
treasureChests := make([]*types.TreasureChest, 0)
err := s.db.SelectContext(ctx, &treasureChests, `SELECT * FROM treasure_chest WHERE user_id = ?`, user.Id)
err = db.TransformAndLogDbError(ctx, "dashboard TreasureChests", nil, err)
if err != nil {
return nil, err
}
treasureChests = sortTreasureChests(treasureChests)
result := make([]*types.DashboardTreasureChest, 0)
for _, t := range treasureChests {
if t.ParentId == nil {
result = append(result, &types.DashboardTreasureChest{
Name: t.Name,
Value: t.CurrentBalance,
Children: make([]types.DashboardTreasureChest, 0),
})
} else {
result[len(result)-1].Children = append(result[len(result)-1].Children, types.DashboardTreasureChest{
Name: t.Name,
Value: t.CurrentBalance,
Children: make([]types.DashboardTreasureChest, 0),
})
}
}
return result, nil
}
func (s Dashboard) TreasureChest(
ctx context.Context,
user *types.User,
treausureChestId *uuid.UUID,
) ([]types.DashboardMainChartEntry, error) {
if user == nil {
return nil, ErrUnauthorized
}
transactions := make([]types.Transaction, 0)
err := s.db.SelectContext(ctx, &transactions, `
SELECT *
FROM "transaction"
WHERE user_id = ?
AND treasure_chest_id = ?
ORDER BY timestamp`, user.Id, treausureChestId)
err = db.TransformAndLogDbError(ctx, "dashboard Chart", nil, err)
if err != nil {
return nil, err
}
timeEntries := make([]types.DashboardMainChartEntry, 0)
var lastEntry *types.DashboardMainChartEntry
for _, t := range transactions {
if t.Error != nil {
continue
}
newDay := t.Timestamp.Truncate(24 * time.Hour)
if lastEntry == nil {
lastEntry = &types.DashboardMainChartEntry{
Day: newDay,
Value: 0,
}
} else if lastEntry.Day != newDay {
timeEntries = append(timeEntries, *lastEntry)
lastEntry = &types.DashboardMainChartEntry{
Day: newDay,
Value: lastEntry.Value,
}
}
if t.TreasureChestId != nil {
lastEntry.Value += t.Value
}
}
if lastEntry != nil {
timeEntries = append(timeEntries, *lastEntry)
}
return timeEntries, nil
}

View File

@@ -10,7 +10,7 @@ const (
)
var (
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9ÄÖÜäöüß,&'" -]+$`)
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9ÄÖÜäöüß,&'". \-\?]+$`)
)
func validateString(value string, fieldName string) error {

View File

@@ -7,12 +7,15 @@ import (
"log/slog"
"spend-sparrow/internal/db"
"spend-sparrow/internal/types"
"strconv"
"time"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
const page_size = 25
type Transaction interface {
Add(ctx context.Context, tx *sqlx.Tx, user *types.User, transaction types.Transaction) (*types.Transaction, error)
Update(ctx context.Context, user *types.User, transaction types.Transaction) (*types.Transaction, error)
@@ -231,22 +234,41 @@ func (s TransactionImpl) GetAll(ctx context.Context, user *types.User, filter ty
return nil, ErrUnauthorized
}
var (
page int64
offset int64
err error
)
if filter.Page != "" {
page, err = strconv.ParseInt(filter.Page, 10, 64)
if err != nil {
offset = 0
} else {
offset = page - 1
offset *= page_size
}
}
transactions := make([]*types.Transaction, 0)
err := s.db.SelectContext(ctx, &transactions, `
err = s.db.SelectContext(ctx, &transactions, `
SELECT *
FROM "transaction"
WHERE user_id = ?
AND (? = '' OR account_id = ?)
AND (? = '' OR treasure_chest_id = ?)
AND (? = ''
OR (? = "true" AND error IS NOT NULL)
OR (? = "false" AND error IS NULL)
AND ($1 = '' OR account_id = $1)
AND ($2 = '' OR treasure_chest_id = $2)
AND ($3 = ''
OR ($3 = "true" AND error IS NOT NULL)
OR ($3 = "false" AND error IS NULL)
)
ORDER BY timestamp DESC, created_at DESC`,
ORDER BY timestamp DESC, created_at DESC
LIMIT $4 OFFSET $5
`,
user.Id,
filter.AccountId, filter.AccountId,
filter.TreasureChestId, filter.TreasureChestId,
filter.Error, filter.Error, filter.Error)
filter.AccountId,
filter.TreasureChestId,
filter.Error,
page_size,
offset)
err = db.TransformAndLogDbError(ctx, "transaction GetAll", nil, err)
if err != nil {
return nil, err

View File

@@ -205,7 +205,7 @@ func (s TreasureChestImpl) GetAll(ctx context.Context, user *types.User) ([]*typ
return nil, err
}
return sortTree(treasureChests), nil
return sortTreasureChests(treasureChests), nil
}
func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr string) error {
@@ -277,7 +277,7 @@ func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr s
return nil
}
func sortTree(nodes []*types.TreasureChest) []*types.TreasureChest {
func sortTreasureChests(nodes []*types.TreasureChest) []*types.TreasureChest {
var (
roots []*types.TreasureChest
)

View File

@@ -66,7 +66,9 @@ templ EditAccount(account *types.Account) {
hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2"
>
@svg.Cancel()
<span class="h-4 w-4">
@svg.Cancel()
</span>
<span>
Cancel
</span>

View File

@@ -71,10 +71,10 @@ if isSignIn {
Don't have an account?
Sign Up
</a>
<button class="button button-primary font-pirata text-gray-600 text-2xl px-1">Sign In</button>
<button class="button button-primary text-gray-600 text-2xl px-1">Sign In</button>
} else {
<a href="/auth/signin" class="text-gray-500 text-sm px-1 button button-neglect">Already have an account? Sign In</a>
<button class="button button-primary font-pirata text-gray-600 text-2xl px-1">
<button class="button button-primary text-gray-600 text-2xl px-1">
Sign Up
</button>
}

View File

@@ -1,7 +1,7 @@
package auth
templ UserComp(user string) {
<div id="user-info" class="flex gap-5 items-center">
<div id="user-info" class="flex items-center gap-2 text-nowrap">
if user != "" {
<div class="inline-block group relative">
<button class="font-semibold py-2 px-4 inline-flex items-center">
@@ -37,8 +37,8 @@ templ UserComp(user string) {
</div>
</div>
} else {
<a href="/auth/signup" class="font-pirata text-xl button px-1 button-neglect">Sign Up</a>
<a href="/auth/signin" class="font-pirata text-xl button px-1 button-neglect">Sign In</a>
<a href="/auth/signup" class="text-xl button px-1 button-neglect">Sign Up</a>
<a href="/auth/signin" class="text-xl button px-1 button-neglect">Sign In</a>
}
</div>
}

View File

@@ -1,7 +1,32 @@
package dashboard
templ Dashboard() {
import "spend-sparrow/internal/types"
templ Dashboard(treasureChests []*types.TreasureChest) {
<div class="mt-10 h-full">
<div id="graph" class="h-96"></div>
<div id="main-chart" class="h-96 mt-10"></div>
<div id="treasure-chests" class="h-96 mt-10"></div>
<section>
<form class="flex items-center justify-end gap-4 mr-40">
<label for="treasure-chest">Treasure Chest:</label>
<select id="treasure-chest-id" name="treasure-chest-id" class="bg-white input">
<option value="">- Select Treasure Chest -</option>
for _, parent := range treasureChests {
if parent.ParentId == nil {
<optgroup label={ parent.Name }>
for _, child := range treasureChests {
if child.ParentId != nil && *child.ParentId == parent.Id {
<option
value={ child.Id.String() }
>{ child.Name }</option>
}
}
</optgroup>
}
}
</select>
</form>
<div id="treasure-chest" class="h-96 mt-10"></div>
</section>
</div>
}

View File

@@ -3,8 +3,7 @@ package template
templ Index() {
<div class="h-full flex flex-col items-center justify-center">
<h1 class="flex gap-2 w-full justify-center">
<img class="w-24" src="/static/favicon.svg" alt="SpendSparrow logo"/>
<span class="text-8xl tracking-tighter font-bold font-pirata">SpendSparrow</span>
<img width="600" src="/static/logo.svg" alt="SpendSparrow logo"/>
</h1>
<h2 class="text-2xl mt-8 text-gray-800">
Spend your <span class="px-2 text-3xl text-yellow-800">treasure</span> on the important

View File

@@ -1,10 +1,14 @@
package template
import "spend-sparrow/internal/template/svg"
func layoutLinkClass(isActive bool) string {
common := "text-2xl p-2 text-gray-900 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg"
if isActive {
return "text-xl hover:bg-gray-100 p-1 duration-100 rounded-xl transition-colors text-gray-900"
return common + " " + "underline"
}
return "text-xl hover:bg-gray-100 hover:text-gray-900 p-1 duration-200 rounded-xl transition-colors text-gray-400"
return common + " " + "hover:underline"
}
templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path string) {
@@ -22,48 +26,70 @@ templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path str
"includeIndicatorStyles": false,
"selfRequestsOnly": true,
"allowScriptTags": false
}'
}'
/>
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/toast.js"></script>
<script src="/static/js/layout.js"></script>
<script src="/static/js/transaction.js"></script>
<script src="/static/js/time.js"></script>
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/dashboard.js" defer></script>
</head>
<body class="h-screen flex flex-col" hx-headers='{"Csrf-Token": "CSRF_TOKEN"}'>
// Header
<nav class="flex bg-white items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2">
<a href="/" class="flex gap-2 mr-20">
<img class="w-6" src="/static/favicon.svg" alt="SpendSparrow logo"/>
<span class="text-4xl font-bold font-pirata">SpendSparrow</span>
</a>
if loggedIn {
<a class={ layoutLinkClass(path == "/dashboard") } href="/dashboard">Dashboard</a>
<a class={ layoutLinkClass(path == "/transaction") } href="/transaction">Transaction</a>
<a class={ layoutLinkClass(path == "/treasurechest") } href="/treasurechest">Treasure Chest</a>
<a class={ layoutLinkClass(path == "/account") } href="/account">Account</a>
}
<div class="ml-auto">
@user
<body hx-headers='{"Csrf-Token": "CSRF_TOKEN"}'>
<div class="flex flex-col min-h-screen">
<header class="sticky top-0 z-50 bg-white flex items-center gap-6 p-4 border-b-1 border-gray-200">
<button id="menuButton" class="w-10 h-10 block xl:hidden">
@svg.Menu()
</button>
<a href="/" class="flex gap-2 -mt-2">
<img width="150" src="/static/logo.svg" alt="SpendSparrow logo"/>
</a>
<div class="ml-auto">
@user
</div>
</header>
// Content
<div class="flex flex-1">
if loggedIn {
<aside class="shrink-0 h-[calc(100vh-4rem)] xl:block hidden sticky top-18 border-r-1 border-gray-200 overflow-y-auto p-4">
@navigation(path)
</aside>
}
<main class="flex-1 p-6">
if slot != nil {
@slot
}
</main>
</div>
</nav>
<div class="h-12 fixed top-12 mr-4 inset-0 bg-linear-0 from-transparent to-white"></div>
// Content
<main class="flex-1 overflow-auto">
if slot != nil {
@slot
}
</main>
// Footer
<!-- </div> -->
</div>
<dialog id="menu" class="max-h-none w-64 h-screen">
<header class="sticky top-0 z-50 bg-white flex items-center justify-between p-4 border-b-1 border-gray-200">
<a href="/" class="flex gap-2 -mt-2">
<img width="150" src="/static/logo.svg" alt="SpendSparrow logo"/>
</a>
<button id="menuButtonClose" class="h-6 w-6">
@svg.Cancel()
</button>
</header>
@navigation(path)
</dialog>
<div id="toasts" class="fixed bottom-4 right-4 ml-4 max-w-96 flex flex-col gap-2 z-50">
<div
id="toast"
class="transition-all duration-300 opacity-0 px-4 py-2 text-lg hidden text-bold rounded bg-amber-900 text-white"
>
M
</div>
class="transition-all duration-300
opacity-0 px-4 py-2 text-lg hidden text-bold rounded bg-amber-900 text-white"
></div>
</div>
</body>
</html>
}
templ navigation(path string) {
<nav class="w-64 text-nowrap flex gap-2 flex-col text-lg mt-5 px-5 pt-2">
<a class={ layoutLinkClass(path == "/dashboard") } href="/dashboard">Dashboard</a>
<a class={ layoutLinkClass(path == "/transaction") } href="/transaction">Transaction</a>
<a class={ layoutLinkClass(path == "/treasurechest") } href="/treasurechest">Treasure Chest</a>
<a class={ layoutLinkClass(path == "/account") } href="/account">Account</a>
</nav>
}

View File

@@ -31,7 +31,7 @@ templ Save() {
}
templ Cancel() {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" class="h-4 w-4 text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" class="text-gray-500">
<path fill="currentColor" d="m654 501l346 346l-154 154l-346-346l-346 346L0 847l346-346L0 155L154 1l346 346L846 1l154 154z"></path>
</svg>
}
@@ -47,3 +47,13 @@ templ Info() {
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSInfo0)"></path>
</svg>
}
templ Menu() {
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="text-gray-500">
<g data-name="1" id="_1">
<path d="M441.13,166.52h-372a15,15,0,1,1,0-30h372a15,15,0,0,1,0,30Z"></path>
<path d="M441.13,279.72h-372a15,15,0,1,1,0-30h372a15,15,0,0,1,0,30Z"></path>
<path d="M441.13,392.92h-372a15,15,0,1,1,0-30h372a15,15,0,0,1,0,30Z"></path>
</g>
</svg>
}

View File

@@ -1,74 +1,97 @@
package transaction
import "fmt"
import "time"
import "spend-sparrow/internal/template/svg"
import "spend-sparrow/internal/types"
import "github.com/google/uuid"
templ Transaction(items templ.Component, filter types.TransactionItemsFilter, accounts []*types.Account, treasureChests []*types.TreasureChest) {
<div class="max-w-6xl mt-10 mx-auto">
<div class="flex items-center gap-4">
<form
hx-get="/transaction"
hx-target="#transaction-items"
hx-push-url="true"
hx-trigger="change"
>
<select name="account-id" class="bg-white input">
<option value="">- Filter Acount -</option>
for _, account := range accounts {
<option
value={ account.Id.String() }
selected?={ filter.AccountId == account.Id.String() }
>{ account.Name }</option>
}
</select>
<select name="treasure-chest-id" class="bg-white input">
<option value="">- Filter Treasure Chest -</option>
for _, parent := range treasureChests {
if parent.ParentId == nil {
<optgroup label={ parent.Name }>
for _, child := range treasureChests {
if child.ParentId != nil && *child.ParentId == parent.Id {
<option
value={ child.Id.String() }
selected?={ filter.TreasureChestId == child.Id.String() }
>{ child.Name }</option>
}
}
</optgroup>
}
}
</select>
<select name="error" class="bg-white input">
<option value="">- Filter Error -</option>
<option
value="true"
selected?={ filter.Error == "true" }
>Has Errors</option>
<option
value="false"
selected?={ filter.Error == "false" }
>Has no Errors</option>
</select>
</form>
<button
hx-get="/transaction/new"
hx-target="#transaction-items"
hx-swap="afterbegin"
class="button button-primary ml-auto px-2 flex items-center gap-2 justify-center"
>
@svg.Plus()
<p>New Transaction</p>
</button>
templ Transaction(
items templ.Component,
filter types.TransactionItemsFilter,
accounts []*types.Account,
treasureChests []*types.TreasureChest) {
<div class="">
<div class="">
<!-- <form -->
<!-- id="transactionFilterForm" -->
<!-- hx-get="/transaction" -->
<!-- hx-target="#transaction-items" -->
<!-- hx-push-url="true" -->
<!-- hx-trigger="change" -->
<!-- > -->
<!-- <select name="account-id" class=""> -->
<!-- <option value="">- Filter Acount -</option> -->
<!-- for _, account := range accounts { -->
<!-- <option -->
<!-- value={ account.Id.String() } -->
<!-- selected?={ filter.AccountId == account.Id.String() } -->
<!-- >{ account.Name }</option> -->
<!-- } -->
<!-- </select> -->
<!-- <select name="treasure-chest-id" class=""> -->
<!-- <option value="">- Filter Treasure Chest -</option> -->
<!-- for _, parent := range treasureChests { -->
<!-- if parent.ParentId == nil { -->
<!-- <optgroup label={ parent.Name }> -->
<!-- for _, child := range treasureChests { -->
<!-- if child.ParentId != nil && *child.ParentId == parent.Id { -->
<!-- <option -->
<!-- value={ child.Id.String() } -->
<!-- selected?={ filter.TreasureChestId == child.Id.String() } -->
<!-- >{ child.Name }</option> -->
<!-- } -->
<!-- } -->
<!-- </optgroup> -->
<!-- } -->
<!-- } -->
<!-- </select> -->
<!-- <select name="error" class=""> -->
<!-- <option value="">- Filter Error -</option> -->
<!-- <option -->
<!-- value="true" -->
<!-- selected?={ filter.Error == "true" } -->
<!-- >Has Errors</option> -->
<!-- <option -->
<!-- value="false" -->
<!-- selected?={ filter.Error == "false" } -->
<!-- >Has no Errors</option> -->
<!-- </select> -->
<!-- <input id="page" name="page" type="hidden" value={ filter.Page }/> -->
<!-- </form> -->
<!-- <button -->
<!-- hx-get="/transaction/new" -->
<!-- hx-target="#transaction-items" -->
<!-- hx-swap="afterbegin" -->
<!-- class="" -->
<!-- > -->
<!-- @svg.Plus() -->
<!-- <p>New Transaction</p> -->
<!-- </button> -->
</div>
<!-- <div class=""> -->
<!-- <button id="pagePrev1" class=""> -->
<!-- &lt; -->
<!-- </button> -->
<!-- <span class="">Page: <span class="" id="page1">{ getPageNumber(filter.Page) }</span></span> -->
<!-- <button id="pageNext1" class=""> -->
<!-- &gt; -->
<!-- </button> -->
<!-- </div> -->
@items
<!-- <div class=""> -->
<!-- <button id="pagePrev2" class=""> -->
<!-- &lt; -->
<!-- </button> -->
<!-- <span class="">Page: <span class="" id="page2">{ getPageNumber(filter.Page) }</span></span> -->
<!-- <button id="pageNext2" class=""> -->
<!-- &gt; -->
<!-- </button> -->
<!-- </div> -->
</div>
}
templ TransactionItems(transactions []*types.Transaction, accounts, treasureChests map[uuid.UUID]string) {
<div id="transaction-items" class="my-6">
<div id="transaction-items" class="flex flex-col gap-8">
for _, transaction := range transactions {
@TransactionItem(transaction, accounts, treasureChests)
}
@@ -76,207 +99,186 @@ templ TransactionItems(transactions []*types.Transaction, accounts, treasureChes
}
templ EditTransaction(transaction *types.Transaction, accounts []*types.Account, treasureChests []*types.TreasureChest) {
{{
var (
timestamp time.Time
id string
cancelUrl string
)
party := ""
description := ""
accountId := ""
value := "0.00"
treasureChestId := ""
if transaction == nil {
timestamp = time.Now().UTC().Truncate(time.Minute)
id = "new"
cancelUrl = "/empty"
} else {
timestamp = transaction.Timestamp.UTC().Truncate(time.Minute)
party = transaction.Party
description = transaction.Description
if transaction.AccountId != nil {
accountId = transaction.AccountId.String()
}
if transaction.TreasureChestId != nil {
treasureChestId = transaction.TreasureChestId.String()
}
value = formatFloat(transaction.Value)
id = transaction.Id.String()
cancelUrl = "/transaction/" + id
}
}}
<div id="transaction" class="border-1 border-gray-300 w-full my-4 p-4 bg-gray-50 rounded-lg">
<form
hx-post={ "/transaction/" + id }
hx-target="closest #transaction"
hx-swap="outerHTML"
class="text-xl flex justify-end gap-4 items-center"
>
<div class="grid grid-cols-[auto_auto] items-center gap-4 mr-auto">
<label for="timestamp" class="text-sm text-gray-500">Transaction Date</label>
<input
autofocus
name="timestamp"
type="date"
value={ timestamp.String() }
class="bg-white input datetime"
/>
<label for="party" class="text-sm text-gray-500">Party</label>
<input
name="party"
type="text"
value={ party }
class="mr-auto bg-white input"
/>
<label for="description" class="text-sm text-gray-500">Description</label>
<input
name="description"
type="text"
value={ description }
class="mr-auto bg-white input"
/>
<label for="value" class="text-sm text-gray-500">Value (€)</label>
<input
name="value"
step="0.01"
type="number"
value={ value }
class="bg-white input"
/>
<label for="account-id" class="text-sm text-gray-500">Account</label>
<select
name="account-id"
class="bg-white input"
>
<option value="">-</option>
for _, account := range accounts {
<option selected?={ account.Id.String() == accountId } value={ account.Id.String() }>{ account.Name }</option>
}
</select>
<label for="treasure-chest-id" class="text-sm text-gray-500">Treasure Chest</label>
<select name="treasure-chest-id" class="bg-white input">
<option value="">- Filter Treasure Chest -</option>
for _, parent := range treasureChests {
if parent.ParentId == nil {
<optgroup label={ parent.Name }>
for _, child := range treasureChests {
if child.ParentId != nil && *child.ParentId == parent.Id {
<option
value={ child.Id.String() }
selected?={ treasureChestId == child.Id.String() }
>{ child.Name }</option>
}
}
</optgroup>
}
}
</select>
</div>
<button type="submit" class="button button-neglect px-1 flex items-center gap-2">
@svg.Save()
<span>
Save
</span>
</button>
<button
hx-get={ cancelUrl }
hx-target="closest #transaction"
hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2"
>
@svg.Cancel()
<span>
Cancel
</span>
</button>
</form>
</div>
// {{
// var (
// timestamp time.Time
//
// id string
// cancelUrl string
// )
// party := ""
// description := ""
// accountId := ""
// value := "0.00"
// treasureChestId := ""
// if transaction == nil {
// timestamp = time.Now().UTC().Truncate(time.Minute)
//
// id = "new"
// cancelUrl = "/empty"
// } else {
// timestamp = transaction.Timestamp.UTC().Truncate(time.Minute)
// party = transaction.Party
// description = transaction.Description
// if transaction.AccountId != nil {
// accountId = transaction.AccountId.String()
// }
// if transaction.TreasureChestId != nil {
// treasureChestId = transaction.TreasureChestId.String()
// }
// value = formatFloat(transaction.Value)
//
// id = transaction.Id.String()
// cancelUrl = "/transaction/" + id
// }
// }}
// <div id="transaction" class="">
// <form
// hx-post={ "/transaction/" + id }
// hx-target="closest #transaction"
// hx-swap="outerHTML"
// class=""
// >
// <div class="">
// <label for="timestamp" class="">Transaction Date</label>
// <input
// autofocus
// name="timestamp"
// type="date"
// value={ timestamp.String() }
// class=""
// />
// <label for="party" class="">Party</label>
// <input
// name="party"
// type="text"
// value={ party }
// class=""
// />
// <label for="description" class="">Description</label>
// <input
// name="description"
// type="text"
// value={ description }
// class=""
// />
// <label for="value" class="">Value (€)</label>
// <input
// name="value"
// step="0.01"
// type="number"
// value={ value }
// class=""
// />
// <label for="account-id" class="">Account</label>
// <select
// name="account-id"
// class=""
// >
// <option value="">-</option>
// for _, account := range accounts {
// <option selected?={ account.Id.String() == accountId } value={ account.Id.String() }>{ account.Name }</option>
// }
// </select>
// <label for="treasure-chest-id" class="">Treasure Chest</label>
// <select name="treasure-chest-id" class="">
// <option value="">- Filter Treasure Chest -</option>
// for _, parent := range treasureChests {
// if parent.ParentId == nil {
// <optgroup label={ parent.Name }>
// for _, child := range treasureChests {
// if child.ParentId != nil && *child.ParentId == parent.Id {
// <option
// value={ child.Id.String() }
// selected?={ treasureChestId == child.Id.String() }
// >{ child.Name }</option>
// }
// }
// </optgroup>
// }
// }
// </select>
// </div>
// <button type="submit" class="">
// @svg.Save()
// <span>
// Save
// </span>
// </button>
// <button
// hx-get={ cancelUrl }
// hx-target="closest #transaction"
// hx-swap="outerHTML"
// class=""
// >
// <span class="">
// @svg.Cancel()
// </span>
// <span>
// Cancel
// </span>
// </button>
// </form>
// </div>
}
templ TransactionItem(transaction *types.Transaction, accounts, treasureChests map[uuid.UUID]string) {
{{
background := "bg-gray-50"
if transaction.Error != nil {
background = "bg-yellow-50"
}
}}
<div
id="transaction"
class={ "mt-4 border-1 grid grid-cols-[auto_auto_1fr_1fr_auto_auto_auto_auto] gap-4 items-center text-xl border-gray-300 w-full p-4 rounded-lg " + background }
class="grid grid-cols-[1fr_auto]"
if transaction.Error != nil {
title={ *transaction.Error }
}
>
<p class="mr-auto datetime">{ transaction.Timestamp.String() }</p>
<div class="w-6">
if transaction.Error != nil {
@svg.Info()
<p class="datetime">{ transaction.Timestamp.String() }</p>
<p class="text-2xl flex items-center col-start-2 row-start-1 row-end-3 align-center">
if transaction.Value < 0 {
<span class="text-red-700">- { types.FormatEuros(transaction.Value) }</span>
} else {
<span class="text-green-700">+ { types.FormatEuros(transaction.Value) }</span>
}
</div>
<div>
<p class="text-sm text-gray-500">
if transaction.AccountId != nil {
{ accounts[*transaction.AccountId] }
} else {
&nbsp;
}
</p>
<!-- if transaction.AccountId != nil { -->
<!-- <p class="col-start-1"> -->
<!-- { accounts[*transaction.AccountId] } -->
<!-- </p> -->
<!-- } -->
if transaction.TreasureChestId != nil {
<p class="col-start-1">
{ treasureChests[*transaction.TreasureChestId] }
</p>
<p class="text-sm text-gray-500">
if transaction.TreasureChestId != nil {
{ treasureChests[*transaction.TreasureChestId] }
} else {
&nbsp;
}
</p>
</div>
<div>
<p class="text-sm text-gray-500">
if transaction.Party != "" {
{ transaction.Party }
} else {
&nbsp;
}
</p>
<p class="text-sm text-gray-500">
if transaction.Description != "" {
{ transaction.Description }
} else {
&nbsp;
}
</p>
</div>
if transaction.Value < 0 {
<p class="mr-8 min-w-22 text-right text-red-700">{ types.FormatEuros(transaction.Value) }</p>
} else {
<p class="mr-8 w-22 text-right text-green-700">{ types.FormatEuros(transaction.Value) }</p>
}
<button
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
hx-target="closest #transaction"
hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2"
>
@svg.Edit()
<span>
Edit
</span>
</button>
<button
hx-delete={ "/transaction/" + transaction.Id.String() }
hx-target="closest #transaction"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete this transaction?"
class="button button-neglect px-1 flex items-center gap-2"
>
@svg.Delete()
<span>
Delete
</span>
</button>
<!-- if transaction.Party != "" { -->
<!-- <p class="col-start-1"> -->
<!-- { transaction.Party } -->
<!-- </p> -->
<!-- } -->
<!-- if transaction.Description != "" { -->
<!-- <p class="col-start-1"> -->
<!-- { transaction.Description } -->
<!-- </p> -->
<!-- } -->
<!-- <div class="col-start-2 col-end-3 flex gap-10 justify-end"> -->
<!-- <button -->
<!-- hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" } -->
<!-- hx-target="closest #transaction" -->
<!-- hx-swap="outerHTML" -->
<!-- class="flex items-center gap-2" -->
<!-- > -->
<!-- @svg.Edit() -->
<!-- Edit -->
<!-- </button> -->
<!-- <button -->
<!-- hx-delete={ "/transaction/" + transaction.Id.String() } -->
<!-- hx-target="closest #transaction" -->
<!-- hx-swap="outerHTML" -->
<!-- hx-confirm="Are you sure you want to delete this transaction?" -->
<!-- class="flex items-center gap-2" -->
<!-- > -->
<!-- @svg.Delete() -->
<!-- Delete -->
<!-- </button> -->
<!-- </div> -->
</div>
}
@@ -285,3 +287,11 @@ func formatFloat(balance int64) string {
euros := float64(balance) / 100
return fmt.Sprintf("%.2f", euros)
}
func getPageNumber(page string) string {
if page == "" {
return "1"
} else {
return page
}
}

View File

@@ -193,7 +193,9 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2"
>
@svg.Cancel()
<span class="h-4 w-4">
@svg.Cancel()
</span>
<span>
Cancel
</span>

View File

@@ -88,7 +88,9 @@ templ EditTreasureChest(treasureChest *types.TreasureChest, parents []*types.Tre
hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2"
>
@svg.Cancel()
<span class="h-4 w-4">
@svg.Cancel()
</span>
<span>
Cancel
</span>

View File

@@ -22,3 +22,9 @@ type DashboardMainChartEntry struct {
Value int64
Savings int64
}
type DashboardTreasureChest struct {
Name string
Value int64
Children []DashboardTreasureChest
}

View File

@@ -51,4 +51,5 @@ type TransactionItemsFilter struct {
AccountId string
TreasureChestId string
Error string
Page string
}

View File

@@ -22,7 +22,7 @@ func main() {
return
}
db, err := otelsqlx.Open("sqlite3", "./data/spend-sparrow.db",
db, err := otelsqlx.Open("sqlite3", "./data/spend-sparrow.db?_journal_mode=WAL",
otelsql.WithAttributes(semconv.DBSystemSqlite))
if err != nil {
slog.ErrorContext(ctx, "Could not open Database data.db", "err", err)

264
package-lock.json generated
View File

@@ -8,27 +8,11 @@
"name": "spend-sparrow",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"echarts": "^5.6.0"
},
"devDependencies": {
"@tailwindcss/cli": "4.1.10",
"htmx.org": "2.0.4",
"tailwindcss": "4.1.10"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
"@tailwindcss/cli": "4.1.12",
"echarts": "6.0.0",
"htmx.org": "2.0.6",
"tailwindcss": "4.1.12"
}
},
"node_modules/@isaacs/fs-minipass": {
@@ -45,18 +29,25 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@@ -69,16 +60,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
@@ -87,9 +68,9 @@
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"version": "0.3.30",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -407,44 +388,44 @@
}
},
"node_modules/@tailwindcss/cli": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.10.tgz",
"integrity": "sha512-TuO7IOUpTG1JeqtMQbQXjR4RIhfZ43mor/vpCp3S5X9h0WxUom5NYgxfNO0PiFoLMJ6/eYCelC7KGvUOmqqK6A==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.12.tgz",
"integrity": "sha512-2PyJ5MGh/6JPS+cEaAq6MGDx3UemkX/mJt+/phm7/VOpycpecwNnHuFZbbgx6TNK/aIjvFOhhTVlappM7tmqvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@parcel/watcher": "^2.5.1",
"@tailwindcss/node": "4.1.10",
"@tailwindcss/oxide": "4.1.10",
"enhanced-resolve": "^5.18.1",
"@tailwindcss/node": "4.1.12",
"@tailwindcss/oxide": "4.1.12",
"enhanced-resolve": "^5.18.3",
"mri": "^1.2.0",
"picocolors": "^1.1.1",
"tailwindcss": "4.1.10"
"tailwindcss": "4.1.12"
},
"bin": {
"tailwindcss": "dist/index.mjs"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz",
"integrity": "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz",
"integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
"jiti": "^2.5.1",
"lightningcss": "1.30.1",
"magic-string": "^0.30.17",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.10"
"tailwindcss": "4.1.12"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz",
"integrity": "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz",
"integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -456,24 +437,24 @@
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.10",
"@tailwindcss/oxide-darwin-arm64": "4.1.10",
"@tailwindcss/oxide-darwin-x64": "4.1.10",
"@tailwindcss/oxide-freebsd-x64": "4.1.10",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.10",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.10",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.10",
"@tailwindcss/oxide-linux-x64-musl": "4.1.10",
"@tailwindcss/oxide-wasm32-wasi": "4.1.10",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.10",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.10"
"@tailwindcss/oxide-android-arm64": "4.1.12",
"@tailwindcss/oxide-darwin-arm64": "4.1.12",
"@tailwindcss/oxide-darwin-x64": "4.1.12",
"@tailwindcss/oxide-freebsd-x64": "4.1.12",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.12",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.12",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.12",
"@tailwindcss/oxide-linux-x64-musl": "4.1.12",
"@tailwindcss/oxide-wasm32-wasi": "4.1.12",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.12",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.12"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.10.tgz",
"integrity": "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz",
"integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==",
"cpu": [
"arm64"
],
@@ -488,9 +469,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.10.tgz",
"integrity": "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz",
"integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==",
"cpu": [
"arm64"
],
@@ -505,9 +486,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.10.tgz",
"integrity": "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz",
"integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==",
"cpu": [
"x64"
],
@@ -522,9 +503,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.10.tgz",
"integrity": "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz",
"integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==",
"cpu": [
"x64"
],
@@ -539,9 +520,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.10.tgz",
"integrity": "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz",
"integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==",
"cpu": [
"arm"
],
@@ -556,9 +537,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.10.tgz",
"integrity": "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz",
"integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==",
"cpu": [
"arm64"
],
@@ -573,9 +554,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.10.tgz",
"integrity": "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz",
"integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==",
"cpu": [
"arm64"
],
@@ -590,9 +571,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.10.tgz",
"integrity": "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz",
"integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==",
"cpu": [
"x64"
],
@@ -607,9 +588,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.10.tgz",
"integrity": "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz",
"integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==",
"cpu": [
"x64"
],
@@ -624,9 +605,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.10.tgz",
"integrity": "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz",
"integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -642,11 +623,11 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@emnapi/wasi-threads": "^1.0.2",
"@napi-rs/wasm-runtime": "^0.2.10",
"@tybys/wasm-util": "^0.9.0",
"@emnapi/core": "^1.4.5",
"@emnapi/runtime": "^1.4.5",
"@emnapi/wasi-threads": "^1.0.4",
"@napi-rs/wasm-runtime": "^0.2.12",
"@tybys/wasm-util": "^0.10.0",
"tslib": "^2.8.0"
},
"engines": {
@@ -654,18 +635,18 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.4.3",
"version": "1.4.5",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.0.2",
"@emnapi/wasi-threads": "1.0.4",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.4.3",
"version": "1.4.5",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -675,7 +656,7 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.0.2",
"version": "1.0.4",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -685,7 +666,7 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.10",
"version": "0.2.12",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -693,11 +674,11 @@
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.9.0"
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.9.0",
"version": "0.10.0",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -714,9 +695,9 @@
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz",
"integrity": "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
"integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==",
"cpu": [
"arm64"
],
@@ -731,9 +712,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.10.tgz",
"integrity": "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz",
"integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==",
"cpu": [
"x64"
],
@@ -794,19 +775,20 @@
}
},
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
"zrender": "6.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -838,9 +820,9 @@
"license": "ISC"
},
"node_modules/htmx.org": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz",
"integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.6.tgz",
"integrity": "sha512-7ythjYneGSk3yCHgtCnQeaoF+D+o7U2LF37WU3O0JYv3gTZSicdEFiI/Ai/NJyC5ZpYJWMpUb11OC5Lr6AfAqA==",
"dev": true,
"license": "0BSD"
},
@@ -878,9 +860,9 @@
}
},
"node_modules/jiti": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
"dev": true,
"license": "MIT",
"bin": {
@@ -1247,16 +1229,16 @@
}
},
"node_modules/tailwindcss": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
"integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1298,6 +1280,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"dev": true,
"license": "0BSD"
},
"node_modules/yallist": {
@@ -1311,9 +1294,10 @@
}
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"

View File

@@ -11,9 +11,9 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@tailwindcss/cli": "4.1.10",
"htmx.org": "2.0.4",
"tailwindcss": "4.1.10",
"echarts": "5.6.0"
"@tailwindcss/cli": "4.1.12",
"htmx.org": "2.0.6",
"tailwindcss": "4.1.12",
"echarts": "6.0.0"
}
}

View File

@@ -1 +1,53 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 128 128"><path d="M93.46 39.45c6.71-1.49 15.45-8.15 16.78-11.43.78-1.92-3.11-4.92-4.15-6.13-2.38-2.76-1.42-4.12-.5-7.41 1.05-3.74-1.44-7.87-4.97-9.49s-7.75-1.11-11.3.47-6.58 4.12-9.55 6.62c-2.17-1.37-5.63-7.42-11.23-3.49-3.87 2.71-4.22 8.61-3.72 13.32 1.17 10.87 3.85 16.51 8.9 18.03 6.38 1.92 13.44.91 19.74-.49" style="fill:#ffca28"/><path d="M104.36 8.18c-.85 14.65-15.14 24.37-21.92 28.65l4.4 3.78s2.79.06 6.61-1.16c6.55-2.08 16.12-7.96 16.78-11.43.97-5.05-4.21-3.95-5.38-7.94-.61-2.11 2.97-6.1-.49-11.9M79.78 12.09s-2.55-2.61-4.44-3.8c-.94 1.77-1.61 3.69-1.94 5.67-.59 3.48 0 8.42 1.39 12.1.22.57 1.04.48 1.13-.12 1.2-7.91 3.86-13.85 3.86-13.85" style="fill:#e2a610"/><path d="M61.96 38.16S30.77 41.53 16.7 68.61s-2.11 43.5 10.55 49.48 44.56 8.09 65.31 3.17 25.94-15.12 24.97-24.97c-1.41-14.38-14.77-23.22-14.77-23.22s.53-17.76-13.25-29.29c-12.23-10.24-27.55-5.62-27.55-5.62" style="fill:#ffca28"/><path d="M74.76 83.73c-6.69-8.44-14.59-9.57-17.12-12.6-1.38-1.65-2.19-3.32-1.88-5.39.33-2.2 2.88-3.72 4.86-4.09 2.31-.44 7.82-.21 12.45 4.2 1.1 1.04.7 2.66.67 4.11-.08 3.11 4.37 6.13 7.97 3.53 3.61-2.61.84-8.42-1.49-11.24-1.76-2.13-8.14-6.82-16.07-7.56-2.23-.21-11.2-1.54-16.38 8.31-1.49 2.83-2.04 9.67 5.76 15.45 1.63 1.21 10.09 5.51 12.44 8.3 4.07 4.83 1.28 9.08-1.9 9.64-8.67 1.52-13.58-3.17-14.49-5.74-.65-1.83.03-3.81-.81-5.53-.86-1.77-2.62-2.47-4.48-1.88-6.1 1.94-4.16 8.61-1.46 12.28 2.89 3.93 6.44 6.3 10.43 7.6 14.89 4.85 22.05-2.81 23.3-8.42.92-4.11.82-7.67-1.8-10.97" style="fill:#6b4b46"/><path d="M71.16 48.99c-12.67 27.06-14.85 61.23-14.85 61.23" style="fill:none;stroke:#6b4b46;stroke-width:5;stroke-miterlimit:10"/><path d="M81.67 31.96c8.44 2.75 10.31 10.38 9.7 12.46-.73 2.44-10.08-7.06-23.98-6.49-4.86.2-3.45-2.78-1.2-4.5 2.97-2.27 7.96-3.91 15.48-1.47" style="fill:#6d4c41"/><path d="M81.67 31.96c8.44 2.75 10.31 10.38 9.7 12.46-.73 2.44-10.08-7.06-23.98-6.49-4.86.2-3.45-2.78-1.2-4.5 2.97-2.27 7.96-3.91 15.48-1.47" style="fill:#6b4b46"/><path d="M96.49 58.86c1.06-.73 4.62.53 5.62 7.5.49 3.41.64 6.71.64 6.71s-4.2-3.77-5.59-6.42c-1.75-3.35-2.43-6.59-.67-7.79" style="fill:#e2a610"/></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="28.384802mm"
height="31.749905mm"
viewBox="0 0 28.384802 31.749905"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-37.253301,-88.598061)">
<path
d="m 59.240389,97.978247 c 1.775354,-0.394229 4.087813,-2.156354 4.439709,-3.024187 0.206375,-0.508 -0.822855,-1.30175 -1.098021,-1.621896 -0.629709,-0.73025 -0.375709,-1.090083 -0.132292,-1.960562 0.277813,-0.989542 -0.381,-2.082271 -1.314979,-2.510896 -0.933979,-0.428625 -2.050521,-0.293688 -2.989792,0.124354 -0.939271,0.418042 -1.740958,1.090083 -2.526771,1.751542 -0.574145,-0.36248 -1.489604,-1.963209 -2.97127,-0.923396 -1.023938,0.717021 -1.116542,2.278062 -0.98425,3.52425 0.309562,2.876021 1.018645,4.368271 2.354791,4.770437 1.688042,0.508 3.556,0.240771 5.222875,-0.129646"
style="fill:#ffca28;stroke-width:0.264583"
id="path1-3" />
<path
d="m 62.124348,89.704727 c -0.224896,3.876145 -4.005792,6.447895 -5.799667,7.580312 l 1.164167,1.000125 c 0,0 0.738187,0.01588 1.748895,-0.306917 1.733021,-0.550333 4.265084,-2.106083 4.439709,-3.024187 0.256646,-1.336146 -1.113896,-1.045104 -1.423459,-2.100792 -0.161395,-0.558271 0.785813,-1.613958 -0.129645,-3.148541 m -6.503459,1.03452 c 0,0 -0.674687,-0.690562 -1.17475,-1.005416 -0.248708,0.468312 -0.425979,0.976312 -0.513291,1.500187 -0.156105,0.92075 0,2.227792 0.36777,3.201459 0.05821,0.150812 0.275167,0.127 0.29898,-0.03175 0.3175,-2.092855 1.021291,-3.66448 1.021291,-3.66448"
style="fill:#e2a610;stroke-width:0.264583"
id="path2-6" />
<path
d="m 50.906014,97.636935 c 0,0 -8.252354,0.891646 -11.975042,8.056565 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490104,-1.30175 6.863292,-4.0005 6.606646,-6.60664 -0.373062,-3.80471 -3.907896,-6.14363 -3.907896,-6.14363 0,0 0.140229,-4.699 -3.505729,-7.749647 -3.235854,-2.709333 -7.289271,-1.486958 -7.289271,-1.486958"
style="fill:#ffca28;stroke-width:0.264583"
id="path3-0" />
<path
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
style="fill:#6d4c41;stroke-width:0.264583"
id="path6-6" />
<path
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
style="fill:#6b4b46;stroke-width:0.264583"
id="path7-2" />
<path
d="m 60.042077,103.11381 c 0.280458,-0.19314 1.222375,0.14023 1.486958,1.98438 0.129646,0.90223 0.169333,1.77535 0.169333,1.77535 0,0 -1.11125,-0.99748 -1.47902,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
style="fill:#e2a610;stroke-width:0.264583"
id="path8-6" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="82.355011"
y="90.66716"
id="text4-9"
transform="rotate(20.578693)"><tspan
id="tspan4-2"
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
x="82.355011"
y="90.66716">$</tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

View File

@@ -1,15 +1,18 @@
// Initialize the echarts instance based on the prepared dom
async function init() {
const element = document.getElementById('graph')
async function initMainChart() {
const element = document.getElementById('main-chart')
if (element === null) {
return;
}
var myChart = echarts.init(element);
window.addEventListener('resize', function() {
myChart.resize();
});
try {
const response = await fetch("/dashboard/dataset");
const response = await fetch("/dashboard/main-chart");
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
@@ -22,17 +25,76 @@ async function init() {
'Sum of Savings: <span class="font-bold">' + params[1].data[1] + '</span> €'
};
const chart = myChart.setOption(option);
window.addEventListener('resize', function() {
myChart.resize();
});
myChart.setOption(option);
console.log("initialized charts");
console.log("initialized main-chart");
} catch (error) {
console.error(error.message);
}
// Display the chart using the configuration items and data just specified.
}
init();
async function initTreasureChests() {
const element = document.getElementById('treasure-chests')
if (element === null) {
return;
}
var myChart = echarts.init(element);
window.addEventListener('resize', function() {
myChart.resize();
});
try {
const response = await fetch("/dashboard/treasure-chests");
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const option = await response.json();
myChart.setOption(option);
console.log("initialized treasure-chests");
} catch (error) {
console.error(error.message);
}
}
async function initTreasureChest() {
const element = document.getElementById('treasure-chest')
if (element === null) {
return;
}
var myChart = echarts.init(element);
window.addEventListener('resize', function() {
myChart.resize();
});
const treasureChestSelect = document.getElementById('treasure-chest-id')
treasureChestSelect.addEventListener("change", async (e) => {
try {
const response = await fetch("/dashboard/treasure-chest?id="+e.target.value);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const option = await response.json();
option.tooltip.formatter = function (params) {
return new Date(params[0].data[0]).toLocaleString([], { day: 'numeric', month: 'short', year: 'numeric' }) +
'<br />' +
'Sum of Accounts: <span class="font-bold">' + params[0].data[1] + '</span> €'
};
myChart.setOption(option);
} catch (error) {
console.error(error.message);
}
});
console.log("initialized treasure-chest");
}
initMainChart();
initTreasureChests();
initTreasureChest();

11
static/js/layout.js Normal file
View File

@@ -0,0 +1,11 @@
document.addEventListener("DOMContentLoaded", () => {
menuButton.addEventListener("click", function (e) {
menu.showModal();
});
menuButtonClose.addEventListener("click", function (e) {
menu.close();
});
})

43
static/js/transaction.js Normal file
View File

@@ -0,0 +1,43 @@
// document.addEventListener("DOMContentLoaded", () => {
// if (!page || !page1 || !pagePrev1 || !pageNext1 || !page2 || !pagePrev2 || !pageNext2 || !transactionFilterForm) {
// return;
// }
//
//
// const scrollToTop = function() {
// window.scrollTo(0, 0);
// };
// const incPage = function() {
// const currPage = Number(page.value);
// var nextPage = currPage
// if (currPage > 1) {
// nextPage -= 1;
// page.value = nextPage;
// transactionFilterForm.dispatchEvent(new Event('change'));
// }
// page1.textContent = nextPage;
// page2.textContent = nextPage;
// scrollToTop();
// };
// const decPage = function() {
// const currPage = Number(page.value);
// var nextPage = currPage + 1;
// page.value = nextPage;
// transactionFilterForm.dispatchEvent(new Event('change'));
// page1.textContent = nextPage;
// page2.textContent = nextPage;
// scrollToTop();
// };
//
//
//
// pagePrev1.addEventListener("click", incPage);
// pagePrev2.addEventListener("click", incPage);
//
// pageNext1.addEventListener("click", decPage);
// pageNext2.addEventListener("click", decPage);
//
// console.log("initialized pagination");
// })

71
static/logo.svg Normal file
View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="96.820343mm"
height="31.749899mm"
viewBox="0 0 96.820343 31.749899"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g7"
transform="translate(-38.090175,-77.467441)">
<g
id="g8"
transform="translate(-38.797122,-28.178962)">
<path
d="m 98.874384,115.02659 c 1.775356,-0.39423 4.087816,-2.15635 4.439706,-3.02419 0.20638,-0.508 -0.82285,-1.30175 -1.09802,-1.62189 -0.62971,-0.73025 -0.37571,-1.09009 -0.13229,-1.96057 0.27781,-0.98954 -0.381,-2.08227 -1.31498,-2.51089 -0.933978,-0.42863 -2.05052,-0.29369 -2.989791,0.12435 -0.939271,0.41804 -1.740958,1.09009 -2.526771,1.75154 -0.574145,-0.36248 -1.489604,-1.96321 -2.97127,-0.92339 -1.023938,0.71702 -1.116542,2.27806 -0.98425,3.52425 0.309562,2.87602 1.018645,4.36827 2.354791,4.77043 1.688042,0.508 3.556,0.24078 5.222875,-0.12964"
style="fill:#ffca28;stroke-width:0.264583"
id="path1-3-3" />
<path
d="m 101.75834,106.75307 c -0.22489,3.87614 -4.005789,6.44789 -5.799664,7.58031 l 1.164167,1.00013 c 0,0 0.738187,0.0159 1.748895,-0.30692 1.733022,-0.55033 4.265082,-2.10608 4.439712,-3.02419 0.25664,-1.33614 -1.1139,-1.0451 -1.42346,-2.10079 -0.1614,-0.55827 0.78581,-1.61396 -0.12965,-3.14854 m -6.503456,1.03452 c 0,0 -0.674687,-0.69056 -1.17475,-1.00542 -0.248708,0.46832 -0.425979,0.97632 -0.513291,1.50019 -0.156105,0.92075 0,2.22779 0.36777,3.20146 0.05821,0.15081 0.275167,0.127 0.29898,-0.0317 0.3175,-2.09286 1.021291,-3.66448 1.021291,-3.66448"
style="fill:#e2a610;stroke-width:0.264583"
id="path2-6-6" />
<path
d="m 90.540009,114.68528 c 0,0 -8.252354,0.89164 -11.975042,8.05656 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490101,-1.30175 6.863291,-4.0005 6.606641,-6.60664 -0.37306,-3.80471 -3.90789,-6.14363 -3.90789,-6.14363 0,0 0.14023,-4.699 -3.50573,-7.74964 -3.235854,-2.70934 -7.289271,-1.48696 -7.289271,-1.48696"
style="fill:#ffca28;stroke-width:0.264583"
id="path3-0-1" />
<path
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
style="fill:#6d4c41;stroke-width:0.264583"
id="path6-6-2" />
<path
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
style="fill:#6b4b46;stroke-width:0.264583"
id="path7-2-9" />
<path
d="m 99.676072,120.16215 c 0.280458,-0.19314 1.222378,0.14023 1.486958,1.98438 0.12965,0.90223 0.16933,1.77535 0.16933,1.77535 0,0 -1.11125,-0.99748 -1.479017,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
style="fill:#e2a610;stroke-width:0.264583"
id="path8-6-3" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="125.45235"
y="92.696564"
id="text4-9-1"
transform="rotate(20.578693)"><tspan
id="tspan4-2-9"
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
x="125.45235"
y="92.696564">$</tspan></text>
</g>
<g
id="layer2"
transform="translate(-1.4293676,48.496402)">
<text
xml:space="preserve"
style="font-size:17.6389px;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="57.635151"
y="55.655094"
id="text1"><tspan
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.6389px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:-0.529167px;fill:#4d4d4d;stroke:none;stroke-width:0.264583"
x="57.635151"
y="55.655094">pendSparrow</tspan></text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -756,7 +756,7 @@ func TestIntegrationAuth(t *testing.T) {
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/", nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/dashboard", nil)
require.NoError(t, err)
req.Header.Set("Cookie", "id="+sessionId)
resp, err := httpClient.Do(req)
@@ -1862,7 +1862,6 @@ func TestIntegrationAccount(t *testing.T) {
">": 400,
"/": 400,
"\\": 400,
"?": 400,
":": 400,
"*": 400,
"|": 400,