Compare commits
80 Commits
d3c7a96ada
...
111-transa
| Author | SHA1 | Date | |
|---|---|---|---|
|
061d63a8ad
|
|||
| d0faee2950 | |||
|
01d459e913
|
|||
| cee533694c | |||
| f6283c6ab3 | |||
| e48d11b818 | |||
| b9150334ee | |||
| d20981beaa | |||
| 3c95abe59c | |||
| fad2bd3928 | |||
| c75b99ea9d | |||
| 56737a4156 | |||
| ab425d759c | |||
| 283679fc4f | |||
| e0802cf232 | |||
| 6577dbb297 | |||
| 7e37c24b07 | |||
| 3f3edbb8ad | |||
| 16429f1950 | |||
| 82a9fd8220 | |||
| 192e6b7f50 | |||
| 57377f9c27 | |||
| 66227c5818 | |||
| 6e51e3c8b3 | |||
| 6c916aecb4 | |||
| 8575fbf56e | |||
| f820fcdfeb | |||
| f6e58b7afc | |||
| 93e669b038 | |||
| d037317aab | |||
|
0517e7ec89
|
|||
| 867c0ca1cd | |||
| ddcbfaa075 | |||
| 4583c0a70e | |||
|
380854272a
|
|||
|
6bc9e0666b
|
|||
| 9fa554c60a | |||
| 763c952cbe | |||
| fce669146f | |||
| 06219d1fd3 | |||
| 9fac68d7ae | |||
| 8afd48b981 | |||
|
25568591fd
|
|||
| e1551c1fa3 | |||
| e8b3d3e16c | |||
| 59288d4544 | |||
| a2d1f22d46 | |||
| 0e150b3d7d | |||
| 19567313bd | |||
| 42f1cfc07f | |||
| 0276bc6a4c | |||
| d6c8559d4c | |||
| 38cdd96b6f | |||
| cb49494e60 | |||
| 3ffe7514e2 | |||
| d13a387303 | |||
| a398d275f5 | |||
| 23b97a9cac | |||
| 4b74a9b6d4 | |||
| c67f232e9b | |||
| 93727ee49a | |||
| 1a79df9423 | |||
| 582d265fd5 | |||
| f094767582 | |||
| 440fed9ed1 | |||
|
6e1d24eef7
|
|||
| f37b50515b | |||
| b2a512d186 | |||
| 4a28fb5ca4 | |||
| e5f98c1fb0 | |||
| 3072df6507 | |||
| 9f35ca7476 | |||
| 472ab68986 | |||
|
2fd2200ac2
|
|||
| a58ddb7a1d | |||
|
147d57f6e5
|
|||
|
d064626197
|
|||
|
72869e5c68
|
|||
|
3120c19669
|
|||
|
c9bf320611
|
@@ -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
|
||||
|
||||
@@ -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
2
.gitignore
vendored
@@ -25,7 +25,7 @@ go.work.sum
|
||||
# env file
|
||||
.env
|
||||
|
||||
*.db
|
||||
data/
|
||||
secrets/
|
||||
|
||||
node_modules/
|
||||
|
||||
@@ -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,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
|
||||
|
||||
10
Readme.md
10
Readme.md
@@ -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.
|
||||
|
||||
|
||||
|
||||
188
assest-source/logo-inkscape.svg
Normal file
188
assest-source/logo-inkscape.svg
Normal 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
6
dev.sh
@@ -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
54
go.mod
@@ -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
104
go.sum
@@ -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=
|
||||
|
||||
33
input.css
33
input.css
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
type WrappedWriter struct {
|
||||
http.ResponseWriter
|
||||
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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=""> -->
|
||||
<!-- < -->
|
||||
<!-- </button> -->
|
||||
<!-- <span class="">Page: <span class="" id="page1">{ getPageNumber(filter.Page) }</span></span> -->
|
||||
<!-- <button id="pageNext1" class=""> -->
|
||||
<!-- > -->
|
||||
<!-- </button> -->
|
||||
<!-- </div> -->
|
||||
@items
|
||||
<!-- <div class=""> -->
|
||||
<!-- <button id="pagePrev2" class=""> -->
|
||||
<!-- < -->
|
||||
<!-- </button> -->
|
||||
<!-- <span class="">Page: <span class="" id="page2">{ getPageNumber(filter.Page) }</span></span> -->
|
||||
<!-- <button id="pageNext2" class=""> -->
|
||||
<!-- > -->
|
||||
<!-- </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 {
|
||||
|
||||
}
|
||||
</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 {
|
||||
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">
|
||||
if transaction.Party != "" {
|
||||
{ transaction.Party }
|
||||
} else {
|
||||
|
||||
}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
if transaction.Description != "" {
|
||||
{ transaction.Description }
|
||||
} else {
|
||||
|
||||
}
|
||||
</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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -22,3 +22,9 @@ type DashboardMainChartEntry struct {
|
||||
Value int64
|
||||
Savings int64
|
||||
}
|
||||
|
||||
type DashboardTreasureChest struct {
|
||||
Name string
|
||||
Value int64
|
||||
Children []DashboardTreasureChest
|
||||
}
|
||||
|
||||
@@ -51,4 +51,5 @@ type TransactionItemsFilter struct {
|
||||
AccountId string
|
||||
TreasureChestId string
|
||||
Error string
|
||||
Page string
|
||||
}
|
||||
|
||||
2
main.go
2
main.go
@@ -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
264
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
@@ -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
11
static/js/layout.js
Normal 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
43
static/js/transaction.js
Normal 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
71
static/logo.svg
Normal 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 |
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user