Compare commits
46 Commits
fa2d178d82
...
prod
| Author | SHA1 | Date | |
|---|---|---|---|
| 14018c667d | |||
| 092d5b027e | |||
| 7156d8f460 | |||
| 8ffae93dd9 | |||
| 7a8167cb3c | |||
| 08b0a7b6fa | |||
| 41d3d04fd7 | |||
| 9d094244ad | |||
|
ae00022f49
|
|||
| 0775f81142 | |||
| 8468fd4293 | |||
| 5afaf3ecdb | |||
| a5d21bfc66 | |||
| d7dcfa7088 | |||
| 2423ed6314 | |||
| 77c901fb78 | |||
| 7de9aa6452 | |||
| 0bc8812a92 | |||
| e82295a4c6 | |||
| d7e6993049 | |||
| 3cfc007f36 | |||
| 7c7566497b | |||
| b0303c224d | |||
| 94de96847c | |||
| c1ee572856 | |||
| 8ee63c6b90 | |||
|
0d56d86a41
|
|||
|
a570c44d75
|
|||
|
5af5ab2a0c
|
|||
|
f1e0c1c1c2
|
|||
|
b13712b0df
|
|||
|
70d6110bc4
|
|||
|
faf28b559a
|
|||
|
39f196341f
|
|||
|
5efba04f1b
|
|||
|
238ec6d55d
|
|||
| 76fdafc709 | |||
|
7b95216987
|
|||
|
209af10fd4
|
|||
|
029c01cd32
|
|||
|
cee01c9a29
|
|||
| 43e4334201 | |||
|
2bbfe7b175
|
|||
|
b5ab697cca
|
|||
|
ada411e1eb
|
|||
|
818dab401e
|
@@ -10,6 +10,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- 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
@@ -36,3 +36,5 @@ tmp/
|
||||
|
||||
mocks/*
|
||||
!mocks/default.go
|
||||
|
||||
arch.png
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.5@sha256:31c1e53dfc1cc2d269deec9c83f58729fa3c53dc9a576f6426109d1e319e9e9a AS builder_go
|
||||
FROM golang:1.26.0@sha256:c83e68f3ebb6943a2904fa66348867d108119890a2c6a2e6f07b38d0eb6c25c5 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:24.12.0@sha256:b52a8d1206132b36d60e51e413d9a81336e8a0206d3b648cabd6d5a49c4c0f54 AS builder_node
|
||||
FROM node:24.13.1@sha256:00e9195ebd49985a6da8921f419978d85dfe354589755192dc090425ce4da2f7 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:13.2@sha256:c71b05eac0b20adb4cdcc9f7b052227efd7da381ad10bb92f972e8eae7c6cdc9
|
||||
FROM debian:13.3@sha256:2c91e484d93f0830a7e05a2b9d92a7b102be7cab562198b984a84fdbc7806d91
|
||||
WORKDIR /spend-sparrow
|
||||
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
|
||||
COPY migration ./migration
|
||||
|
||||
32
arch.gv
Normal file
32
arch.gv
Normal file
@@ -0,0 +1,32 @@
|
||||
digraph {
|
||||
Tag
|
||||
Transaction
|
||||
Account
|
||||
BankConnection
|
||||
Analytics
|
||||
|
||||
// Buckets
|
||||
Budget
|
||||
RecurringCost
|
||||
SavingGoal
|
||||
|
||||
|
||||
// Analytics -> {
|
||||
// Budget
|
||||
// RecurringCost
|
||||
// SavingGoal
|
||||
// Tag
|
||||
// Transaction
|
||||
// Account
|
||||
// } [label="uses"]
|
||||
|
||||
BankConnection -> Transaction [label="imports into"]
|
||||
BankConnection -> Account [label="references"]
|
||||
|
||||
Transaction -> Account [label="references"]
|
||||
Transaction -> Tag [label="references"]
|
||||
|
||||
Budget -> Tag [label="references"]
|
||||
RecurringCost -> Tag [label="references"]
|
||||
SavingGoal -> Tag [label="references"]
|
||||
}
|
||||
48
go.mod
48
go.mod
@@ -2,7 +2,7 @@ module spend-sparrow
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.25.5
|
||||
toolchain go1.26.0
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.3.977
|
||||
@@ -10,23 +10,23 @@ require (
|
||||
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.32
|
||||
github.com/mattn/go-sqlite3 v1.14.34
|
||||
github.com/stretchr/testify v1.11.1
|
||||
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.14.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
|
||||
go.opentelemetry.io/otel v1.39.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0
|
||||
go.opentelemetry.io/otel/log v0.15.0
|
||||
go.opentelemetry.io/otel/sdk v1.39.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.15.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0
|
||||
go.opentelemetry.io/otel/trace v1.39.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/net v0.48.0
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.15.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0
|
||||
go.opentelemetry.io/otel/log v0.16.0
|
||||
go.opentelemetry.io/otel/sdk v1.40.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0
|
||||
go.opentelemetry.io/otel/trace v1.40.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/net v0.50.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -36,18 +36,18 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // 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.27.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
96
go.sum
96
go.sum
@@ -25,8 +25,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.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
@@ -38,8 +38,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.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -54,56 +54,56 @@ 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
|
||||
go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY=
|
||||
go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE=
|
||||
go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.15.0 h1:yOYhGNPZseueTTvWp5iBD3/CthrmvayUXYEX862dDi4=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.15.0/go.mod h1:CvaNVqIfcybc+7xqZNubbE+26K6P7AKZF/l0lE2kdCk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
|
||||
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
|
||||
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -13,7 +13,9 @@ templ template(accounts []*Account) {
|
||||
hx-swap="afterbegin"
|
||||
class="ml-auto button button-primary px-2 flex-1 flex items-center gap-2 justify-center"
|
||||
>
|
||||
@svg.Plus()
|
||||
<div class="w-3">
|
||||
@svg.Plus()
|
||||
</div>
|
||||
<p>New Account</p>
|
||||
</button>
|
||||
<div id="account-items" class="my-6 flex flex-col items-center">
|
||||
|
||||
@@ -27,9 +27,9 @@ func NewDbSqlite(db *sqlx.DB) *DbSqlite {
|
||||
|
||||
func (db DbSqlite) Insert(ctx context.Context, budget Budget) (*Budget, error) {
|
||||
r, err := db.db.ExecContext(ctx, `
|
||||
INSERT INTO budget (id, user_id, description, value, created_at, created_by, updated_at, updated_by)
|
||||
INSERT INTO budget (id, user_id, name, value, created_at, created_by, updated_at, updated_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
budget.Id, budget.UserId, budget.Description, budget.Value, budget.CreatedAt, budget.CreatedBy, budget.UpdatedAt, budget.UpdatedBy,
|
||||
budget.Id, budget.UserId, budget.Name, budget.Value, budget.CreatedAt, budget.CreatedBy, budget.UpdatedAt, budget.UpdatedBy,
|
||||
)
|
||||
err = core.TransformAndLogDbError(ctx, "budget", r, err)
|
||||
if err != nil {
|
||||
@@ -42,13 +42,13 @@ func (db DbSqlite) Insert(ctx context.Context, budget Budget) (*Budget, error) {
|
||||
func (db DbSqlite) Update(ctx context.Context, budget Budget) (*Budget, error) {
|
||||
_, err := db.db.ExecContext(ctx, `
|
||||
UPDATE budget
|
||||
SET description = ?,
|
||||
SET name = ?,
|
||||
value = ?,
|
||||
updated_at = ?,
|
||||
updated_by = ?
|
||||
WHERE user_id = ?
|
||||
AND id = ?`,
|
||||
budget.Description, budget.Value, budget.UpdatedAt, budget.UpdatedBy, budget.UserId, budget.Id)
|
||||
budget.Name, budget.Value, budget.UpdatedAt, budget.UpdatedBy, budget.UserId, budget.Id)
|
||||
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "SQL error UpdateUser", "err", err)
|
||||
|
||||
@@ -2,7 +2,6 @@ package budget
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
"spend-sparrow/internal/core"
|
||||
@@ -33,9 +32,10 @@ func NewHandler(s Service, r *core.Render) Handler {
|
||||
|
||||
func (h HandlerImpl) Handle(r *http.ServeMux) {
|
||||
r.Handle("GET /budget", h.handlePage())
|
||||
// r.Handle("GET /budget/{id}", h.handleTransactionItemComp())
|
||||
r.Handle("GET /budget/new", h.handleNew())
|
||||
r.Handle("GET /budget/{id}", h.handleEdit())
|
||||
r.Handle("POST /budget/{id}", h.handlePost())
|
||||
// r.Handle("DELETE /budget/{id}", h.handleDelete())
|
||||
r.Handle("DELETE /budget/{id}", h.handleDelete())
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handlePage() http.HandlerFunc {
|
||||
@@ -48,13 +48,55 @@ func (h HandlerImpl) handlePage() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// transactions, err := h.s.GetAll(r.Context(), user)
|
||||
// if err != nil {
|
||||
// core.HandleError(w, r, err)
|
||||
// return
|
||||
// }
|
||||
budgets, err := h.s.GetAll(r.Context(), user)
|
||||
if err != nil {
|
||||
h.r.RenderLayout(r, w, core.ErrorComp(err), user)
|
||||
return
|
||||
}
|
||||
|
||||
comp := Page()
|
||||
comp := page(budgets)
|
||||
h.r.RenderLayout(r, w, comp, user)
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handleNew() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
core.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
comp := editNew()
|
||||
h.r.RenderLayout(r, w, comp, user)
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handleEdit() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
core.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
budget, err := h.s.Get(r.Context(), user, id)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
comp := edit(*budget)
|
||||
h.r.RenderLayout(r, w, comp, user)
|
||||
}
|
||||
}
|
||||
@@ -91,29 +133,52 @@ func (h HandlerImpl) handlePost() http.HandlerFunc {
|
||||
value := int64(math.Round(valueF * DECIMALS_MULTIPLIER))
|
||||
|
||||
input := Budget{
|
||||
Id: id,
|
||||
Value: value,
|
||||
Description: r.FormValue("description"),
|
||||
Id: id,
|
||||
Name: r.FormValue("name"),
|
||||
Value: value,
|
||||
}
|
||||
|
||||
var budget *Budget
|
||||
if idStr == "new" {
|
||||
budget, err = h.s.Add(r.Context(), user, input)
|
||||
_, err = h.s.Add(r.Context(), user, input)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
budget, err = h.s.Update(r.Context(), user, input)
|
||||
_, err = h.s.Update(r.Context(), user, input)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// To disable unused variable
|
||||
slog.Info("test", "item", budget)
|
||||
comp := Item()
|
||||
h.r.Render(r, w, comp)
|
||||
core.DoRedirect(w, r, "/budget")
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handleDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
core.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := r.PathValue("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
err = h.s.Delete(r.Context(), user, id)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
core.DoRedirect(w, r, "/budget")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,15 +53,24 @@ func (s ServiceImpl) Add(ctx context.Context, user *auth_types.User, budget Budg
|
||||
return s.db.Insert(ctx, budget)
|
||||
}
|
||||
|
||||
func (s ServiceImpl) Update(ctx context.Context, user *auth_types.User, budget Budget) (*Budget, error) {
|
||||
func (s ServiceImpl) Update(ctx context.Context, user *auth_types.User, input Budget) (*Budget, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
budget, err := s.Get(ctx, user, input.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
budget.Name = input.Name
|
||||
budget.Value = input.Value
|
||||
|
||||
if user.Id != budget.UserId {
|
||||
return nil, core.ErrBadRequest
|
||||
}
|
||||
|
||||
isValid := s.isBudgetValid(budget)
|
||||
isValid := s.isBudgetValid(*budget)
|
||||
if !isValid {
|
||||
return nil, core.ErrBadRequest
|
||||
}
|
||||
@@ -70,7 +79,7 @@ func (s ServiceImpl) Update(ctx context.Context, user *auth_types.User, budget B
|
||||
now := s.clock.Now()
|
||||
budget.UpdatedAt = &now
|
||||
|
||||
return s.db.Update(ctx, budget)
|
||||
return s.db.Update(ctx, *budget)
|
||||
}
|
||||
|
||||
func (s ServiceImpl) Delete(ctx context.Context, user *auth_types.User, budgetId uuid.UUID) error {
|
||||
@@ -98,12 +107,11 @@ func (s ServiceImpl) GetAll(ctx context.Context, user *auth_types.User) ([]Budge
|
||||
}
|
||||
|
||||
func (s ServiceImpl) isBudgetValid(budget Budget) bool {
|
||||
if budget.Description != "" {
|
||||
err := core.ValidateString(budget.Description, "description")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
} else if budget.Value < 0 {
|
||||
err := core.ValidateString(budget.Name, "name")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if budget.Value < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,138 @@
|
||||
package budget
|
||||
|
||||
templ Page() {
|
||||
<div></div>
|
||||
import (
|
||||
"spend-sparrow/internal/core"
|
||||
"spend-sparrow/internal/tag"
|
||||
"spend-sparrow/internal/template/svg"
|
||||
)
|
||||
|
||||
templ page(budgets []Budget) {
|
||||
@core.Breadcrumb([]string{"Home", "Budget"}, []string{"/", "/budget"})
|
||||
<div class="flex flex-wrap gap-20 text-xl mt-10 justify-center">
|
||||
@newItem()
|
||||
for _,budget:=range(budgets ) {
|
||||
@item(budget)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Items() {
|
||||
<div></div>
|
||||
templ editNew() {
|
||||
<div class="flex flex-col h-full">
|
||||
@core.Breadcrumb([]string{"Home", "Budget", "New"}, []string{"/", "/budget", "/budget/new"})
|
||||
<div class="flex justify-center items-center flex-1">
|
||||
<form
|
||||
hx-post={ "/budget/new" }
|
||||
class="grid grid-cols-4 items-center gap-4 max-w-2xl"
|
||||
>
|
||||
<label for="timestamp" class="text-sm text-gray-500">Name</label>
|
||||
<input
|
||||
autofocus
|
||||
name="name"
|
||||
type="text"
|
||||
class="bg-white input datetime col-span-3"
|
||||
/>
|
||||
<label for="value" class="text-sm text-gray-500">Value</label>
|
||||
<input
|
||||
name="value"
|
||||
type="number"
|
||||
class="bg-white input col-span-3"
|
||||
/>
|
||||
<div class="flex gap-6 justify-end col-span-4">
|
||||
<a href="/budget" class="col-start-3 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||
<span class="h-4 w-4">
|
||||
@svg.Cancel()
|
||||
</span>
|
||||
<span>
|
||||
Cancel
|
||||
</span>
|
||||
</a>
|
||||
<button type="submit" class="col-start-4 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||
@svg.Save()
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Edit() {
|
||||
<div></div>
|
||||
templ edit(budget Budget) {
|
||||
<div class="flex flex-col h-full">
|
||||
@core.Breadcrumb([]string{"Home", "Budget", budget.Name}, []string{"/", "/budget", "/budget/" + budget.Id.String()})
|
||||
<div class="flex justify-center items-center flex-1">
|
||||
<form
|
||||
hx-post={ "/budget/" + budget.Id.String() }
|
||||
class="grid grid-cols-4 items-center gap-4 max-w-2xl"
|
||||
>
|
||||
<label for="timestamp" class="text-sm text-gray-500">Name</label>
|
||||
<input
|
||||
autofocus
|
||||
name="name"
|
||||
type="text"
|
||||
value={ budget.Name }
|
||||
class="bg-white input datetime col-span-3"
|
||||
/>
|
||||
<label for="value" class="text-sm text-gray-500">Value</label>
|
||||
<input
|
||||
name="value"
|
||||
type="number"
|
||||
value={ budget.Value / 100 }
|
||||
class="bg-white input col-span-3"
|
||||
/>
|
||||
<label for="tag" class="text-sm text-gray-500">Tags</label>
|
||||
@tag.InlineEditInput("col-span-3")
|
||||
<div class="flex flex-row-reverse gap-6 justify-end col-span-4">
|
||||
<button type="submit" class="col-start-4 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||
@svg.Save()
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
<a href="/budget" class="col-start-3 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||
<span class="h-4 w-4">
|
||||
@svg.Cancel()
|
||||
</span>
|
||||
<span>
|
||||
Cancel
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
hx-delete={ "/budget/" + budget.Id.String() }
|
||||
hx-confirm={ "Do you really want to delete '" + budget.Name + "'" }
|
||||
class="col-start-4 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-red-50 rounded-lg hover:underline flex items-center gap-2 justify-center"
|
||||
>
|
||||
@svg.Delete()
|
||||
<span>
|
||||
Delete
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Item() {
|
||||
<div></div>
|
||||
templ newItem() {
|
||||
<a
|
||||
href="/budget/new"
|
||||
class="p-5 w-64 h-64 flex gap-10 active:bg-gray-200 flex-col justify-center items-center hover:bg-gray-100 transition-all cursor-pointer rounded-lg bg-gray-50 border-1 border-gray-300"
|
||||
>
|
||||
New Budget
|
||||
<div class="w-10">
|
||||
@svg.Plus()
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
templ item(budget Budget) {
|
||||
<a href={ "/budget/" + budget.Id.String() } class="p-5 w-64 h-64 flex gap-10 active:bg-gray-200 flex-col justify-center items-center hover:bg-gray-100 transition-all cursor-pointer rounded-lg bg-gray-50 border-1 border-gray-300">
|
||||
<span>
|
||||
{ budget.Name }
|
||||
</span>
|
||||
<span>
|
||||
{ core.FormatEuros(budget.Value) }
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ type Budget struct {
|
||||
Id uuid.UUID `db:"id"`
|
||||
UserId uuid.UUID `db:"user_id"`
|
||||
|
||||
Description string `db:"description"`
|
||||
Value int64 `db:"value"`
|
||||
Name string `db:"name"`
|
||||
Value int64 `db:"value"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
CreatedBy uuid.UUID `db:"created_by"`
|
||||
|
||||
13
internal/core/breadcrumb.templ
Normal file
13
internal/core/breadcrumb.templ
Normal file
@@ -0,0 +1,13 @@
|
||||
package core
|
||||
|
||||
templ Breadcrumb(elements []string, links []string) {
|
||||
<div class="flex gap-5 mb-10 text-lg items-center">
|
||||
for i, element := range(elements) {
|
||||
if (i>0) {
|
||||
<span class="text-gray-500">></span><a class="p-2 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline" href={ links[i] }>{ element }</a>
|
||||
} else {
|
||||
<a class="p-2 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline" href={ links[i] }>{ element }</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package core
|
||||
|
||||
import "spend-sparrow/internal/template/svg"
|
||||
import (
|
||||
"spend-sparrow/internal/template/svg"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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"
|
||||
@@ -87,10 +90,17 @@ templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path str
|
||||
|
||||
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>
|
||||
<a class={ layoutLinkClass(path == "/budget") } href="/budget">Budget</a>
|
||||
<a class={ layoutLinkClass(strings.HasPrefix(path, "/dashboard")) } href="/dashboard">Dashboard</a>
|
||||
<a class={ layoutLinkClass(strings.HasPrefix(path, "/transaction")) } href="/transaction">Transaction</a>
|
||||
<a class={ layoutLinkClass(strings.HasPrefix(path, "/treasurechest")) } href="/treasurechest">Treasure Chest</a>
|
||||
<a class={ layoutLinkClass(strings.HasPrefix(path, "/account")) } href="/account">Account</a>
|
||||
<a class={ layoutLinkClass(strings.HasPrefix(path, "/budget")) } href="/budget">Budget</a>
|
||||
<a class={ layoutLinkClass(strings.HasPrefix(path, "/tag")) } href="/tag">Tag</a>
|
||||
</nav>
|
||||
}
|
||||
|
||||
templ ErrorComp(err error) {
|
||||
<div>
|
||||
The following error occured: { err.Error() }
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"spend-sparrow/internal/dashboard"
|
||||
"spend-sparrow/internal/handler"
|
||||
"spend-sparrow/internal/handler/middleware"
|
||||
"spend-sparrow/internal/tag"
|
||||
"spend-sparrow/internal/transaction"
|
||||
"spend-sparrow/internal/transaction_recurring"
|
||||
"spend-sparrow/internal/treasure_chest"
|
||||
@@ -30,8 +31,8 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu
|
||||
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
otelEnabled := core.IsOtelEnabled(env)
|
||||
if otelEnabled {
|
||||
isOtelEnabled := core.IsOtelEnabled(env)
|
||||
if isOtelEnabled {
|
||||
// use context.Background(), otherwise the shutdown can't be called, as the context is already cancelled
|
||||
otelShutdown, err := setupOTelSDK(context.Background())
|
||||
if err != nil {
|
||||
@@ -67,7 +68,7 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu
|
||||
// init server
|
||||
httpServer := &http.Server{
|
||||
Addr: ":" + serverSettings.Port,
|
||||
Handler: createHandlerWithServices(ctx, database, serverSettings),
|
||||
Handler: createHandlerWithServices(ctx, database, serverSettings, isOtelEnabled),
|
||||
ReadHeaderTimeout: 2 * time.Second,
|
||||
}
|
||||
go startServer(ctx, httpServer)
|
||||
@@ -106,11 +107,12 @@ func shutdownServer(ctx context.Context, s *http.Server, wg *sync.WaitGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *core.Settings) http.Handler {
|
||||
func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *core.Settings, isOtelEnabled bool) http.Handler {
|
||||
var router = http.NewServeMux()
|
||||
|
||||
authDb := authentication.NewDbSqlite(d)
|
||||
budgetDb := budget.NewDbSqlite(d)
|
||||
tagDb := tag.NewDbSqlite(d)
|
||||
|
||||
randomService := core.NewRandom()
|
||||
clockService := core.NewClock()
|
||||
@@ -123,6 +125,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
||||
transactionRecurringService := transaction_recurring.NewService(d, randomService, clockService)
|
||||
dashboardService := dashboard.NewService(d)
|
||||
budgetService := budget.NewService(budgetDb, randomService, clockService)
|
||||
tagService := tag.NewService(tagDb, randomService, clockService)
|
||||
|
||||
render := core.NewRender()
|
||||
indexHandler := handler.NewIndex(render, clockService)
|
||||
@@ -133,6 +136,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
||||
transactionHandler := transaction.NewHandler(transactionService, accountService, treasureChestService, render)
|
||||
transactionRecurringHandler := transaction_recurring.NewHandler(transactionRecurringService, render)
|
||||
budgetHandler := budget.NewHandler(budgetService, render)
|
||||
tagHandler := tag.NewHandler(tagService, render)
|
||||
|
||||
go dailyTaskTimer(ctx, transactionService, authService)
|
||||
|
||||
@@ -144,6 +148,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
||||
transactionHandler.Handle(router)
|
||||
transactionRecurringHandler.Handle(router)
|
||||
budgetHandler.Handle(router)
|
||||
tagHandler.Handle(router)
|
||||
|
||||
// Serve static files (CSS, JS and images)
|
||||
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
||||
@@ -155,7 +160,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
||||
middleware.CrossSiteRequestForgery(authService),
|
||||
middleware.Authenticate(authService),
|
||||
middleware.Gzip,
|
||||
middleware.Log,
|
||||
middleware.Log(isOtelEnabled),
|
||||
)
|
||||
|
||||
wrapper = otelhttp.NewHandler(wrapper, "http.request")
|
||||
|
||||
@@ -17,21 +17,28 @@ func (w *WrappedWriter) WriteHeader(code int) {
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func Log(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
func Log(enabled bool) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !enabled {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
wrapped := &WrappedWriter{
|
||||
ResponseWriter: w,
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
next.ServeHTTP(wrapped, r)
|
||||
start := time.Now()
|
||||
|
||||
slog.InfoContext(r.Context(), "request",
|
||||
"remoteAddr", r.RemoteAddr,
|
||||
"status", wrapped.StatusCode,
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"duration", time.Since(start).String())
|
||||
})
|
||||
wrapped := &WrappedWriter{
|
||||
ResponseWriter: w,
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
next.ServeHTTP(wrapped, r)
|
||||
|
||||
slog.InfoContext(r.Context(), "request",
|
||||
"remoteAddr", r.RemoteAddr,
|
||||
"status", wrapped.StatusCode,
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"duration", time.Since(start).String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,6 @@ import (
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
)
|
||||
|
||||
var (
|
||||
otelEndpoint = "otel-collector:4317"
|
||||
)
|
||||
|
||||
// setupOTelSDK bootstraps the OpenTelemetry pipeline.
|
||||
@@ -50,10 +45,7 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
|
||||
prop := newPropagator()
|
||||
otel.SetTextMapPropagator(prop)
|
||||
|
||||
resources, err := resource.New(
|
||||
ctx,
|
||||
resource.WithAttributes(semconv.ServiceName("spend-sparrow")),
|
||||
)
|
||||
resources, err := resource.New(ctx)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to create resource", "error", err)
|
||||
}
|
||||
@@ -96,11 +88,7 @@ func newPropagator() propagation.TextMapPropagator {
|
||||
}
|
||||
|
||||
func newTracerProvider(ctx context.Context, resource *resource.Resource) (*trace.TracerProvider, error) {
|
||||
exp, err := otlptracegrpc.New(
|
||||
ctx,
|
||||
otlptracegrpc.WithEndpoint(otelEndpoint),
|
||||
otlptracegrpc.WithInsecure(),
|
||||
)
|
||||
exp, err := otlptracegrpc.New(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -112,10 +100,7 @@ func newTracerProvider(ctx context.Context, resource *resource.Resource) (*trace
|
||||
}
|
||||
|
||||
func newMeterProvider(ctx context.Context, resource *resource.Resource) (*metric.MeterProvider, error) {
|
||||
exp, err := otlpmetricgrpc.New(
|
||||
ctx,
|
||||
otlpmetricgrpc.WithInsecure(),
|
||||
otlpmetricgrpc.WithEndpoint(otelEndpoint))
|
||||
exp, err := otlpmetricgrpc.New(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -127,10 +112,7 @@ func newMeterProvider(ctx context.Context, resource *resource.Resource) (*metric
|
||||
}
|
||||
|
||||
func newLoggerProvider(ctx context.Context, resource *resource.Resource) (*log.LoggerProvider, error) {
|
||||
logExporter, err := otlploggrpc.New(
|
||||
ctx,
|
||||
otlploggrpc.WithInsecure(),
|
||||
otlploggrpc.WithEndpoint(otelEndpoint))
|
||||
logExporter, err := otlploggrpc.New(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
111
internal/tag/db.go
Normal file
111
internal/tag/db.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"spend-sparrow/internal/core"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Db interface {
|
||||
insert(ctx context.Context, tag Tag) (*Tag, error)
|
||||
update(ctx context.Context, tag Tag) (*Tag, error)
|
||||
delete(ctx context.Context, userId uuid.UUID, id uuid.UUID) error
|
||||
get(ctx context.Context, userId uuid.UUID, id uuid.UUID) (*Tag, error)
|
||||
getAll(ctx context.Context, userId uuid.UUID) ([]Tag, error)
|
||||
find(ctx context.Context, userId uuid.UUID, search string) ([]Tag, error)
|
||||
}
|
||||
|
||||
type DbSqlite struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewDbSqlite(db *sqlx.DB) *DbSqlite {
|
||||
return &DbSqlite{db: db}
|
||||
}
|
||||
|
||||
func (db DbSqlite) insert(ctx context.Context, tag Tag) (*Tag, error) {
|
||||
r, err := db.db.ExecContext(ctx, `
|
||||
INSERT INTO tag (id, user_id, name, created_at, created_by, updated_at, updated_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
tag.Id, tag.UserId, tag.Name, tag.CreatedAt, tag.CreatedBy, tag.UpdatedAt, tag.UpdatedBy,
|
||||
)
|
||||
err = core.TransformAndLogDbError(ctx, "tag", r, err)
|
||||
if err != nil {
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
return db.get(ctx, tag.UserId, tag.Id)
|
||||
}
|
||||
|
||||
func (db DbSqlite) update(ctx context.Context, tag Tag) (*Tag, error) {
|
||||
_, err := db.db.ExecContext(ctx, `
|
||||
UPDATE tag
|
||||
SET name = ?,
|
||||
updated_at = ?,
|
||||
updated_by = ?
|
||||
WHERE user_id = ?
|
||||
AND id = ?`,
|
||||
tag.Name, tag.UpdatedAt, tag.UpdatedBy, tag.UserId, tag.Id)
|
||||
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "SQL error UpdateUser", "err", err)
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
return db.get(ctx, tag.UserId, tag.Id)
|
||||
}
|
||||
|
||||
func (db DbSqlite) delete(ctx context.Context, userId uuid.UUID, id uuid.UUID) error {
|
||||
r, err := db.db.ExecContext(
|
||||
ctx,
|
||||
"DELETE FROM tag WHERE user_id = ? AND id = ?",
|
||||
userId,
|
||||
id)
|
||||
err = core.TransformAndLogDbError(ctx, "tag", r, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db DbSqlite) get(ctx context.Context, userId uuid.UUID, id uuid.UUID) (*Tag, error) {
|
||||
var tag Tag
|
||||
err := db.db.GetContext(ctx, &tag, "SELECT * FROM tag WHERE id = ? AND user_id = ?", id, userId)
|
||||
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "Could not get tag", "err", err)
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
func (db DbSqlite) getAll(ctx context.Context, userId uuid.UUID) ([]Tag, error) {
|
||||
var tags []Tag
|
||||
err := db.db.SelectContext(ctx, &tags, "SELECT * FROM tag WHERE user_id = ?", userId)
|
||||
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "Could not GetAll tag", "err", err)
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (db DbSqlite) find(ctx context.Context, userId uuid.UUID, search string) ([]Tag, error) {
|
||||
var tags []Tag
|
||||
err := db.db.SelectContext(ctx, &tags, "SELECT * FROM tag WHERE user_id = ? AND name LIKE ?", userId, "%"+search+"%")
|
||||
|
||||
slog.InfoContext(ctx, "find", "len", len(tags), "search", search)
|
||||
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "Could not find tag", "err", err)
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
200
internal/tag/handler.go
Normal file
200
internal/tag/handler.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"spend-sparrow/internal/core"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
DECIMALS_MULTIPLIER = 100
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
Handle(router *http.ServeMux)
|
||||
}
|
||||
|
||||
type HandlerImpl struct {
|
||||
s Service
|
||||
r *core.Render
|
||||
}
|
||||
|
||||
func NewHandler(s Service, r *core.Render) Handler {
|
||||
return HandlerImpl{
|
||||
s: s,
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) Handle(r *http.ServeMux) {
|
||||
r.Handle("GET /tag", h.handlePage())
|
||||
r.Handle("POST /tag/search", h.handleInlineEditSearch())
|
||||
r.Handle("GET /tag/new", h.handleNew())
|
||||
r.Handle("GET /tag/{id}", h.handleEdit())
|
||||
r.Handle("POST /tag/{id}", h.handlePost())
|
||||
r.Handle("DELETE /tag/{id}", h.handleDelete())
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handlePage() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
core.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.s.getAll(r.Context(), user)
|
||||
if err != nil {
|
||||
h.r.RenderLayout(r, w, core.ErrorComp(err), user)
|
||||
return
|
||||
}
|
||||
|
||||
comp := page(tags)
|
||||
h.r.RenderLayout(r, w, comp, user)
|
||||
}
|
||||
}
|
||||
func (h HandlerImpl) handleInlineEditSearch() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
core.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
search := r.FormValue("search")
|
||||
tags, err := h.s.find(r.Context(), user, search)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, fmt.Errorf("could not find tags: %w", core.ErrInternal))
|
||||
}
|
||||
|
||||
comp := inlineEditSearch(tags)
|
||||
h.r.Render(r, w, comp)
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handleNew() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
core.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
comp := editNew()
|
||||
h.r.RenderLayout(r, w, comp, user)
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handleEdit() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
core.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := h.s.get(r.Context(), user, id)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
comp := edit(*tag)
|
||||
h.r.RenderLayout(r, w, comp, user)
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handlePost() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
core.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
id uuid.UUID
|
||||
err error
|
||||
)
|
||||
|
||||
idStr := r.PathValue("id")
|
||||
if idStr != "new" {
|
||||
id, err = uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
core.HandleError(w, r, fmt.Errorf("could not parse value: %w", core.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
input := Tag{
|
||||
Id: id,
|
||||
Name: r.FormValue("name"),
|
||||
}
|
||||
|
||||
if idStr == "new" {
|
||||
_, err = h.s.add(r.Context(), user, input)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err = h.s.update(r.Context(), user, input)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
core.DoRedirect(w, r, "/tag")
|
||||
}
|
||||
}
|
||||
|
||||
func (h HandlerImpl) handleDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
core.UpdateSpan(r)
|
||||
|
||||
user := core.GetUser(r)
|
||||
if user == nil {
|
||||
core.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := r.PathValue("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
err = h.s.delete(r.Context(), user, id)
|
||||
if err != nil {
|
||||
core.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
core.DoRedirect(w, r, "/tag")
|
||||
}
|
||||
}
|
||||
119
internal/tag/service.go
Normal file
119
internal/tag/service.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"spend-sparrow/internal/auth_types"
|
||||
"spend-sparrow/internal/core"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
add(ctx context.Context, user *auth_types.User, tag Tag) (*Tag, error)
|
||||
update(ctx context.Context, user *auth_types.User, tag Tag) (*Tag, error)
|
||||
delete(ctx context.Context, user *auth_types.User, tagId uuid.UUID) error
|
||||
get(ctx context.Context, user *auth_types.User, tagId uuid.UUID) (*Tag, error)
|
||||
getAll(ctx context.Context, user *auth_types.User) ([]Tag, error)
|
||||
find(ctx context.Context, user *auth_types.User, search string) ([]Tag, error)
|
||||
}
|
||||
|
||||
type ServiceImpl struct {
|
||||
db Db
|
||||
clock core.Clock
|
||||
random core.Random
|
||||
}
|
||||
|
||||
func NewService(db Db, random core.Random, clock core.Clock) Service {
|
||||
return ServiceImpl{
|
||||
db: db,
|
||||
clock: clock,
|
||||
random: random,
|
||||
}
|
||||
}
|
||||
|
||||
func (s ServiceImpl) add(ctx context.Context, user *auth_types.User, tag Tag) (*Tag, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
isValid := s.isTagValid(tag)
|
||||
if !isValid {
|
||||
return nil, core.ErrBadRequest
|
||||
}
|
||||
|
||||
newId, err := s.random.UUID(ctx)
|
||||
if err != nil {
|
||||
return nil, core.ErrInternal
|
||||
}
|
||||
|
||||
tag.Id = newId
|
||||
tag.UserId = user.Id
|
||||
tag.CreatedBy = user.Id
|
||||
tag.CreatedAt = s.clock.Now()
|
||||
|
||||
return s.db.insert(ctx, tag)
|
||||
}
|
||||
|
||||
func (s ServiceImpl) update(ctx context.Context, user *auth_types.User, input Tag) (*Tag, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
tag, err := s.get(ctx, user, input.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tag.Name = input.Name
|
||||
|
||||
if user.Id != tag.UserId {
|
||||
return nil, core.ErrBadRequest
|
||||
}
|
||||
|
||||
isValid := s.isTagValid(*tag)
|
||||
if !isValid {
|
||||
return nil, core.ErrBadRequest
|
||||
}
|
||||
|
||||
tag.UpdatedBy = &user.Id
|
||||
now := s.clock.Now()
|
||||
tag.UpdatedAt = &now
|
||||
|
||||
return s.db.update(ctx, *tag)
|
||||
}
|
||||
|
||||
func (s ServiceImpl) delete(ctx context.Context, user *auth_types.User, tagId uuid.UUID) error {
|
||||
if user == nil {
|
||||
return core.ErrUnauthorized
|
||||
}
|
||||
|
||||
return s.db.delete(ctx, user.Id, tagId)
|
||||
}
|
||||
|
||||
func (s ServiceImpl) get(ctx context.Context, user *auth_types.User, tagId uuid.UUID) (*Tag, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
return s.db.get(ctx, user.Id, tagId)
|
||||
}
|
||||
|
||||
func (s ServiceImpl) getAll(ctx context.Context, user *auth_types.User) ([]Tag, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
return s.db.getAll(ctx, user.Id)
|
||||
}
|
||||
|
||||
func (s ServiceImpl) find(ctx context.Context, user *auth_types.User, search string) ([]Tag, error) {
|
||||
if user == nil {
|
||||
return nil, core.ErrUnauthorized
|
||||
}
|
||||
|
||||
return s.db.find(ctx, user.Id, search)
|
||||
}
|
||||
func (s ServiceImpl) isTagValid(tag Tag) bool {
|
||||
err := core.ValidateString(tag.Name, "name")
|
||||
return err == nil
|
||||
}
|
||||
159
internal/tag/template.templ
Normal file
159
internal/tag/template.templ
Normal file
@@ -0,0 +1,159 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"spend-sparrow/internal/core"
|
||||
"spend-sparrow/internal/template/svg"
|
||||
)
|
||||
|
||||
templ InlineEditInput(classes string) {
|
||||
<div class={ "flex flex-wrap gap-2 input max-w-full " + classes }>
|
||||
<span class="flex items-center gap-1 p-1 bg-green-100 rounded-sm">
|
||||
Lebensmittel
|
||||
<button class="hover:bg-red-900 rounded p-1 w-5 transition-all">
|
||||
@svg.Cancel()
|
||||
</button>
|
||||
</span>
|
||||
<span class="flex items-center gap-1 p-1 bg-yellow-100 rounded-sm">
|
||||
Sparen
|
||||
<button class="hover:bg-red-900 rounded p-1 w-5 transition-all">
|
||||
@svg.Cancel()
|
||||
</button>
|
||||
</span>
|
||||
<span class="flex items-center gap-1 p-1 bg-red-100 rounded-sm">
|
||||
Tanken
|
||||
<button class="hover:bg-red-900 rounded p-1 w-5 transition-all">
|
||||
@svg.Cancel()
|
||||
</button>
|
||||
</span>
|
||||
<div>
|
||||
<input
|
||||
class="inline"
|
||||
name="search"
|
||||
placeholder="Begin Typing To Search ..."
|
||||
hx-post="/tag/search"
|
||||
hx-trigger="input changed delay:250ms, keyup[key=='Enter'], load"
|
||||
hx-target="#tag-search-results"
|
||||
/>
|
||||
<div id="tag-search-results" class="absolute bg-white border-gray-200 border-1 rounded-lg p-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ inlineEditSearch(tags []Tag) {
|
||||
for _,tag:=range(tags) {
|
||||
<p x-data={ tag.Id.String() }>{ tag.Name }</p>
|
||||
}
|
||||
}
|
||||
|
||||
templ page(tags []Tag) {
|
||||
@core.Breadcrumb([]string{"Home", "Tag"}, []string{"/", "/tag"})
|
||||
<div class="flex flex-wrap gap-20 text-xl mt-10 justify-center">
|
||||
@newItem()
|
||||
for _,tag:=range(tags ) {
|
||||
@item(tag)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ editNew() {
|
||||
<div class="flex flex-col h-full">
|
||||
@core.Breadcrumb([]string{"Home", "Tag", "New"}, []string{"/", "/tag", "/tag/new"})
|
||||
<div class="flex justify-center items-center flex-1">
|
||||
<form
|
||||
hx-post={ "/tag/new" }
|
||||
class="grid grid-cols-4 items-center gap-4 max-w-2xl"
|
||||
>
|
||||
<label for="timestamp" class="text-sm text-gray-500">Name</label>
|
||||
<input
|
||||
autofocus
|
||||
name="name"
|
||||
type="text"
|
||||
class="bg-white input datetime col-span-3"
|
||||
/>
|
||||
<div class="flex gap-6 justify-end col-span-4">
|
||||
<a href="/tag" class="col-start-3 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||
<span class="h-4 w-4">
|
||||
@svg.Cancel()
|
||||
</span>
|
||||
<span>
|
||||
Cancel
|
||||
</span>
|
||||
</a>
|
||||
<button type="submit" class="col-start-4 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||
@svg.Save()
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ edit(tag Tag) {
|
||||
<div class="flex flex-col h-full">
|
||||
@core.Breadcrumb([]string{"Home", "Tag", tag.Name}, []string{"/", "/tag", "/tag/" + tag.Id.String()})
|
||||
<div class="flex justify-center items-center flex-1">
|
||||
<form
|
||||
hx-post={ "/tag/" + tag.Id.String() }
|
||||
class="grid grid-cols-4 items-center gap-4 max-w-2xl"
|
||||
>
|
||||
<label for="timestamp" class="text-sm text-gray-500">Name</label>
|
||||
<input
|
||||
autofocus
|
||||
name="name"
|
||||
type="text"
|
||||
value={ tag.Name }
|
||||
class="bg-white input datetime col-span-3"
|
||||
/>
|
||||
<div class="flex flex-row-reverse gap-6 justify-end col-span-4">
|
||||
<button type="submit" class="col-start-4 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||
@svg.Save()
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
<a href="/tag" class="col-start-3 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||
<span class="h-4 w-4">
|
||||
@svg.Cancel()
|
||||
</span>
|
||||
<span>
|
||||
Cancel
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
hx-delete={ "/tag/" + tag.Id.String() }
|
||||
hx-confirm={ "Do you really want to delete '" + tag.Name + "'" }
|
||||
class="col-start-4 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-red-50 rounded-lg hover:underline flex items-center gap-2 justify-center"
|
||||
>
|
||||
@svg.Delete()
|
||||
<span>
|
||||
Delete
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ newItem() {
|
||||
<a
|
||||
href="/tag/new"
|
||||
class="p-5 w-64 h-64 flex gap-10 active:bg-gray-200 flex-col justify-center items-center hover:bg-gray-100 transition-all cursor-pointer rounded-lg bg-gray-50 border-1 border-gray-300"
|
||||
>
|
||||
New Tag
|
||||
<div class="w-10">
|
||||
@svg.Plus()
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
templ item(tag Tag) {
|
||||
<a href={ "/tag/" + tag.Id.String() } class="p-5 w-64 h-64 flex gap-10 active:bg-gray-200 flex-col justify-center items-center hover:bg-gray-100 transition-all cursor-pointer rounded-lg bg-gray-50 border-1 border-gray-300">
|
||||
<span>
|
||||
{ tag.Name }
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
19
internal/tag/types.go
Normal file
19
internal/tag/types.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Id uuid.UUID `db:"id"`
|
||||
UserId uuid.UUID `db:"user_id"`
|
||||
|
||||
Name string `db:"name"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
CreatedBy uuid.UUID `db:"created_by"`
|
||||
UpdatedAt *time.Time `db:"updated_at"`
|
||||
UpdatedBy *uuid.UUID `db:"updated_by"`
|
||||
}
|
||||
@@ -19,7 +19,7 @@ templ Eye() {
|
||||
}
|
||||
|
||||
templ Plus() {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 304 384" class="h-4 w-4 text-gray-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 304 384" class="text-gray-500">
|
||||
<path fill="currentColor" d="M299 213H171v128h-43V213H0v-42h128V43h43v128h128v42z"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
@@ -65,7 +65,9 @@ templ TransactionComp(items templ.Component, filter TransactionItemsFilter, acco
|
||||
hx-swap="afterbegin"
|
||||
class="button button-primary ml-auto px-2 flex items-center gap-2 justify-center"
|
||||
>
|
||||
@svg.Plus()
|
||||
<div class="w-3">
|
||||
@svg.Plus()
|
||||
</div>
|
||||
<p>New Transaction</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,9 @@ templ TreasureChestComp(treasureChests []*treasure_chest_types.TreasureChest, mo
|
||||
hx-swap="afterbegin"
|
||||
class="ml-auto text-center button button-primary px-2 flex items-center gap-2"
|
||||
>
|
||||
@svg.Plus()
|
||||
<div class="w-3">
|
||||
@svg.Plus()
|
||||
</div>
|
||||
New Treasure Chest
|
||||
</button>
|
||||
<div id="treasurechest-items" class="my-6 flex flex-col">
|
||||
@@ -109,7 +111,9 @@ templ EditTreasureChest(treasureChest *treasure_chest_types.TreasureChest, paren
|
||||
hx-swap="outerHTML"
|
||||
class="button button-primary ml-auto px-2 flex items-center gap-2"
|
||||
>
|
||||
@svg.Plus()
|
||||
<div class="w-3">
|
||||
@svg.Plus()
|
||||
</div>
|
||||
<p>New Monthly Transaction</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
7
main.go
7
main.go
@@ -22,8 +22,11 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
db, err := otelsqlx.Open("sqlite3", "./data/spend-sparrow.db?_journal_mode=WAL",
|
||||
otelsql.WithAttributes(semconv.DBSystemSqlite))
|
||||
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)
|
||||
return
|
||||
|
||||
1
migration/011_budget_rename_description.up.sql
Normal file
1
migration/011_budget_rename_description.up.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "budget" RENAME COLUMN description TO name;
|
||||
13
migration/012_tag.up.sql
Normal file
13
migration/012_tag.up.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
CREATE TABLE "tag" (
|
||||
id TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
|
||||
name TEXT NOT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL,
|
||||
created_by TEXT NOT NULL,
|
||||
updated_at DATETIME,
|
||||
updated_by TEXT
|
||||
) WITHOUT ROWID;
|
||||
|
||||
1
mocks/default.go
Normal file
1
mocks/default.go
Normal file
@@ -0,0 +1 @@
|
||||
package mocks
|
||||
230
package-lock.json
generated
230
package-lock.json
generated
@@ -55,9 +55,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.30",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
|
||||
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -66,17 +66,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
|
||||
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"detect-libc": "^2.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
"node-addon-api": "^7.0.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
@@ -86,25 +86,25 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
"@parcel/watcher-android-arm64": "2.5.6",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.6",
|
||||
"@parcel/watcher-darwin-x64": "2.5.6",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.6",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.6",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.6",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.6",
|
||||
"@parcel/watcher-win32-arm64": "2.5.6",
|
||||
"@parcel/watcher-win32-ia32": "2.5.6",
|
||||
"@parcel/watcher-win32-x64": "2.5.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -123,9 +123,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -144,9 +144,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -165,9 +165,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -186,9 +186,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -207,9 +207,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -228,9 +228,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -249,9 +249,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -270,9 +270,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -291,9 +291,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -312,9 +312,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -333,9 +333,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
|
||||
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -354,9 +354,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -710,30 +710,14 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
@@ -748,32 +732,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"version": "5.19.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
|
||||
"integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
"tapable": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -811,16 +782,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
@@ -1092,16 +1053,6 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss/node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -1112,20 +1063,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -1151,13 +1088,13 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
@@ -1181,26 +1118,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
||||
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
menuButton.addEventListener("click", function (e) {
|
||||
menuButton.addEventListener("click", function() {
|
||||
menu.showModal();
|
||||
});
|
||||
menuButtonClose.addEventListener("click", function (e) {
|
||||
menuButtonClose.addEventListener("click", function() {
|
||||
menu.close();
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ function updateTime() {
|
||||
document.querySelectorAll(".datetime").forEach((el) => {
|
||||
if (el.textContent !== "") {
|
||||
el.textContent = el.textContent.includes("UTC") ? new Date(el.textContent).toLocaleString([], { day: 'numeric', month: 'short', year: 'numeric' }) : el.textContent;
|
||||
} else if (el.attributes['value'] !== "") {
|
||||
} else if (el.attributes['value'] && el.attributes['value'] !== "") {
|
||||
const value = el.attributes['value'].value;
|
||||
const newDate = value.includes("UTC") ? new Date(value) : value;
|
||||
el.valueAsDate = newDate;
|
||||
|
||||
Reference in New Issue
Block a user