aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorzberwaldt <17715430+zberwaldt@users.noreply.github.com>2024-03-15 22:03:11 -0400
committerGitHub <noreply@github.com>2024-03-15 22:03:11 -0400
commit6f8cfbd6cc3d5adbda38e74013c68e3d4745766d (patch)
treeb3f045cd06d6622e23441b442e8f3861050ed444
parentfac21fa0a72d4a7f1a01ccd44e3acf9c90fd95bd (diff)
parentfd1332a3df191577e91c6d846a8b5db1747099fd (diff)
Merge pull request #1 from zberwaldt/staging
Staging to Prod
-rw-r--r--.gitignore49
-rw-r--r--api/.env.sample10
-rw-r--r--api/cmd/main.go21
-rw-r--r--api/cmd/main_test.go62
-rw-r--r--api/go.mod56
-rw-r--r--api/go.sum128
-rw-r--r--api/internal/config/config.go17
-rw-r--r--api/internal/controllers/auth.go79
-rw-r--r--api/internal/controllers/preferences.go45
-rw-r--r--api/internal/controllers/stats.go168
-rw-r--r--api/internal/controllers/user.go68
-rw-r--r--api/internal/database/database.go24
-rw-r--r--api/internal/middleware/middleware.go56
-rw-r--r--api/internal/models/auth.go11
-rw-r--r--api/internal/models/preferences.go14
-rw-r--r--api/internal/models/statistics.go26
-rw-r--r--api/internal/models/user.go10
-rw-r--r--api/internal/router/router.go43
-rw-r--r--db/scripts/water_init.sql97
-rw-r--r--fe/.env.sample1
-rw-r--r--fe/.gitignore24
-rw-r--r--fe/.prettierrc4
-rw-r--r--fe/.vscode/extensions.json3
-rw-r--r--fe/README.md47
-rw-r--r--fe/index.html13
-rw-r--r--fe/package-lock.json1780
-rw-r--r--fe/package.json25
-rw-r--r--fe/public/vite.svg1
-rw-r--r--fe/src/App.svelte16
-rw-r--r--fe/src/app.css120
-rw-r--r--fe/src/assets/svelte.svg1
-rw-r--r--fe/src/errors.ts5
-rw-r--r--fe/src/http.ts92
-rw-r--r--fe/src/lib/Card.svelte23
-rw-r--r--fe/src/lib/Chart.svelte63
-rw-r--r--fe/src/lib/Column.svelte13
-rw-r--r--fe/src/lib/DataView.svelte228
-rw-r--r--fe/src/lib/Layout.svelte74
-rw-r--r--fe/src/lib/LoginForm.svelte86
-rw-r--r--fe/src/lib/PreferencesForm.svelte119
-rw-r--r--fe/src/lib/Table.svelte114
-rw-r--r--fe/src/lib/forms/AddForm.svelte78
-rw-r--r--fe/src/main.ts8
-rw-r--r--fe/src/stores/auth.ts90
-rw-r--r--fe/src/stores/forms.ts6
-rw-r--r--fe/src/types.ts53
-rw-r--r--fe/src/utils.ts14
-rw-r--r--fe/src/vite-env.d.ts2
-rw-r--r--fe/svelte.config.js8
-rw-r--r--fe/tsconfig.json20
-rw-r--r--fe/tsconfig.node.json10
-rw-r--r--fe/vite.config.ts7
52 files changed, 4132 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8a5cdb1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,49 @@
1# If you prefer the allow list template instead of the deny list, see community template:
2# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3#
4# Binaries for programs and plugins
5*.exe
6*.exe~
7*.dll
8*.so
9*.dylib
10
11# Test binary, built with `go test -c`
12*.test
13
14# Output of the go coverage tool, specifically when used with LiteIDE
15*.out
16
17# Dependency directories (remove the comment below to include it)
18vendor/
19
20# Go workspace file
21go.work
22
23
24# Logs
25logs
26*.log
27npm-debug.log*
28yarn-debug.log*
29yarn-error.log*
30lerna-debug.log*
31.pnpm-debug.log*
32
33# Dependency directories
34node_modules/
35
36# TypeScript cache
37*.tsbuildinfo
38
39# Optional REPL history
40.node_repl_history
41
42# dotenv environment variable files
43.env
44.env.development.local
45.env.test.local
46.env.production.local
47.env.local
48
49*.sqlite3
diff --git a/api/.env.sample b/api/.env.sample
new file mode 100644
index 0000000..6e25893
--- /dev/null
+++ b/api/.env.sample
@@ -0,0 +1,10 @@
1# user for test
2TEST_USER=user1
3# test user password
4TEST_PASS=12345
5# database path
6DB_PATH="path/to/database/file"
7# database driver
8DB_DRIVER="sqlite3"
9# port
10PORT=":8080" \ No newline at end of file
diff --git a/api/cmd/main.go b/api/cmd/main.go
new file mode 100644
index 0000000..c23eff1
--- /dev/null
+++ b/api/cmd/main.go
@@ -0,0 +1,21 @@
1package main
2
3import (
4 "log"
5 "water/api/internal/config"
6 "water/api/internal/router"
7)
8
9func main() {
10 c, err := config.Load()
11 if err != nil {
12 log.Fatalf("Error while reading config file %s", err)
13 }
14
15 r := router.SetupRouter()
16 // Listen and Server in 0.0.0.0:8080
17 err = r.Run(c.GetString("PORT"))
18 if err != nil {
19 log.Fatal(err)
20 }
21}
diff --git a/api/cmd/main_test.go b/api/cmd/main_test.go
new file mode 100644
index 0000000..a4db57a
--- /dev/null
+++ b/api/cmd/main_test.go
@@ -0,0 +1,62 @@
1package main
2
3import (
4 "encoding/json"
5 "log"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9 "water/api/internal/router"
10
11 "github.com/stretchr/testify/assert"
12 "water/api/internal/config"
13)
14
15func getTestUserCredentials() (string, string) {
16 viper, err := config.Load()
17 if err != nil {
18 log.Fatalf("Error while reading config file %s", err)
19 }
20
21 testUser := viper.GetString("TEST_USER")
22 testPass := viper.GetString("TEST_PASS")
23 return testUser, testPass
24}
25
26func TestAuthRoute(t *testing.T) {
27 r := router.SetupRouter()
28
29 username, password := getTestUserCredentials()
30
31 w := httptest.NewRecorder()
32 req, err := http.NewRequest("POST", "/api/v1/auth", nil)
33 if err != nil {
34 t.Fatalf("Failed to create request: %v", err)
35 }
36 req.SetBasicAuth(username, password)
37 r.ServeHTTP(w, req)
38
39 assert.Equal(t, http.StatusOK, w.Code, "response should return a 200 code")
40
41 var response map[string]interface{}
42 err = json.Unmarshal(w.Body.Bytes(), &response)
43 if err != nil {
44 t.Fatalf("Failed to unmarshal response: %v", err)
45 }
46 _, exists := response["token"]
47 assert.True(t, exists, "response should return a token")
48 if !exists {
49 t.Fatalf("response did not contain token")
50 }
51}
52
53func TestAuthRouteFailure(t *testing.T) {
54 r := router.SetupRouter()
55
56 w := httptest.NewRecorder()
57 req, _ := http.NewRequest("POST", "/api/v1/auth", nil)
58 req.SetBasicAuth("asdf", "asdf")
59 r.ServeHTTP(w, req)
60
61 assert.Equal(t, http.StatusUnauthorized, w.Code, "should return a 401 code")
62}
diff --git a/api/go.mod b/api/go.mod
new file mode 100644
index 0000000..6c414bc
--- /dev/null
+++ b/api/go.mod
@@ -0,0 +1,56 @@
1module water/api
2
3go 1.18
4
5require (
6 github.com/gin-gonic/gin v1.9.1
7 github.com/google/uuid v1.6.0
8 github.com/mattn/go-sqlite3 v1.14.22
9 github.com/spf13/viper v1.18.2
10 github.com/stretchr/testify v1.8.4
11 golang.org/x/crypto v0.19.0
12)
13
14require (
15 github.com/bytedance/sonic v1.11.0 // indirect
16 github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
17 github.com/chenzhuoyu/iasm v0.9.1 // indirect
18 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
19 github.com/fsnotify/fsnotify v1.7.0 // indirect
20 github.com/gabriel-vasile/mimetype v1.4.3 // indirect
21 github.com/gin-contrib/sse v0.1.0 // indirect
22 github.com/go-playground/locales v0.14.1 // indirect
23 github.com/go-playground/universal-translator v0.18.1 // indirect
24 github.com/go-playground/validator/v10 v10.18.0 // indirect
25 github.com/goccy/go-json v0.10.2 // indirect
26 github.com/hashicorp/hcl v1.0.0 // indirect
27 github.com/json-iterator/go v1.1.12 // indirect
28 github.com/klauspost/cpuid/v2 v2.2.6 // indirect
29 github.com/leodido/go-urn v1.4.0 // indirect
30 github.com/magiconair/properties v1.8.7 // indirect
31 github.com/mattn/go-isatty v0.0.20 // indirect
32 github.com/mitchellh/mapstructure v1.5.0 // indirect
33 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
34 github.com/modern-go/reflect2 v1.0.2 // indirect
35 github.com/pelletier/go-toml/v2 v2.1.1 // indirect
36 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
37 github.com/sagikazarmark/locafero v0.4.0 // indirect
38 github.com/sagikazarmark/slog-shim v0.1.0 // indirect
39 github.com/sourcegraph/conc v0.3.0 // indirect
40 github.com/spf13/afero v1.11.0 // indirect
41 github.com/spf13/cast v1.6.0 // indirect
42 github.com/spf13/pflag v1.0.5 // indirect
43 github.com/subosito/gotenv v1.6.0 // indirect
44 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
45 github.com/ugorji/go/codec v1.2.12 // indirect
46 go.uber.org/atomic v1.9.0 // indirect
47 go.uber.org/multierr v1.9.0 // indirect
48 golang.org/x/arch v0.7.0 // indirect
49 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
50 golang.org/x/net v0.21.0 // indirect
51 golang.org/x/sys v0.17.0 // indirect
52 golang.org/x/text v0.14.0 // indirect
53 google.golang.org/protobuf v1.32.0 // indirect
54 gopkg.in/ini.v1 v1.67.0 // indirect
55 gopkg.in/yaml.v3 v3.0.1 // indirect
56)
diff --git a/api/go.sum b/api/go.sum
new file mode 100644
index 0000000..20771f7
--- /dev/null
+++ b/api/go.sum
@@ -0,0 +1,128 @@
1github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
2github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
3github.com/bytedance/sonic v1.11.0 h1:FwNNv6Vu4z2Onf1++LNzxB/QhitD8wuTdpZzMTGITWo=
4github.com/bytedance/sonic v1.11.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
5github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
6github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
7github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
8github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
9github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
10github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
11github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
12github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
15github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
17github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
18github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
19github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
20github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
21github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
22github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
23github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
24github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
25github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
26github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
27github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
28github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
29github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
30github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
31github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
32github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
33github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
34github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
35github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
36github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
37github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
38github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
39github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
40github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
41github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
42github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
43github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
44github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
45github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
46github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
47github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
48github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
49github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
50github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
51github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
52github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
53github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
54github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
55github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
56github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
57github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
58github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
59github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
60github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
61github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
62github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
63github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
64github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
65github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
66github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
67github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
68github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
69github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
70github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
71github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
72github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
73github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
74github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
75github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
76github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
77github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
78github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
79github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
80github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
81github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
82github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
83github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
84github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
85github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
86github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
87github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
88github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
89github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
90github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
91github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
92github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
93github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
94github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
95github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
96github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
97github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
98github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
99go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
100go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
101go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
102go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
103golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
104golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
105golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
106golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
107golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
108golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
109golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
110golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
111golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
112golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
113golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
114golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
115golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
116golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
117golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
118google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
119google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
120gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
121gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
122gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
123gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
124gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
125gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
126gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
127nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
128rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/api/internal/config/config.go b/api/internal/config/config.go
new file mode 100644
index 0000000..d54e40e
--- /dev/null
+++ b/api/internal/config/config.go
@@ -0,0 +1,17 @@
1package config
2
3import (
4 "fmt"
5 "github.com/spf13/viper"
6)
7
8func Load() (*viper.Viper, error) {
9 v := viper.New()
10 v.SetConfigFile(".env")
11 v.AddConfigPath(".")
12 err := v.ReadInConfig()
13 if err != nil {
14 return nil, fmt.Errorf("error reading .env file: %s", err)
15 }
16 return v, nil
17}
diff --git a/api/internal/controllers/auth.go b/api/internal/controllers/auth.go
new file mode 100644
index 0000000..ab2fbbb
--- /dev/null
+++ b/api/internal/controllers/auth.go
@@ -0,0 +1,79 @@
1package controllers
2
3import (
4 "crypto/rand"
5 "database/sql"
6 "encoding/base64"
7 "errors"
8 "github.com/gin-gonic/gin"
9 "net/http"
10 "water/api/internal/models"
11
12 _ "github.com/mattn/go-sqlite3"
13 "golang.org/x/crypto/bcrypt"
14 "water/api/internal/database"
15)
16
17
18
19// AuthHandler is a function that handles users' authentication. It checks if the request
20// has valid credentials, authenticates the user and sets the user's session.
21// If the authentication is successful, it will allow the user to access protected routes.
22func AuthHandler (c *gin.Context) {
23 username, password, ok := c.Request.BasicAuth()
24 if !ok {
25 c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`)
26 c.AbortWithStatus(http.StatusUnauthorized)
27 return
28 }
29
30 db := database.EstablishDBConnection()
31 defer func(db *sql.DB) {
32 err := db.Close()
33 if err != nil {
34 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
35 return
36 }
37 }(db)
38
39 var user models.User
40 var preference models.Preference
41
42 row := db.QueryRow("SELECT id as 'id', name, uuid, password FROM Users WHERE name = ?", username)
43 if err := row.Scan(&user.ID, &user.Name, &user.UUID, &user.Password); err != nil {
44 if errors.Is(err, sql.ErrNoRows) {
45 c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
46 return
47 }
48 }
49
50 row = db.QueryRow("SELECT id, color, size_id, user_id FROM Preferences where user_id = ?", user.ID)
51 if err := row.Scan(&preference.ID, &preference.Color, &preference.SizeID, &preference.UserID); err != nil {
52 if errors.Is(err, sql.ErrNoRows) {
53 c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
54 }
55 }
56
57 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
58 c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
59 return
60 }
61
62 // Generate a simple API token
63 apiToken := generateToken()
64 c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference})
65}
66
67
68// generateToken is a helper function used in the AuthHandler. It generates a random token for API authentication.
69// This function creates an empty byte slice of length 32 and fills it with cryptographic random data using the rand.Read function.
70// If an error occurs during the generation, it will return an empty string.
71// The generated cryptographic random data is then encoded into a base64 string and returned.
72func generateToken() string {
73 token := make([]byte, 32)
74 _, err := rand.Read(token)
75 if err != nil {
76 return ""
77 }
78 return base64.StdEncoding.EncodeToString(token)
79} \ No newline at end of file
diff --git a/api/internal/controllers/preferences.go b/api/internal/controllers/preferences.go
new file mode 100644
index 0000000..a1bcf4f
--- /dev/null
+++ b/api/internal/controllers/preferences.go
@@ -0,0 +1,45 @@
1package controllers
2
3import (
4 "github.com/gin-gonic/gin"
5 "net/http"
6 "database/sql"
7 "water/api/internal/database"
8 "water/api/internal/models"
9)
10
11func GetSizes(c *gin.Context) {
12 db := database.EstablishDBConnection()
13 defer func(db *sql.DB) {
14 err := db.Close()
15 if err != nil {
16 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
17 }
18 }(db)
19
20 rows, err := db.Query("SELECT id, size, unit FROM Sizes")
21 if err != nil {
22 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
23 return
24 }
25 defer func(rows *sql.Rows) {
26 err := rows.Close()
27 if err != nil {
28 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
29 return
30 }
31 }(rows)
32
33 var data []models.Size
34
35 for rows.Next() {
36 var size models.Size
37 if err := rows.Scan(&size.ID, &size.Size, &size.Unit); err != nil {
38 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
39 return
40 }
41 data = append(data, size)
42 }
43
44 c.JSON(http.StatusOK, data)
45}
diff --git a/api/internal/controllers/stats.go b/api/internal/controllers/stats.go
new file mode 100644
index 0000000..2234787
--- /dev/null
+++ b/api/internal/controllers/stats.go
@@ -0,0 +1,168 @@
1package controllers
2
3import (
4 "database/sql"
5 "github.com/gin-gonic/gin"
6 "net/http"
7 "water/api/internal/database"
8 "water/api/internal/models"
9)
10
11// TODO: add comments to all exported members of package.
12
13// GetAllStatistics connects to the database and queries for all statistics in the database.
14// If none have been found it will return an error, otherwise a 200 code is sent along with the list of statistics.
15func GetAllStatistics(c *gin.Context) {
16 db := database.EstablishDBConnection()
17 defer func(db *sql.DB) {
18 err := db.Close()
19 if err != nil {
20 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
21 return
22 }
23 }(db)
24
25 rows, err := db.Query("SELECT s.date, s.quantity, u.uuid, u.name FROM Statistics s INNER JOIN Users u ON u.id = s.user_id")
26 if err != nil {
27 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
28 return
29 }
30 defer func(rows *sql.Rows) {
31 err := rows.Close()
32 if err != nil {
33 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
34 return
35 }
36 }(rows)
37
38 var data []models.Statistic
39
40 for rows.Next() {
41 var stat models.Statistic
42 var user models.User
43 if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil {
44 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
45 return
46 }
47 stat.User = user
48 data = append(data, stat)
49 }
50
51 c.JSON(http.StatusOK, data)
52}
53
54func PostNewStatistic(c *gin.Context) {
55 var stat models.StatisticPost
56
57 if err := c.BindJSON(&stat); err != nil {
58 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
59 return
60 }
61
62 db := database.EstablishDBConnection()
63 defer func(db *sql.DB) {
64 err := db.Close()
65 if err != nil {
66 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
67 return
68 }
69 }(db)
70
71 result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity)
72
73 if err != nil {
74 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
75 }
76
77 id, err := result.LastInsertId()
78 if err != nil {
79 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
80 }
81
82 c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id})
83}
84
85func GetWeeklyStatistics(c *gin.Context) {
86 db := database.EstablishDBConnection()
87 defer func(db *sql.DB) {
88 err := db.Close()
89 if err != nil {
90 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
91 return
92 }
93 }(db)
94
95 rows, err := db.Query("SELECT date, total FROM `WeeklyStatisticsView`")
96 if err != nil {
97 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
98 return
99 }
100 defer func(rows *sql.Rows) {
101 err := rows.Close()
102 if err != nil {
103 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
104 return
105 }
106 }(rows)
107
108 var data []models.WeeklyStatistic
109 for rows.Next() {
110 var weeklyStat models.WeeklyStatistic
111 if err := rows.Scan(&weeklyStat.Date, &weeklyStat.Total); err != nil {
112 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
113 }
114 data = append(data, weeklyStat)
115 }
116
117 c.JSON(http.StatusOK, data)
118}
119
120func GetDailyUserStatistics(c *gin.Context) {
121 db := database.EstablishDBConnection()
122 defer func(db *sql.DB) {
123 err := db.Close()
124 if err != nil {
125 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
126 return
127 }
128 }(db)
129
130 rows, err := db.Query("SELECT name, total FROM DailyUserStatistics")
131
132 if err != nil {
133 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
134 return
135 }
136 defer func(rows *sql.Rows) {
137 err := rows.Close()
138 if err != nil {
139 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
140 return
141 }
142 }(rows)
143
144 var data []models.DailyUserTotals
145 for rows.Next() {
146 var stat models.DailyUserTotals
147 if err := rows.Scan(&stat.Name, &stat.Total); err != nil {
148 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
149 return
150 }
151 data = append(data, stat)
152 }
153
154 c.JSON(http.StatusOK, data)
155
156}
157
158func GetUserStatistics(c *gin.Context) {
159 c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")})
160}
161
162func UpdateUserStatistic(c *gin.Context) {
163 c.JSON(http.StatusNoContent, gin.H{"status": "No Content"})
164}
165
166func DeleteUserStatistic(c *gin.Context) {
167 c.JSON(http.StatusNoContent, gin.H{"status": "No Content"})
168}
diff --git a/api/internal/controllers/user.go b/api/internal/controllers/user.go
new file mode 100644
index 0000000..dbb09cf
--- /dev/null
+++ b/api/internal/controllers/user.go
@@ -0,0 +1,68 @@
1package controllers
2
3import (
4 "database/sql"
5 "errors"
6 "log"
7 "net/http"
8 "water/api/internal/database"
9 "water/api/internal/models"
10
11 "github.com/gin-gonic/gin"
12)
13
14func GetUser(c *gin.Context) {
15 c.JSON(http.StatusOK, gin.H{"message": "User found"})
16}
17func GetUserPreferences(c *gin.Context) {
18 db := database.EstablishDBConnection()
19 defer func(db *sql.DB) {
20 err := db.Close()
21 if err != nil {
22 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
23 return
24 }
25 }(db)
26
27 var preference models.Preference
28
29 row := db.QueryRow("SELECT id, user_id, color, size_id FROM Preferences WHERE user_id = ?", c.Param("id"))
30 if err := row.Scan(&preference.ID, &preference.UserID, &preference.Color, &preference.SizeID); err != nil {
31 if errors.Is(err, sql.ErrNoRows) {
32 c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
33 return
34 } else {
35 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
36 return
37 }
38 }
39
40 c.JSON(http.StatusOK, preference)
41}
42
43func UpdateUserPreferences(c *gin.Context) {
44 db := database.EstablishDBConnection()
45 defer func(db *sql.DB) {
46 err := db.Close()
47 if err != nil {
48 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
49 return
50 }
51 }(db)
52
53 var newPreferences models.Preference
54 if err := c.BindJSON(&newPreferences); err != nil {
55 c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
56 return
57 }
58
59 log.Printf("newPreferences: %v", newPreferences)
60
61 _, err := db.Exec("UPDATE Preferences SET color = ?, size_id = ? WHERE id = ?", newPreferences.Color, newPreferences.SizeID, newPreferences.ID)
62 if err != nil {
63 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
64 return
65 }
66
67 c.Status(http.StatusNoContent)
68}
diff --git a/api/internal/database/database.go b/api/internal/database/database.go
new file mode 100644
index 0000000..1866655
--- /dev/null
+++ b/api/internal/database/database.go
@@ -0,0 +1,24 @@
1package database
2
3import (
4 "database/sql"
5 _ "github.com/mattn/go-sqlite3"
6 "log"
7 "path/filepath"
8 "water/api/internal/config"
9)
10
11func EstablishDBConnection() *sql.DB {
12 c, err := config.Load()
13
14 driver := c.GetString("DB_DRIVER")
15 path, err := filepath.Abs(c.GetString("DB_PATH"))
16 if err != nil {
17 log.Fatal("There was and error getting the absolute path of the database.")
18 }
19 db, err := sql.Open(driver, path)
20 if err != nil {
21 panic(err)
22 }
23 return db
24} \ No newline at end of file
diff --git a/api/internal/middleware/middleware.go b/api/internal/middleware/middleware.go
new file mode 100644
index 0000000..aa27fb8
--- /dev/null
+++ b/api/internal/middleware/middleware.go
@@ -0,0 +1,56 @@
1package middleware
2
3import (
4 "errors"
5 "log"
6 "net/http"
7 "strings"
8
9 "github.com/gin-gonic/gin"
10)
11
12func TokenRequired() gin.HandlerFunc {
13 return func(c *gin.Context) {
14 _, err := checkForTokenInContext(c)
15
16 if err != nil {
17 c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
18 c.Abort()
19 return
20 }
21
22 c.Next()
23 }
24}
25
26func CORSMiddleware() gin.HandlerFunc {
27 return func(c *gin.Context) {
28 c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
29 c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
30 c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
31 c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH")
32
33 if c.Request.Method == "OPTIONS" {
34 log.Println(c.Request.Header)
35 c.AbortWithStatus(http.StatusNoContent)
36 return
37 }
38
39 c.Next()
40 }
41}
42
43func checkForTokenInContext(c *gin.Context) (string, error) {
44 authorizationHeader := c.GetHeader("Authorization")
45 if authorizationHeader == "" {
46 return "", errors.New("authorization header is missing")
47 }
48
49 parts := strings.Split(authorizationHeader, " ")
50
51 if len(parts) != 2 || parts[0] != "Bearer" {
52 return "", errors.New("invalid Authorization header format")
53 }
54
55 return parts[1], nil
56}
diff --git a/api/internal/models/auth.go b/api/internal/models/auth.go
new file mode 100644
index 0000000..fa7dbe4
--- /dev/null
+++ b/api/internal/models/auth.go
@@ -0,0 +1,11 @@
1package models
2
3import "time"
4
5type Token struct {
6 ID int64 `json:"id"`
7 UserID int64 `json:"user_id"`
8 Token string `json:"token"`
9 CreatedAt time.Time `json:"created_at"`
10 ExpiredAt time.Time `json:"expired_at"`
11}
diff --git a/api/internal/models/preferences.go b/api/internal/models/preferences.go
new file mode 100644
index 0000000..8022099
--- /dev/null
+++ b/api/internal/models/preferences.go
@@ -0,0 +1,14 @@
1package models
2
3type Preference struct {
4 ID int64 `json:"id"`
5 Color string `json:"color"`
6 UserID int64 `json:"user_id"`
7 SizeID int64 `json:"size_id"`
8}
9
10type Size struct {
11 ID int64 `json:"id"`
12 Size int64 `json:"size"`
13 Unit string `json:"unit"`
14}
diff --git a/api/internal/models/statistics.go b/api/internal/models/statistics.go
new file mode 100644
index 0000000..7dceb3a
--- /dev/null
+++ b/api/internal/models/statistics.go
@@ -0,0 +1,26 @@
1package models
2
3import "time"
4
5type Statistic struct {
6 ID int64 `json:"id"`
7 Date time.Time `json:"date"`
8 User User `json:"user"`
9 Quantity int `json:"quantity"`
10}
11
12type StatisticPost struct {
13 Date time.Time `json:"date"`
14 Quantity int64 `json:"quantity"`
15 UserID int64 `json:"user_id"`
16}
17
18type WeeklyStatistic struct {
19 Date string `json:"date"`
20 Total int64 `json:"total"`
21}
22
23type DailyUserTotals struct {
24 Name string `json:"name"`
25 Total int64 `json:"total"`
26}
diff --git a/api/internal/models/user.go b/api/internal/models/user.go
new file mode 100644
index 0000000..ca5daa4
--- /dev/null
+++ b/api/internal/models/user.go
@@ -0,0 +1,10 @@
1package models
2
3import "github.com/google/uuid"
4
5type User struct {
6 ID int64 `json:"id"`
7 Name string `json:"name"`
8 UUID uuid.UUID `json:"uuid"`
9 Password string `json:"-"`
10}
diff --git a/api/internal/router/router.go b/api/internal/router/router.go
new file mode 100644
index 0000000..a71c3e6
--- /dev/null
+++ b/api/internal/router/router.go
@@ -0,0 +1,43 @@
1package router
2
3import (
4 "github.com/gin-gonic/gin"
5 "water/api/internal/controllers"
6 "water/api/internal/middleware"
7)
8
9func SetupRouter() *gin.Engine {
10 // Disable Console Color
11 // gin.DisableConsoleColor()
12 r := gin.Default()
13 r.Use(middleware.CORSMiddleware())
14 r.Use(gin.Logger())
15 r.Use(gin.Recovery())
16
17 api := r.Group("api/v1")
18
19 api.POST("/auth", controllers.AuthHandler)
20 api.GET("/sizes", middleware.TokenRequired(), controllers.GetSizes)
21 api.PATCH("/user/preferences", controllers.UpdateUserPreferences)
22
23 user := api.Group("/user/:id")
24 user.Use(middleware.TokenRequired())
25 {
26 user.GET("", controllers.GetUser)
27 user.GET("preferences", controllers.GetUserPreferences)
28 }
29
30 stats := api.Group("/stats")
31 stats.Use(middleware.TokenRequired())
32 {
33 stats.GET("", controllers.GetAllStatistics)
34 stats.POST("", controllers.PostNewStatistic)
35 stats.GET("weekly", controllers.GetWeeklyStatistics)
36 stats.GET("daily", controllers.GetDailyUserStatistics)
37 stats.GET("user/:uuid", controllers.GetUserStatistics)
38 stats.PATCH("user/:uuid", controllers.UpdateUserStatistic)
39 stats.DELETE("user/:uuid", controllers.DeleteUserStatistic)
40 }
41
42 return r
43} \ No newline at end of file
diff --git a/db/scripts/water_init.sql b/db/scripts/water_init.sql
new file mode 100644
index 0000000..3b79ed5
--- /dev/null
+++ b/db/scripts/water_init.sql
@@ -0,0 +1,97 @@
1-- user table for users.
2CREATE TABLE IF NOT EXISTS Users (
3 id INTEGER PRIMARY KEY,
4 password TEXT UNIQUE NOT NULL,
5 uuid TEXT UNIQUE NOT NULL,
6 name TEXT UNIQUE NOT NULL
7);
8
9-- statistics table for users to log their consumption
10CREATE TABLE IF NOT EXISTS Statistics (
11 id INTEGER PRIMARY KEY,
12 date DATETIME NOT NULL,
13 user_id INT NOT NULL,
14 quantity INT
15);
16
17-- preferences table for a user.
18CREATE TABLE IF NOT EXISTS Preferences (
19 id INTEGER PRIMARY KEY,
20 color TEXT NOT NULL DEFAULT "#000000",
21 user_id INT UNIQUE NOT NULL,
22 size_id INT NOT NULL DEFAULT 1,
23 FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE,
24 FOREIGN KEY(size_id) REFERENCES Sizes(id)
25);
26
27-- lookup table for sizes.
28CREATE TABLE IF NOT EXISTS Sizes (
29 id INTEGER PRIMARY KEY,
30 size INT NOT NULL,
31 unit TEXT DEFAULT "oz"
32);
33
34CREATE TABLE IF NOT EXISTS APIToken (
35 id INTEGER PRIMARY KEY,
36 token TEXT NOT NULL,
37 user_id INTEGER NOT NULL,
38 FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE
39);
40
41-- create default sizes for sizes lookup table.
42INSERT OR IGNORE INTO Sizes (id, size) VALUES (1, 8), (2, 16), (3, 24), (4, 32), (5, 40), (6, 48);
43
44-- create default users.
45INSERT OR IGNORE INTO Users (name, password, uuid) VALUES (
46 'parker',
47 '$2y$10$2UlKrQJQV5cQOo/8VcFlq.ai3MWf7mA4//knEs2xVnHTeB.RnfN.m',
48 '1aa668f3-7527-4a67-9c24-fdf307542eeb'
49), (
50 'zach',
51 '$2y$10$35UJnLpBj8ulhqN/3G4qKe0GYBOa/YunXit11n7ET6zknZpNeKpRS',
52 'be3fd6b7-cf55-4eb8-92d8-1b745b439f34'
53);
54
55-- create default preferences.
56INSERT OR IGNORE INTO Preferences (user_id) VALUES (1), (2);
57
58CREATE TRIGGER IF NOT EXISTS enforce_size_id
59BEFORE INSERT ON Preferences
60BEGIN
61 SELECT
62 CASE
63 WHEN (
64 SELECT COUNT(*) FROM Sizes WHERE id = new.size_id
65 ) = 0
66 THEN RAISE(ABORT, 'Size does not exist')
67 END;
68END;
69
70--
71CREATE VIEW IF NOT EXISTS aggregated_stats AS
72 SELECT u.uuid, SUM(s.quantity * s.size) from Statistics s INNER JOIN Users u ON u.id = s.user_id INNER JOIN Preferences p ON p.user_id = u.id INNER JOIN Size s ON s.id = p.size_id;
73
74CREATE VIEW IF NOT EXISTS `DailyUserStatistics` AS
75SELECT users.name, IFNULL(SUM(statistics.quantity), 0) as total, preferences.color as color
76FROM users
77LEFT JOIN statistics ON users.id = statistics.user_id AND DATE(statistics.date) = DATE('now', '-1 day')
78LEFT JOIN preferences ON users.id = preferences.user_id
79GROUP BY users.name;
80
81
82CREATE VIEW IF NOT EXISTS `WeeklyStatisticsView` AS
83 WITH DateSequence(Dates) AS
84 (
85 SELECT Date(CURRENT_DATE, '-7 day')
86 UNION ALL
87 SELECT Date(Dates, '+1 day')
88 FROM DateSequence
89 WHERE Date(Dates, '+1 day') < Date(CURRENT_DATE)
90 )
91SELECT DateSequence.Dates as 'date',
92 IFNULL(SUM(statistics.quantity), 0) AS 'total'
93FROM DateSequence
94LEFT JOIN statistics
95ON Date(statistics.date) = DateSequence.Dates
96GROUP BY DateSequence.Dates
97ORDER BY DateSequence.Dates; \ No newline at end of file
diff --git a/fe/.env.sample b/fe/.env.sample
new file mode 100644
index 0000000..60c383f
--- /dev/null
+++ b/fe/.env.sample
@@ -0,0 +1 @@
VITE_API_BASE_URL="https://www.example.org" \ No newline at end of file
diff --git a/fe/.gitignore b/fe/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/fe/.gitignore
@@ -0,0 +1,24 @@
1# Logs
2logs
3*.log
4npm-debug.log*
5yarn-debug.log*
6yarn-error.log*
7pnpm-debug.log*
8lerna-debug.log*
9
10node_modules
11dist
12dist-ssr
13*.local
14
15# Editor directories and files
16.vscode/*
17!.vscode/extensions.json
18.idea
19.DS_Store
20*.suo
21*.ntvs*
22*.njsproj
23*.sln
24*.sw?
diff --git a/fe/.prettierrc b/fe/.prettierrc
new file mode 100644
index 0000000..222861c
--- /dev/null
+++ b/fe/.prettierrc
@@ -0,0 +1,4 @@
1{
2 "tabWidth": 2,
3 "useTabs": false
4}
diff --git a/fe/.vscode/extensions.json b/fe/.vscode/extensions.json
new file mode 100644
index 0000000..bdef820
--- /dev/null
+++ b/fe/.vscode/extensions.json
@@ -0,0 +1,3 @@
1{
2 "recommendations": ["svelte.svelte-vscode"]
3}
diff --git a/fe/README.md b/fe/README.md
new file mode 100644
index 0000000..e6cd94f
--- /dev/null
+++ b/fe/README.md
@@ -0,0 +1,47 @@
1# Svelte + TS + Vite
2
3This template should help get you started developing with Svelte and TypeScript in Vite.
4
5## Recommended IDE Setup
6
7[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
8
9## Need an official Svelte framework?
10
11Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
12
13## Technical considerations
14
15**Why use this over SvelteKit?**
16
17- It brings its own routing solution which might not be preferable for some users.
18- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
19
20This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
21
22Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
23
24**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
25
26Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
27
28**Why include `.vscode/extensions.json`?**
29
30Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
31
32**Why enable `allowJs` in the TS template?**
33
34While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
35
36**Why is HMR not preserving my local component state?**
37
38HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
39
40If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
41
42```ts
43// store.ts
44// An extremely simple external store
45import { writable } from 'svelte/store'
46export default writable(0)
47```
diff --git a/fe/index.html b/fe/index.html
new file mode 100644
index 0000000..b6c5f0a
--- /dev/null
+++ b/fe/index.html
@@ -0,0 +1,13 @@
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>Vite + Svelte + TS</title>
8 </head>
9 <body>
10 <div id="app"></div>
11 <script type="module" src="/src/main.ts"></script>
12 </body>
13</html>
diff --git a/fe/package-lock.json b/fe/package-lock.json
new file mode 100644
index 0000000..f74bf6c
--- /dev/null
+++ b/fe/package-lock.json
@@ -0,0 +1,1780 @@
1{
2 "name": "fe",
3 "version": "0.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "fe",
9 "version": "0.0.0",
10 "dependencies": {
11 "chart.js": "^4.4.2",
12 "svelte-chartjs": "^3.1.5"
13 },
14 "devDependencies": {
15 "@sveltejs/vite-plugin-svelte": "^3.0.2",
16 "@tsconfig/svelte": "^5.0.2",
17 "svelte": "^4.2.10",
18 "svelte-check": "^3.6.3",
19 "tslib": "^2.6.2",
20 "typescript": "^5.2.2",
21 "vite": "^5.1.0"
22 }
23 },
24 "node_modules/@ampproject/remapping": {
25 "version": "2.2.1",
26 "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
27 "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
28 "dependencies": {
29 "@jridgewell/gen-mapping": "^0.3.0",
30 "@jridgewell/trace-mapping": "^0.3.9"
31 },
32 "engines": {
33 "node": ">=6.0.0"
34 }
35 },
36 "node_modules/@esbuild/aix-ppc64": {
37 "version": "0.19.12",
38 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
39 "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
40 "cpu": [
41 "ppc64"
42 ],
43 "dev": true,
44 "optional": true,
45 "os": [
46 "aix"
47 ],
48 "engines": {
49 "node": ">=12"
50 }
51 },
52 "node_modules/@esbuild/android-arm": {
53 "version": "0.19.12",
54 "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
55 "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
56 "cpu": [
57 "arm"
58 ],
59 "dev": true,
60 "optional": true,
61 "os": [
62 "android"
63 ],
64 "engines": {
65 "node": ">=12"
66 }
67 },
68 "node_modules/@esbuild/android-arm64": {
69 "version": "0.19.12",
70 "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
71 "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
72 "cpu": [
73 "arm64"
74 ],
75 "dev": true,
76 "optional": true,
77 "os": [
78 "android"
79 ],
80 "engines": {
81 "node": ">=12"
82 }
83 },
84 "node_modules/@esbuild/android-x64": {
85 "version": "0.19.12",
86 "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
87 "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
88 "cpu": [
89 "x64"
90 ],
91 "dev": true,
92 "optional": true,
93 "os": [
94 "android"
95 ],
96 "engines": {
97 "node": ">=12"
98 }
99 },
100 "node_modules/@esbuild/darwin-arm64": {
101 "version": "0.19.12",
102 "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
103 "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
104 "cpu": [
105 "arm64"
106 ],
107 "dev": true,
108 "optional": true,
109 "os": [
110 "darwin"
111 ],
112 "engines": {
113 "node": ">=12"
114 }
115 },
116 "node_modules/@esbuild/darwin-x64": {
117 "version": "0.19.12",
118 "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
119 "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
120 "cpu": [
121 "x64"
122 ],
123 "dev": true,
124 "optional": true,
125 "os": [
126 "darwin"
127 ],
128 "engines": {
129 "node": ">=12"
130 }
131 },
132 "node_modules/@esbuild/freebsd-arm64": {
133 "version": "0.19.12",
134 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
135 "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
136 "cpu": [
137 "arm64"
138 ],
139 "dev": true,
140 "optional": true,
141 "os": [
142 "freebsd"
143 ],
144 "engines": {
145 "node": ">=12"
146 }
147 },
148 "node_modules/@esbuild/freebsd-x64": {
149 "version": "0.19.12",
150 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
151 "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
152 "cpu": [
153 "x64"
154 ],
155 "dev": true,
156 "optional": true,
157 "os": [
158 "freebsd"
159 ],
160 "engines": {
161 "node": ">=12"
162 }
163 },
164 "node_modules/@esbuild/linux-arm": {
165 "version": "0.19.12",
166 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
167 "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
168 "cpu": [
169 "arm"
170 ],
171 "dev": true,
172 "optional": true,
173 "os": [
174 "linux"
175 ],
176 "engines": {
177 "node": ">=12"
178 }
179 },
180 "node_modules/@esbuild/linux-arm64": {
181 "version": "0.19.12",
182 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
183 "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
184 "cpu": [
185 "arm64"
186 ],
187 "dev": true,
188 "optional": true,
189 "os": [
190 "linux"
191 ],
192 "engines": {
193 "node": ">=12"
194 }
195 },
196 "node_modules/@esbuild/linux-ia32": {
197 "version": "0.19.12",
198 "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
199 "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
200 "cpu": [
201 "ia32"
202 ],
203 "dev": true,
204 "optional": true,
205 "os": [
206 "linux"
207 ],
208 "engines": {
209 "node": ">=12"
210 }
211 },
212 "node_modules/@esbuild/linux-loong64": {
213 "version": "0.19.12",
214 "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
215 "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
216 "cpu": [
217 "loong64"
218 ],
219 "dev": true,
220 "optional": true,
221 "os": [
222 "linux"
223 ],
224 "engines": {
225 "node": ">=12"
226 }
227 },
228 "node_modules/@esbuild/linux-mips64el": {
229 "version": "0.19.12",
230 "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
231 "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
232 "cpu": [
233 "mips64el"
234 ],
235 "dev": true,
236 "optional": true,
237 "os": [
238 "linux"
239 ],
240 "engines": {
241 "node": ">=12"
242 }
243 },
244 "node_modules/@esbuild/linux-ppc64": {
245 "version": "0.19.12",
246 "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
247 "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
248 "cpu": [
249 "ppc64"
250 ],
251 "dev": true,
252 "optional": true,
253 "os": [
254 "linux"
255 ],
256 "engines": {
257 "node": ">=12"
258 }
259 },
260 "node_modules/@esbuild/linux-riscv64": {
261 "version": "0.19.12",
262 "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
263 "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
264 "cpu": [
265 "riscv64"
266 ],
267 "dev": true,
268 "optional": true,
269 "os": [
270 "linux"
271 ],
272 "engines": {
273 "node": ">=12"
274 }
275 },
276 "node_modules/@esbuild/linux-s390x": {
277 "version": "0.19.12",
278 "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
279 "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
280 "cpu": [
281 "s390x"
282 ],
283 "dev": true,
284 "optional": true,
285 "os": [
286 "linux"
287 ],
288 "engines": {
289 "node": ">=12"
290 }
291 },
292 "node_modules/@esbuild/linux-x64": {
293 "version": "0.19.12",
294 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
295 "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
296 "cpu": [
297 "x64"
298 ],
299 "dev": true,
300 "optional": true,
301 "os": [
302 "linux"
303 ],
304 "engines": {
305 "node": ">=12"
306 }
307 },
308 "node_modules/@esbuild/netbsd-x64": {
309 "version": "0.19.12",
310 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
311 "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
312 "cpu": [
313 "x64"
314 ],
315 "dev": true,
316 "optional": true,
317 "os": [
318 "netbsd"
319 ],
320 "engines": {
321 "node": ">=12"
322 }
323 },
324 "node_modules/@esbuild/openbsd-x64": {
325 "version": "0.19.12",
326 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
327 "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
328 "cpu": [
329 "x64"
330 ],
331 "dev": true,
332 "optional": true,
333 "os": [
334 "openbsd"
335 ],
336 "engines": {
337 "node": ">=12"
338 }
339 },
340 "node_modules/@esbuild/sunos-x64": {
341 "version": "0.19.12",
342 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
343 "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
344 "cpu": [
345 "x64"
346 ],
347 "dev": true,
348 "optional": true,
349 "os": [
350 "sunos"
351 ],
352 "engines": {
353 "node": ">=12"
354 }
355 },
356 "node_modules/@esbuild/win32-arm64": {
357 "version": "0.19.12",
358 "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
359 "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
360 "cpu": [
361 "arm64"
362 ],
363 "dev": true,
364 "optional": true,
365 "os": [
366 "win32"
367 ],
368 "engines": {
369 "node": ">=12"
370 }
371 },
372 "node_modules/@esbuild/win32-ia32": {
373 "version": "0.19.12",
374 "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
375 "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
376 "cpu": [
377 "ia32"
378 ],
379 "dev": true,
380 "optional": true,
381 "os": [
382 "win32"
383 ],
384 "engines": {
385 "node": ">=12"
386 }
387 },
388 "node_modules/@esbuild/win32-x64": {
389 "version": "0.19.12",
390 "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
391 "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
392 "cpu": [
393 "x64"
394 ],
395 "dev": true,
396 "optional": true,
397 "os": [
398 "win32"
399 ],
400 "engines": {
401 "node": ">=12"
402 }
403 },
404 "node_modules/@jridgewell/gen-mapping": {
405 "version": "0.3.3",
406 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
407 "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
408 "dependencies": {
409 "@jridgewell/set-array": "^1.0.1",
410 "@jridgewell/sourcemap-codec": "^1.4.10",
411 "@jridgewell/trace-mapping": "^0.3.9"
412 },
413 "engines": {
414 "node": ">=6.0.0"
415 }
416 },
417 "node_modules/@jridgewell/resolve-uri": {
418 "version": "3.1.2",
419 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
420 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
421 "engines": {
422 "node": ">=6.0.0"
423 }
424 },
425 "node_modules/@jridgewell/set-array": {
426 "version": "1.1.2",
427 "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
428 "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
429 "engines": {
430 "node": ">=6.0.0"
431 }
432 },
433 "node_modules/@jridgewell/sourcemap-codec": {
434 "version": "1.4.15",
435 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
436 "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
437 },
438 "node_modules/@jridgewell/trace-mapping": {
439 "version": "0.3.22",
440 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
441 "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
442 "dependencies": {
443 "@jridgewell/resolve-uri": "^3.1.0",
444 "@jridgewell/sourcemap-codec": "^1.4.14"
445 }
446 },
447 "node_modules/@kurkle/color": {
448 "version": "0.3.2",
449 "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
450 "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
451 },
452 "node_modules/@nodelib/fs.scandir": {
453 "version": "2.1.5",
454 "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
455 "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
456 "dev": true,
457 "dependencies": {
458 "@nodelib/fs.stat": "2.0.5",
459 "run-parallel": "^1.1.9"
460 },
461 "engines": {
462 "node": ">= 8"
463 }
464 },
465 "node_modules/@nodelib/fs.stat": {
466 "version": "2.0.5",
467 "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
468 "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
469 "dev": true,
470 "engines": {
471 "node": ">= 8"
472 }
473 },
474 "node_modules/@nodelib/fs.walk": {
475 "version": "1.2.8",
476 "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
477 "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
478 "dev": true,
479 "dependencies": {
480 "@nodelib/fs.scandir": "2.1.5",
481 "fastq": "^1.6.0"
482 },
483 "engines": {
484 "node": ">= 8"
485 }
486 },
487 "node_modules/@rollup/rollup-android-arm-eabi": {
488 "version": "4.12.0",
489 "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz",
490 "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==",
491 "cpu": [
492 "arm"
493 ],
494 "dev": true,
495 "optional": true,
496 "os": [
497 "android"
498 ]
499 },
500 "node_modules/@rollup/rollup-android-arm64": {
501 "version": "4.12.0",
502 "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz",
503 "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==",
504 "cpu": [
505 "arm64"
506 ],
507 "dev": true,
508 "optional": true,
509 "os": [
510 "android"
511 ]
512 },
513 "node_modules/@rollup/rollup-darwin-arm64": {
514 "version": "4.12.0",
515 "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz",
516 "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==",
517 "cpu": [
518 "arm64"
519 ],
520 "dev": true,
521 "optional": true,
522 "os": [
523 "darwin"
524 ]
525 },
526 "node_modules/@rollup/rollup-darwin-x64": {
527 "version": "4.12.0",
528 "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz",
529 "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==",
530 "cpu": [
531 "x64"
532 ],
533 "dev": true,
534 "optional": true,
535 "os": [
536 "darwin"
537 ]
538 },
539 "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
540 "version": "4.12.0",
541 "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz",
542 "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==",
543 "cpu": [
544 "arm"
545 ],
546 "dev": true,
547 "optional": true,
548 "os": [
549 "linux"
550 ]
551 },
552 "node_modules/@rollup/rollup-linux-arm64-gnu": {
553 "version": "4.12.0",
554 "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz",
555 "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==",
556 "cpu": [
557 "arm64"
558 ],
559 "dev": true,
560 "optional": true,
561 "os": [
562 "linux"
563 ]
564 },
565 "node_modules/@rollup/rollup-linux-arm64-musl": {
566 "version": "4.12.0",
567 "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz",
568 "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==",
569 "cpu": [
570 "arm64"
571 ],
572 "dev": true,
573 "optional": true,
574 "os": [
575 "linux"
576 ]
577 },
578 "node_modules/@rollup/rollup-linux-riscv64-gnu": {
579 "version": "4.12.0",
580 "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz",
581 "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==",
582 "cpu": [
583 "riscv64"
584 ],
585 "dev": true,
586 "optional": true,
587 "os": [
588 "linux"
589 ]
590 },
591 "node_modules/@rollup/rollup-linux-x64-gnu": {
592 "version": "4.12.0",
593 "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz",
594 "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==",
595 "cpu": [
596 "x64"
597 ],
598 "dev": true,
599 "optional": true,
600 "os": [
601 "linux"
602 ]
603 },
604 "node_modules/@rollup/rollup-linux-x64-musl": {
605 "version": "4.12.0",
606 "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz",
607 "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==",
608 "cpu": [
609 "x64"
610 ],
611 "dev": true,
612 "optional": true,
613 "os": [
614 "linux"
615 ]
616 },
617 "node_modules/@rollup/rollup-win32-arm64-msvc": {
618 "version": "4.12.0",
619 "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz",
620 "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==",
621 "cpu": [
622 "arm64"
623 ],
624 "dev": true,
625 "optional": true,
626 "os": [
627 "win32"
628 ]
629 },
630 "node_modules/@rollup/rollup-win32-ia32-msvc": {
631 "version": "4.12.0",
632 "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz",
633 "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==",
634 "cpu": [
635 "ia32"
636 ],
637 "dev": true,
638 "optional": true,
639 "os": [
640 "win32"
641 ]
642 },
643 "node_modules/@rollup/rollup-win32-x64-msvc": {
644 "version": "4.12.0",
645 "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz",
646 "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==",
647 "cpu": [
648 "x64"
649 ],
650 "dev": true,
651 "optional": true,
652 "os": [
653 "win32"
654 ]
655 },
656 "node_modules/@sveltejs/vite-plugin-svelte": {
657 "version": "3.0.2",
658 "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.2.tgz",
659 "integrity": "sha512-MpmF/cju2HqUls50WyTHQBZUV3ovV/Uk8k66AN2gwHogNAG8wnW8xtZDhzNBsFJJuvmq1qnzA5kE7YfMJNFv2Q==",
660 "dev": true,
661 "dependencies": {
662 "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0",
663 "debug": "^4.3.4",
664 "deepmerge": "^4.3.1",
665 "kleur": "^4.1.5",
666 "magic-string": "^0.30.5",
667 "svelte-hmr": "^0.15.3",
668 "vitefu": "^0.2.5"
669 },
670 "engines": {
671 "node": "^18.0.0 || >=20"
672 },
673 "peerDependencies": {
674 "svelte": "^4.0.0 || ^5.0.0-next.0",
675 "vite": "^5.0.0"
676 }
677 },
678 "node_modules/@sveltejs/vite-plugin-svelte-inspector": {
679 "version": "2.0.0",
680 "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.0.0.tgz",
681 "integrity": "sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==",
682 "dev": true,
683 "dependencies": {
684 "debug": "^4.3.4"
685 },
686 "engines": {
687 "node": "^18.0.0 || >=20"
688 },
689 "peerDependencies": {
690 "@sveltejs/vite-plugin-svelte": "^3.0.0",
691 "svelte": "^4.0.0 || ^5.0.0-next.0",
692 "vite": "^5.0.0"
693 }
694 },
695 "node_modules/@tsconfig/svelte": {
696 "version": "5.0.2",
697 "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.2.tgz",
698 "integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==",
699 "dev": true
700 },
701 "node_modules/@types/estree": {
702 "version": "1.0.5",
703 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
704 "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
705 },
706 "node_modules/@types/pug": {
707 "version": "2.0.10",
708 "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
709 "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
710 "dev": true
711 },
712 "node_modules/acorn": {
713 "version": "8.11.3",
714 "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
715 "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
716 "bin": {
717 "acorn": "bin/acorn"
718 },
719 "engines": {
720 "node": ">=0.4.0"
721 }
722 },
723 "node_modules/anymatch": {
724 "version": "3.1.3",
725 "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
726 "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
727 "dev": true,
728 "dependencies": {
729 "normalize-path": "^3.0.0",
730 "picomatch": "^2.0.4"
731 },
732 "engines": {
733 "node": ">= 8"
734 }
735 },
736 "node_modules/aria-query": {
737 "version": "5.3.0",
738 "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
739 "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
740 "dependencies": {
741 "dequal": "^2.0.3"
742 }
743 },
744 "node_modules/axobject-query": {
745 "version": "4.0.0",
746 "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
747 "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==",
748 "dependencies": {
749 "dequal": "^2.0.3"
750 }
751 },
752 "node_modules/balanced-match": {
753 "version": "1.0.2",
754 "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
755 "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
756 "dev": true
757 },
758 "node_modules/binary-extensions": {
759 "version": "2.2.0",
760 "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
761 "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
762 "dev": true,
763 "engines": {
764 "node": ">=8"
765 }
766 },
767 "node_modules/brace-expansion": {
768 "version": "1.1.11",
769 "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
770 "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
771 "dev": true,
772 "dependencies": {
773 "balanced-match": "^1.0.0",
774 "concat-map": "0.0.1"
775 }
776 },
777 "node_modules/braces": {
778 "version": "3.0.2",
779 "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
780 "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
781 "dev": true,
782 "dependencies": {
783 "fill-range": "^7.0.1"
784 },
785 "engines": {
786 "node": ">=8"
787 }
788 },
789 "node_modules/buffer-crc32": {
790 "version": "0.2.13",
791 "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
792 "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
793 "dev": true,
794 "engines": {
795 "node": "*"
796 }
797 },
798 "node_modules/callsites": {
799 "version": "3.1.0",
800 "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
801 "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
802 "dev": true,
803 "engines": {
804 "node": ">=6"
805 }
806 },
807 "node_modules/chart.js": {
808 "version": "4.4.2",
809 "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
810 "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
811 "dependencies": {
812 "@kurkle/color": "^0.3.0"
813 },
814 "engines": {
815 "pnpm": ">=8"
816 }
817 },
818 "node_modules/chokidar": {
819 "version": "3.6.0",
820 "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
821 "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
822 "dev": true,
823 "dependencies": {
824 "anymatch": "~3.1.2",
825 "braces": "~3.0.2",
826 "glob-parent": "~5.1.2",
827 "is-binary-path": "~2.1.0",
828 "is-glob": "~4.0.1",
829 "normalize-path": "~3.0.0",
830 "readdirp": "~3.6.0"
831 },
832 "engines": {
833 "node": ">= 8.10.0"
834 },
835 "funding": {
836 "url": "https://paulmillr.com/funding/"
837 },
838 "optionalDependencies": {
839 "fsevents": "~2.3.2"
840 }
841 },
842 "node_modules/code-red": {
843 "version": "1.0.4",
844 "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
845 "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",
846 "dependencies": {
847 "@jridgewell/sourcemap-codec": "^1.4.15",
848 "@types/estree": "^1.0.1",
849 "acorn": "^8.10.0",
850 "estree-walker": "^3.0.3",
851 "periscopic": "^3.1.0"
852 }
853 },
854 "node_modules/concat-map": {
855 "version": "0.0.1",
856 "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
857 "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
858 "dev": true
859 },
860 "node_modules/css-tree": {
861 "version": "2.3.1",
862 "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
863 "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
864 "dependencies": {
865 "mdn-data": "2.0.30",
866 "source-map-js": "^1.0.1"
867 },
868 "engines": {
869 "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
870 }
871 },
872 "node_modules/debug": {
873 "version": "4.3.4",
874 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
875 "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
876 "dev": true,
877 "dependencies": {
878 "ms": "2.1.2"
879 },
880 "engines": {
881 "node": ">=6.0"
882 },
883 "peerDependenciesMeta": {
884 "supports-color": {
885 "optional": true
886 }
887 }
888 },
889 "node_modules/deepmerge": {
890 "version": "4.3.1",
891 "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
892 "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
893 "dev": true,
894 "engines": {
895 "node": ">=0.10.0"
896 }
897 },
898 "node_modules/dequal": {
899 "version": "2.0.3",
900 "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
901 "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
902 "engines": {
903 "node": ">=6"
904 }
905 },
906 "node_modules/detect-indent": {
907 "version": "6.1.0",
908 "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
909 "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
910 "dev": true,
911 "engines": {
912 "node": ">=8"
913 }
914 },
915 "node_modules/es6-promise": {
916 "version": "3.3.1",
917 "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
918 "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==",
919 "dev": true
920 },
921 "node_modules/esbuild": {
922 "version": "0.19.12",
923 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
924 "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
925 "dev": true,
926 "hasInstallScript": true,
927 "bin": {
928 "esbuild": "bin/esbuild"
929 },
930 "engines": {
931 "node": ">=12"
932 },
933 "optionalDependencies": {
934 "@esbuild/aix-ppc64": "0.19.12",
935 "@esbuild/android-arm": "0.19.12",
936 "@esbuild/android-arm64": "0.19.12",
937 "@esbuild/android-x64": "0.19.12",
938 "@esbuild/darwin-arm64": "0.19.12",
939 "@esbuild/darwin-x64": "0.19.12",
940 "@esbuild/freebsd-arm64": "0.19.12",
941 "@esbuild/freebsd-x64": "0.19.12",
942 "@esbuild/linux-arm": "0.19.12",
943 "@esbuild/linux-arm64": "0.19.12",
944 "@esbuild/linux-ia32": "0.19.12",
945 "@esbuild/linux-loong64": "0.19.12",
946 "@esbuild/linux-mips64el": "0.19.12",
947 "@esbuild/linux-ppc64": "0.19.12",
948 "@esbuild/linux-riscv64": "0.19.12",
949 "@esbuild/linux-s390x": "0.19.12",
950 "@esbuild/linux-x64": "0.19.12",
951 "@esbuild/netbsd-x64": "0.19.12",
952 "@esbuild/openbsd-x64": "0.19.12",
953 "@esbuild/sunos-x64": "0.19.12",
954 "@esbuild/win32-arm64": "0.19.12",
955 "@esbuild/win32-ia32": "0.19.12",
956 "@esbuild/win32-x64": "0.19.12"
957 }
958 },
959 "node_modules/estree-walker": {
960 "version": "3.0.3",
961 "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
962 "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
963 "dependencies": {
964 "@types/estree": "^1.0.0"
965 }
966 },
967 "node_modules/fast-glob": {
968 "version": "3.3.2",
969 "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
970 "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
971 "dev": true,
972 "dependencies": {
973 "@nodelib/fs.stat": "^2.0.2",
974 "@nodelib/fs.walk": "^1.2.3",
975 "glob-parent": "^5.1.2",
976 "merge2": "^1.3.0",
977 "micromatch": "^4.0.4"
978 },
979 "engines": {
980 "node": ">=8.6.0"
981 }
982 },
983 "node_modules/fastq": {
984 "version": "1.17.1",
985 "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
986 "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
987 "dev": true,
988 "dependencies": {
989 "reusify": "^1.0.4"
990 }
991 },
992 "node_modules/fill-range": {
993 "version": "7.0.1",
994 "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
995 "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
996 "dev": true,
997 "dependencies": {
998 "to-regex-range": "^5.0.1"
999 },
1000 "engines": {
1001 "node": ">=8"
1002 }
1003 },
1004 "node_modules/fs.realpath": {
1005 "version": "1.0.0",
1006 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
1007 "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
1008 "dev": true
1009 },
1010 "node_modules/fsevents": {
1011 "version": "2.3.3",
1012 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1013 "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1014 "dev": true,
1015 "hasInstallScript": true,
1016 "optional": true,
1017 "os": [
1018 "darwin"
1019 ],
1020 "engines": {
1021 "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1022 }
1023 },
1024 "node_modules/glob": {
1025 "version": "7.2.3",
1026 "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
1027 "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
1028 "dev": true,
1029 "dependencies": {
1030 "fs.realpath": "^1.0.0",
1031 "inflight": "^1.0.4",
1032 "inherits": "2",
1033 "minimatch": "^3.1.1",
1034 "once": "^1.3.0",
1035 "path-is-absolute": "^1.0.0"
1036 },
1037 "engines": {
1038 "node": "*"
1039 },
1040 "funding": {
1041 "url": "https://github.com/sponsors/isaacs"
1042 }
1043 },
1044 "node_modules/glob-parent": {
1045 "version": "5.1.2",
1046 "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1047 "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1048 "dev": true,
1049 "dependencies": {
1050 "is-glob": "^4.0.1"
1051 },
1052 "engines": {
1053 "node": ">= 6"
1054 }
1055 },
1056 "node_modules/graceful-fs": {
1057 "version": "4.2.11",
1058 "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
1059 "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
1060 "dev": true
1061 },
1062 "node_modules/import-fresh": {
1063 "version": "3.3.0",
1064 "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
1065 "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
1066 "dev": true,
1067 "dependencies": {
1068 "parent-module": "^1.0.0",
1069 "resolve-from": "^4.0.0"
1070 },
1071 "engines": {
1072 "node": ">=6"
1073 },
1074 "funding": {
1075 "url": "https://github.com/sponsors/sindresorhus"
1076 }
1077 },
1078 "node_modules/inflight": {
1079 "version": "1.0.6",
1080 "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
1081 "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
1082 "dev": true,
1083 "dependencies": {
1084 "once": "^1.3.0",
1085 "wrappy": "1"
1086 }
1087 },
1088 "node_modules/inherits": {
1089 "version": "2.0.4",
1090 "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1091 "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1092 "dev": true
1093 },
1094 "node_modules/is-binary-path": {
1095 "version": "2.1.0",
1096 "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1097 "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1098 "dev": true,
1099 "dependencies": {
1100 "binary-extensions": "^2.0.0"
1101 },
1102 "engines": {
1103 "node": ">=8"
1104 }
1105 },
1106 "node_modules/is-extglob": {
1107 "version": "2.1.1",
1108 "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1109 "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1110 "dev": true,
1111 "engines": {
1112 "node": ">=0.10.0"
1113 }
1114 },
1115 "node_modules/is-glob": {
1116 "version": "4.0.3",
1117 "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1118 "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1119 "dev": true,
1120 "dependencies": {
1121 "is-extglob": "^2.1.1"
1122 },
1123 "engines": {
1124 "node": ">=0.10.0"
1125 }
1126 },
1127 "node_modules/is-number": {
1128 "version": "7.0.0",
1129 "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1130 "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1131 "dev": true,
1132 "engines": {
1133 "node": ">=0.12.0"
1134 }
1135 },
1136 "node_modules/is-reference": {
1137 "version": "3.0.2",
1138 "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
1139 "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
1140 "dependencies": {
1141 "@types/estree": "*"
1142 }
1143 },
1144 "node_modules/kleur": {
1145 "version": "4.1.5",
1146 "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
1147 "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
1148 "dev": true,
1149 "engines": {
1150 "node": ">=6"
1151 }
1152 },
1153 "node_modules/locate-character": {
1154 "version": "3.0.0",
1155 "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
1156 "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
1157 },
1158 "node_modules/magic-string": {
1159 "version": "0.30.7",
1160 "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
1161 "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
1162 "dependencies": {
1163 "@jridgewell/sourcemap-codec": "^1.4.15"
1164 },
1165 "engines": {
1166 "node": ">=12"
1167 }
1168 },
1169 "node_modules/mdn-data": {
1170 "version": "2.0.30",
1171 "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
1172 "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
1173 },
1174 "node_modules/merge2": {
1175 "version": "1.4.1",
1176 "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
1177 "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
1178 "dev": true,
1179 "engines": {
1180 "node": ">= 8"
1181 }
1182 },
1183 "node_modules/micromatch": {
1184 "version": "4.0.5",
1185 "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
1186 "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
1187 "dev": true,
1188 "dependencies": {
1189 "braces": "^3.0.2",
1190 "picomatch": "^2.3.1"
1191 },
1192 "engines": {
1193 "node": ">=8.6"
1194 }
1195 },
1196 "node_modules/min-indent": {
1197 "version": "1.0.1",
1198 "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
1199 "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
1200 "dev": true,
1201 "engines": {
1202 "node": ">=4"
1203 }
1204 },
1205 "node_modules/minimatch": {
1206 "version": "3.1.2",
1207 "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1208 "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1209 "dev": true,
1210 "dependencies": {
1211 "brace-expansion": "^1.1.7"
1212 },
1213 "engines": {
1214 "node": "*"
1215 }
1216 },
1217 "node_modules/minimist": {
1218 "version": "1.2.8",
1219 "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
1220 "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
1221 "dev": true,
1222 "funding": {
1223 "url": "https://github.com/sponsors/ljharb"
1224 }
1225 },
1226 "node_modules/mkdirp": {
1227 "version": "0.5.6",
1228 "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
1229 "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
1230 "dev": true,
1231 "dependencies": {
1232 "minimist": "^1.2.6"
1233 },
1234 "bin": {
1235 "mkdirp": "bin/cmd.js"
1236 }
1237 },
1238 "node_modules/mri": {
1239 "version": "1.2.0",
1240 "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
1241 "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
1242 "dev": true,
1243 "engines": {
1244 "node": ">=4"
1245 }
1246 },
1247 "node_modules/ms": {
1248 "version": "2.1.2",
1249 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1250 "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
1251 "dev": true
1252 },
1253 "node_modules/nanoid": {
1254 "version": "3.3.7",
1255 "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
1256 "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
1257 "dev": true,
1258 "funding": [
1259 {
1260 "type": "github",
1261 "url": "https://github.com/sponsors/ai"
1262 }
1263 ],
1264 "bin": {
1265 "nanoid": "bin/nanoid.cjs"
1266 },
1267 "engines": {
1268 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1269 }
1270 },
1271 "node_modules/normalize-path": {
1272 "version": "3.0.0",
1273 "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1274 "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1275 "dev": true,
1276 "engines": {
1277 "node": ">=0.10.0"
1278 }
1279 },
1280 "node_modules/once": {
1281 "version": "1.4.0",
1282 "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1283 "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1284 "dev": true,
1285 "dependencies": {
1286 "wrappy": "1"
1287 }
1288 },
1289 "node_modules/parent-module": {
1290 "version": "1.0.1",
1291 "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
1292 "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
1293 "dev": true,
1294 "dependencies": {
1295 "callsites": "^3.0.0"
1296 },
1297 "engines": {
1298 "node": ">=6"
1299 }
1300 },
1301 "node_modules/path-is-absolute": {
1302 "version": "1.0.1",
1303 "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
1304 "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
1305 "dev": true,
1306 "engines": {
1307 "node": ">=0.10.0"
1308 }
1309 },
1310 "node_modules/periscopic": {
1311 "version": "3.1.0",
1312 "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
1313 "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
1314 "dependencies": {
1315 "@types/estree": "^1.0.0",
1316 "estree-walker": "^3.0.0",
1317 "is-reference": "^3.0.0"
1318 }
1319 },
1320 "node_modules/picocolors": {
1321 "version": "1.0.0",
1322 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
1323 "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
1324 "dev": true
1325 },
1326 "node_modules/picomatch": {
1327 "version": "2.3.1",
1328 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1329 "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1330 "dev": true,
1331 "engines": {
1332 "node": ">=8.6"
1333 },
1334 "funding": {
1335 "url": "https://github.com/sponsors/jonschlinkert"
1336 }
1337 },
1338 "node_modules/postcss": {
1339 "version": "8.4.35",
1340 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
1341 "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
1342 "dev": true,
1343 "funding": [
1344 {
1345 "type": "opencollective",
1346 "url": "https://opencollective.com/postcss/"
1347 },
1348 {
1349 "type": "tidelift",
1350 "url": "https://tidelift.com/funding/github/npm/postcss"
1351 },
1352 {
1353 "type": "github",
1354 "url": "https://github.com/sponsors/ai"
1355 }
1356 ],
1357 "dependencies": {
1358 "nanoid": "^3.3.7",
1359 "picocolors": "^1.0.0",
1360 "source-map-js": "^1.0.2"
1361 },
1362 "engines": {
1363 "node": "^10 || ^12 || >=14"
1364 }
1365 },
1366 "node_modules/queue-microtask": {
1367 "version": "1.2.3",
1368 "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
1369 "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
1370 "dev": true,
1371 "funding": [
1372 {
1373 "type": "github",
1374 "url": "https://github.com/sponsors/feross"
1375 },
1376 {
1377 "type": "patreon",
1378 "url": "https://www.patreon.com/feross"
1379 },
1380 {
1381 "type": "consulting",
1382 "url": "https://feross.org/support"
1383 }
1384 ]
1385 },
1386 "node_modules/readdirp": {
1387 "version": "3.6.0",
1388 "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1389 "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1390 "dev": true,
1391 "dependencies": {
1392 "picomatch": "^2.2.1"
1393 },
1394 "engines": {
1395 "node": ">=8.10.0"
1396 }
1397 },
1398 "node_modules/resolve-from": {
1399 "version": "4.0.0",
1400 "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
1401 "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
1402 "dev": true,
1403 "engines": {
1404 "node": ">=4"
1405 }
1406 },
1407 "node_modules/reusify": {
1408 "version": "1.0.4",
1409 "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
1410 "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
1411 "dev": true,
1412 "engines": {
1413 "iojs": ">=1.0.0",
1414 "node": ">=0.10.0"
1415 }
1416 },
1417 "node_modules/rimraf": {
1418 "version": "2.7.1",
1419 "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
1420 "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
1421 "dev": true,
1422 "dependencies": {
1423 "glob": "^7.1.3"
1424 },
1425 "bin": {
1426 "rimraf": "bin.js"
1427 }
1428 },
1429 "node_modules/rollup": {
1430 "version": "4.12.0",
1431 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz",
1432 "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==",
1433 "dev": true,
1434 "dependencies": {
1435 "@types/estree": "1.0.5"
1436 },
1437 "bin": {
1438 "rollup": "dist/bin/rollup"
1439 },
1440 "engines": {
1441 "node": ">=18.0.0",
1442 "npm": ">=8.0.0"
1443 },
1444 "optionalDependencies": {
1445 "@rollup/rollup-android-arm-eabi": "4.12.0",
1446 "@rollup/rollup-android-arm64": "4.12.0",
1447 "@rollup/rollup-darwin-arm64": "4.12.0",
1448 "@rollup/rollup-darwin-x64": "4.12.0",
1449 "@rollup/rollup-linux-arm-gnueabihf": "4.12.0",
1450 "@rollup/rollup-linux-arm64-gnu": "4.12.0",
1451 "@rollup/rollup-linux-arm64-musl": "4.12.0",
1452 "@rollup/rollup-linux-riscv64-gnu": "4.12.0",
1453 "@rollup/rollup-linux-x64-gnu": "4.12.0",
1454 "@rollup/rollup-linux-x64-musl": "4.12.0",
1455 "@rollup/rollup-win32-arm64-msvc": "4.12.0",
1456 "@rollup/rollup-win32-ia32-msvc": "4.12.0",
1457 "@rollup/rollup-win32-x64-msvc": "4.12.0",
1458 "fsevents": "~2.3.2"
1459 }
1460 },
1461 "node_modules/run-parallel": {
1462 "version": "1.2.0",
1463 "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
1464 "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
1465 "dev": true,
1466 "funding": [
1467 {
1468 "type": "github",
1469 "url": "https://github.com/sponsors/feross"
1470 },
1471 {
1472 "type": "patreon",
1473 "url": "https://www.patreon.com/feross"
1474 },
1475 {
1476 "type": "consulting",
1477 "url": "https://feross.org/support"
1478 }
1479 ],
1480 "dependencies": {
1481 "queue-microtask": "^1.2.2"
1482 }
1483 },
1484 "node_modules/sade": {
1485 "version": "1.8.1",
1486 "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
1487 "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
1488 "dev": true,
1489 "dependencies": {
1490 "mri": "^1.1.0"
1491 },
1492 "engines": {
1493 "node": ">=6"
1494 }
1495 },
1496 "node_modules/sander": {
1497 "version": "0.5.1",
1498 "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
1499 "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==",
1500 "dev": true,
1501 "dependencies": {
1502 "es6-promise": "^3.1.2",
1503 "graceful-fs": "^4.1.3",
1504 "mkdirp": "^0.5.1",
1505 "rimraf": "^2.5.2"
1506 }
1507 },
1508 "node_modules/sorcery": {
1509 "version": "0.11.0",
1510 "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
1511 "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==",
1512 "dev": true,
1513 "dependencies": {
1514 "@jridgewell/sourcemap-codec": "^1.4.14",
1515 "buffer-crc32": "^0.2.5",
1516 "minimist": "^1.2.0",
1517 "sander": "^0.5.0"
1518 },
1519 "bin": {
1520 "sorcery": "bin/sorcery"
1521 }
1522 },
1523 "node_modules/source-map-js": {
1524 "version": "1.0.2",
1525 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
1526 "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
1527 "engines": {
1528 "node": ">=0.10.0"
1529 }
1530 },
1531 "node_modules/strip-indent": {
1532 "version": "3.0.0",
1533 "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
1534 "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
1535 "dev": true,
1536 "dependencies": {
1537 "min-indent": "^1.0.0"
1538 },
1539 "engines": {
1540 "node": ">=8"
1541 }
1542 },
1543 "node_modules/svelte": {
1544 "version": "4.2.11",
1545 "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.11.tgz",
1546 "integrity": "sha512-YIQk3J4X89wOLhjsqIW8tqY3JHPuBdtdOIkASP2PZeAMcSW9RsIjQzMesCrxOF3gdWYC0mKknlKF7OqmLM+Zqg==",
1547 "dependencies": {
1548 "@ampproject/remapping": "^2.2.1",
1549 "@jridgewell/sourcemap-codec": "^1.4.15",
1550 "@jridgewell/trace-mapping": "^0.3.18",
1551 "@types/estree": "^1.0.1",
1552 "acorn": "^8.9.0",
1553 "aria-query": "^5.3.0",
1554 "axobject-query": "^4.0.0",
1555 "code-red": "^1.0.3",
1556 "css-tree": "^2.3.1",
1557 "estree-walker": "^3.0.3",
1558 "is-reference": "^3.0.1",
1559 "locate-character": "^3.0.0",
1560 "magic-string": "^0.30.4",
1561 "periscopic": "^3.1.0"
1562 },
1563 "engines": {
1564 "node": ">=16"
1565 }
1566 },
1567 "node_modules/svelte-chartjs": {
1568 "version": "3.1.5",
1569 "resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.5.tgz",
1570 "integrity": "sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA==",
1571 "peerDependencies": {
1572 "chart.js": "^3.5.0 || ^4.0.0",
1573 "svelte": "^4.0.0"
1574 }
1575 },
1576 "node_modules/svelte-check": {
1577 "version": "3.6.4",
1578 "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.4.tgz",
1579 "integrity": "sha512-mY/dqucqm46p72M8yZmn81WPZx9mN6uuw8UVfR3ZKQeLxQg5HDGO3HHm5AZuWZPYNMLJ+TRMn+TeN53HfQ/vsw==",
1580 "dev": true,
1581 "dependencies": {
1582 "@jridgewell/trace-mapping": "^0.3.17",
1583 "chokidar": "^3.4.1",
1584 "fast-glob": "^3.2.7",
1585 "import-fresh": "^3.2.1",
1586 "picocolors": "^1.0.0",
1587 "sade": "^1.7.4",
1588 "svelte-preprocess": "^5.1.0",
1589 "typescript": "^5.0.3"
1590 },
1591 "bin": {
1592 "svelte-check": "bin/svelte-check"
1593 },
1594 "peerDependencies": {
1595 "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"
1596 }
1597 },
1598 "node_modules/svelte-hmr": {
1599 "version": "0.15.3",
1600 "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
1601 "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==",
1602 "dev": true,
1603 "engines": {
1604 "node": "^12.20 || ^14.13.1 || >= 16"
1605 },
1606 "peerDependencies": {
1607 "svelte": "^3.19.0 || ^4.0.0"
1608 }
1609 },
1610 "node_modules/svelte-preprocess": {
1611 "version": "5.1.3",
1612 "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz",
1613 "integrity": "sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==",
1614 "dev": true,
1615 "hasInstallScript": true,
1616 "dependencies": {
1617 "@types/pug": "^2.0.6",
1618 "detect-indent": "^6.1.0",
1619 "magic-string": "^0.30.5",
1620 "sorcery": "^0.11.0",
1621 "strip-indent": "^3.0.0"
1622 },
1623 "engines": {
1624 "node": ">= 16.0.0",
1625 "pnpm": "^8.0.0"
1626 },
1627 "peerDependencies": {
1628 "@babel/core": "^7.10.2",
1629 "coffeescript": "^2.5.1",
1630 "less": "^3.11.3 || ^4.0.0",
1631 "postcss": "^7 || ^8",
1632 "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
1633 "pug": "^3.0.0",
1634 "sass": "^1.26.8",
1635 "stylus": "^0.55.0",
1636 "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0",
1637 "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0",
1638 "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0"
1639 },
1640 "peerDependenciesMeta": {
1641 "@babel/core": {
1642 "optional": true
1643 },
1644 "coffeescript": {
1645 "optional": true
1646 },
1647 "less": {
1648 "optional": true
1649 },
1650 "postcss": {
1651 "optional": true
1652 },
1653 "postcss-load-config": {
1654 "optional": true
1655 },
1656 "pug": {
1657 "optional": true
1658 },
1659 "sass": {
1660 "optional": true
1661 },
1662 "stylus": {
1663 "optional": true
1664 },
1665 "sugarss": {
1666 "optional": true
1667 },
1668 "typescript": {
1669 "optional": true
1670 }
1671 }
1672 },
1673 "node_modules/to-regex-range": {
1674 "version": "5.0.1",
1675 "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1676 "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1677 "dev": true,
1678 "dependencies": {
1679 "is-number": "^7.0.0"
1680 },
1681 "engines": {
1682 "node": ">=8.0"
1683 }
1684 },
1685 "node_modules/tslib": {
1686 "version": "2.6.2",
1687 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
1688 "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
1689 "dev": true
1690 },
1691 "node_modules/typescript": {
1692 "version": "5.3.3",
1693 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
1694 "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
1695 "dev": true,
1696 "bin": {
1697 "tsc": "bin/tsc",
1698 "tsserver": "bin/tsserver"
1699 },
1700 "engines": {
1701 "node": ">=14.17"
1702 }
1703 },
1704 "node_modules/vite": {
1705 "version": "5.1.3",
1706 "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
1707 "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
1708 "dev": true,
1709 "dependencies": {
1710 "esbuild": "^0.19.3",
1711 "postcss": "^8.4.35",
1712 "rollup": "^4.2.0"
1713 },
1714 "bin": {
1715 "vite": "bin/vite.js"
1716 },
1717 "engines": {
1718 "node": "^18.0.0 || >=20.0.0"
1719 },
1720 "funding": {
1721 "url": "https://github.com/vitejs/vite?sponsor=1"
1722 },
1723 "optionalDependencies": {
1724 "fsevents": "~2.3.3"
1725 },
1726 "peerDependencies": {
1727 "@types/node": "^18.0.0 || >=20.0.0",
1728 "less": "*",
1729 "lightningcss": "^1.21.0",
1730 "sass": "*",
1731 "stylus": "*",
1732 "sugarss": "*",
1733 "terser": "^5.4.0"
1734 },
1735 "peerDependenciesMeta": {
1736 "@types/node": {
1737 "optional": true
1738 },
1739 "less": {
1740 "optional": true
1741 },
1742 "lightningcss": {
1743 "optional": true
1744 },
1745 "sass": {
1746 "optional": true
1747 },
1748 "stylus": {
1749 "optional": true
1750 },
1751 "sugarss": {
1752 "optional": true
1753 },
1754 "terser": {
1755 "optional": true
1756 }
1757 }
1758 },
1759 "node_modules/vitefu": {
1760 "version": "0.2.5",
1761 "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
1762 "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",
1763 "dev": true,
1764 "peerDependencies": {
1765 "vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
1766 },
1767 "peerDependenciesMeta": {
1768 "vite": {
1769 "optional": true
1770 }
1771 }
1772 },
1773 "node_modules/wrappy": {
1774 "version": "1.0.2",
1775 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1776 "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
1777 "dev": true
1778 }
1779 }
1780}
diff --git a/fe/package.json b/fe/package.json
new file mode 100644
index 0000000..9c75965
--- /dev/null
+++ b/fe/package.json
@@ -0,0 +1,25 @@
1{
2 "name": "fe",
3 "private": true,
4 "version": "0.0.0",
5 "type": "module",
6 "scripts": {
7 "dev": "vite",
8 "build": "vite build",
9 "preview": "vite preview",
10 "check": "svelte-check --tsconfig ./tsconfig.json"
11 },
12 "devDependencies": {
13 "@sveltejs/vite-plugin-svelte": "^3.0.2",
14 "@tsconfig/svelte": "^5.0.2",
15 "svelte": "^4.2.10",
16 "svelte-check": "^3.6.3",
17 "tslib": "^2.6.2",
18 "typescript": "^5.2.2",
19 "vite": "^5.1.0"
20 },
21 "dependencies": {
22 "chart.js": "^4.4.2",
23 "svelte-chartjs": "^3.1.5"
24 }
25}
diff --git a/fe/public/vite.svg b/fe/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/fe/public/vite.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> \ No newline at end of file
diff --git a/fe/src/App.svelte b/fe/src/App.svelte
new file mode 100644
index 0000000..25d53dc
--- /dev/null
+++ b/fe/src/App.svelte
@@ -0,0 +1,16 @@
1<script lang="ts">
2 import Layout from "./lib/Layout.svelte";
3 import LoginForm from "./lib/LoginForm.svelte";
4 import DataView from "./lib/DataView.svelte";
5 import { authenticated } from "./stores/auth";
6</script>
7
8<main>
9 <Layout>
10 {#if !$authenticated}
11 <LoginForm />
12 {:else}
13 <DataView />
14 {/if}
15 </Layout>
16</main>
diff --git a/fe/src/app.css b/fe/src/app.css
new file mode 100644
index 0000000..c24c713
--- /dev/null
+++ b/fe/src/app.css
@@ -0,0 +1,120 @@
1:root {
2 font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 line-height: 1.5;
4 font-weight: 400;
5
6 color-scheme: light dark;
7 color: rgba(255, 255, 255, 0.87);
8 background-color: #242424;
9
10 font-synthesis: none;
11 text-rendering: optimizeLegibility;
12 -webkit-font-smoothing: antialiased;
13 -moz-osx-font-smoothing: grayscale;
14
15 --submit: #28a745;
16}
17
18a {
19 font-weight: 500;
20 color: #646cff;
21 text-decoration: inherit;
22}
23
24a:hover {
25 color: #535bf2;
26}
27
28body {
29 margin: 0;
30 display: flex;
31 place-items: center;
32 min-width: 320px;
33 min-height: 100vh;
34}
35
36h1 {
37 font-size: 3.2em;
38 line-height: 1.1;
39}
40
41.card {
42 padding: 2em;
43}
44
45#app {
46 flex-grow: 2;
47 max-width: 1280px;
48 margin: 0 auto;
49}
50
51button {
52 border-radius: 8px;
53 border: 1px solid transparent;
54 padding: 0.6em 1.2em;
55 font-size: 1em;
56 font-weight: 500;
57 font-family: inherit;
58 background-color: #1a1a1a;
59 cursor: pointer;
60 transition: border-color 0.25s;
61}
62
63button:hover {
64 border-color: #646cff;
65}
66
67button:focus,
68button:focus-visible {
69 outline: 4px auto -webkit-focus-ring-color;
70}
71
72@media (prefers-color-scheme: light) {
73 :root {
74 color: #213547;
75 background-color: #ffffff;
76 }
77
78 a:hover {
79 color: #747bff;
80 }
81
82 button {
83 background-color: #f9f9f9;
84 }
85}
86
87@media (prefers-color-scheme: dark) {
88 :root {
89 color: #000;
90 }
91}
92
93.form {
94 display: flex;
95 flex-direction: column;
96}
97
98.form.input.group {
99 display: flex;
100 flex-direction: column;
101 margin-bottom: 1em;
102}
103
104.form.input.group label {
105 margin-bottom: .5em;
106}
107
108.form.input.group input {
109 padding: 1em;
110}
111
112.form.input.group input[type=color] {
113 padding: 0;
114}
115
116.form button[type=submit] {
117 align-self: flex-end;
118 background: var(--submit);
119 color: #fff;
120}
diff --git a/fe/src/assets/svelte.svg b/fe/src/assets/svelte.svg
new file mode 100644
index 0000000..c5e0848
--- /dev/null
+++ b/fe/src/assets/svelte.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg> \ No newline at end of file
diff --git a/fe/src/errors.ts b/fe/src/errors.ts
new file mode 100644
index 0000000..81f7145
--- /dev/null
+++ b/fe/src/errors.ts
@@ -0,0 +1,5 @@
1export class UnauthorizedError extends Error {
2 constructor(message?: string) {
3 super(message);
4 }
5}
diff --git a/fe/src/http.ts b/fe/src/http.ts
new file mode 100644
index 0000000..3b2a4f0
--- /dev/null
+++ b/fe/src/http.ts
@@ -0,0 +1,92 @@
1let instance;
2const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? "http://localhost:8080/api/v1";
3
4class HttpClient {
5 baseURL: string;
6 commonHeaders: Headers;
7
8 constructor(baseURL: string) {
9 this.baseURL = baseURL;
10 this.commonHeaders = new Headers({
11 "Content-Type": "application/json"
12 })
13 if (instance) {
14 throw new Error("New instance cannot be created!");
15 }
16
17 instance = this;
18 }
19
20 private getURL(endpoint: string): URL {
21 return new URL(endpoint, this.baseURL);
22 }
23
24 private token(): string | null {
25 return localStorage.getItem('token');
26 }
27
28 private async makeRequest(request: Request): Promise<Response> {
29 return fetch(request)
30 }
31
32 async get({ endpoint, headers }: IHttpParameters): Promise<Response> {
33 const url: URL = this.getURL(endpoint);
34 headers = Object.assign<Headers, Headers>(headers, this.commonHeaders);
35 const request: Request = new Request(url, {
36 method: "GET",
37 headers
38 });
39
40 return this.makeRequest(request);
41 }
42
43 async post({ endpoint, authenticated, body, headers }: IHttpParameters): Promise<Response> {
44 const url = this.getURL(endpoint);
45
46 if (authenticated) {
47 const token: string | null = this.token();
48 headers.append('Authorization', `Bearer ${token}`);
49 }
50
51 const request: Request = new Request(url, {
52 method: "POST",
53 body: JSON.stringify(body),
54 headers
55 })
56
57 return this.makeRequest(request);
58 }
59
60 async patch({ endpoint, authenticated, headers }: IHttpParameters): Promise<Response> {
61 const url = this.getURL(endpoint);
62 if (authenticated) {
63
64 }
65 const response: Response = await fetch(url, {
66 method: "PATCH",
67 headers
68 });
69 }
70
71 async delete({ endpoint, authenticated, headers }: IHttpParameters): Promise<Response> {
72 const url = this.getURL(endpoint);
73 if (authenticated) {
74
75 }
76 const response: Response = await fetch(url, {
77 method: "DELETE",
78 headers
79 })
80 }
81}
82
83interface IHttpParameters {
84 endpoint: string;
85 body: Record<string, any>;
86 authenticated: boolean;
87 headers: Headers;
88}
89
90let http: Readonly<HttpClient> = Object.freeze(new HttpClient(baseUrl));
91
92export default http;
diff --git a/fe/src/lib/Card.svelte b/fe/src/lib/Card.svelte
new file mode 100644
index 0000000..cd1e02c
--- /dev/null
+++ b/fe/src/lib/Card.svelte
@@ -0,0 +1,23 @@
1<script lang="ts">
2 export let title = "";
3</script>
4
5<div class="card">
6 {#if title}
7 <h2>{title}</h2>
8 {/if}
9 <slot />
10</div>
11
12<style>
13 .card {
14 background: #fff;
15 display: flex;
16 flex-direction: column;
17 align-items: flex-start;
18 border: solid 2px #00000066;
19 border-radius: 0.25em;
20 height: var(--height, fit-content);
21 overflow-y: var(--overflow, initial);
22 }
23</style>
diff --git a/fe/src/lib/Chart.svelte b/fe/src/lib/Chart.svelte
new file mode 100644
index 0000000..b19d932
--- /dev/null
+++ b/fe/src/lib/Chart.svelte
@@ -0,0 +1,63 @@
1<script lang="ts">
2import { onDestroy } from "svelte";
3import ChartJS from "chart.js/auto";
4
5export let data;
6export let labels;
7export let type = 'bar';
8
9let ref: HTMLCanvasElement;
10let chart
11
12function setupChart(result) {
13 [labels, data] = result;
14 chart = new ChartJS(ref, {
15 type,
16 data: {
17 labels,
18 datasets: [
19 {
20 label: "Totals",
21 data,
22 backgroundColor: "rgba(255, 192, 192, 0.2)"
23 }
24 ]
25 },
26 options: {
27 responsive: true,
28 maintainAspectRatio: false,
29 scales: {
30 y: {
31 suggestedMax: 30,
32 beginAtZero: true,
33 ticks: {
34 autoSkip: true,
35 stepSize: 5
36 }
37 }
38 },
39 plugins: {
40 legend: {
41 display: false
42 },
43 title: {
44 display: true,
45 text: "Weekly Breakdown"
46 },
47 subtitle: {
48 display: true,
49 text: "Water consumption over the last week",
50 padding: {bottom: 10}
51 }
52 }
53 }
54 });
55
56 onDestroy(() => {
57 if (chart) chart.destroy();
58 chart = null;
59 })
60}
61</script>
62
63<canvas bind:this={ref} /> \ No newline at end of file
diff --git a/fe/src/lib/Column.svelte b/fe/src/lib/Column.svelte
new file mode 100644
index 0000000..f036073
--- /dev/null
+++ b/fe/src/lib/Column.svelte
@@ -0,0 +1,13 @@
1<div class="column">
2 <slot />
3</div>
4
5<style>
6 .column {
7 display: flex;
8 flex-direction: column;
9 height: 100%;
10 gap: var(--gap, 32px);
11 width: var(--width, initial);
12 }
13</style> \ No newline at end of file
diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte
new file mode 100644
index 0000000..ffc2fe8
--- /dev/null
+++ b/fe/src/lib/DataView.svelte
@@ -0,0 +1,228 @@
1<script lang="ts">
2 import { onDestroy, onMount } from "svelte";
3 import http from "../http";
4 import { token } from "../stores/auth";
5 import { addFormOpen } from "../stores/forms";
6 import Table from "./Table.svelte";
7 import ChartJS from "chart.js/auto";
8 import Chart from './Chart.svelte'
9 import Card from "./Card.svelte";
10 import Column from "./Column.svelte";
11 import AddForm from "./forms/AddForm.svelte";
12 import { apiURL } from "../utils";
13
14 let json: Promise<any>;
15
16 let barCanvasRef: HTMLCanvasElement;
17 let lineCanvasRef: HTMLCanvasElement;
18 let barChart: any;
19 let lineChart: any;
20
21 let lastSevenDays: string[];
22 let lastSevenDaysData: number[];
23
24 let userTotalsLabels: string[];
25 let userTotalsData: number[];
26
27 async function fetchData() {
28 const res = await fetch(apiURL("stats"), {
29 method: "GET",
30 headers: {
31 Authorization: `Bearer ${$token}`
32 }
33 });
34 if (res.ok) {
35 json = res.json();
36 } else {
37 throw new Error("There was a problem with your request");
38 }
39 }
40
41 async function fetchDailyUserStatistics() {
42 const res = await fetch(apiURL("stats/daily"), {
43 method: "GET",
44 headers: {
45 Authorization: `Bearer ${$token}`
46 }
47 });
48
49 if (res.ok) {
50 const json = await res.json();
51 let labels = json.map((d: any) => d.name);
52 let data = json.map((d: any) => d.total);
53 return [labels, data];
54 } else {
55 throw new Error("There was a problem with your request");
56 }
57
58 }
59
60 async function fetchWeeklyTotals() {
61 const res = await fetch(apiURL("stats/weekly"), {
62 method: "GET",
63 headers: {
64 Authorization: `Bearer ${$token}`
65 }
66 });
67
68 if (res.ok) {
69 const json = await res.json();
70 let labels = json.map((d: any) => d.date);
71 let data = json.map((d: any) => d.total);
72 return [labels, data];
73 } else {
74 throw new Error("There was a problem with your request");
75 }
76 }
77
78 function closeDialog() {
79 addFormOpen.set(false);
80 }
81
82 function onStatisticAdd() {
83 closeDialog();
84 fetchData();
85 fetchWeeklyTotals().then(updateWeeklyTotalsChart).catch(err => console.error(err));
86 fetchDailyUserStatistics().then(updateDailyUserTotalsChart).catch(err => console.error(err));
87 }
88
89 function setupWeeklyTotalsChart(result: any) {
90 [lastSevenDays, lastSevenDaysData] = result;
91 lineChart = new ChartJS(lineCanvasRef, {
92 type: "line",
93 data: {
94 labels: lastSevenDays,
95 datasets: [
96 {
97 label: "Totals",
98 data: lastSevenDaysData,
99 backgroundColor: "rgba(255, 192, 192, 0.2)"
100 }
101 ]
102 },
103 options: {
104 responsive: true,
105 maintainAspectRatio: false,
106 scales: {
107 y: {
108 suggestedMax: 30,
109 beginAtZero: true,
110 ticks: {
111 autoSkip: true,
112 stepSize: 5
113 }
114 }
115 },
116 plugins: {
117 legend: {
118 display: false
119 },
120 title: {
121 display: true,
122 text: "Weekly Breakdown"
123 },
124 subtitle: {
125 display: true,
126 text: "Water consumption over the last week",
127 padding: {bottom: 10}
128 }
129 }
130 }
131 });
132 }
133
134 function setupDailyUserTotalsChart(result: any) {
135 [userTotalsLabels, userTotalsData] = result;
136
137 barChart = new ChartJS(barCanvasRef, {
138 type: "bar",
139 data: {
140 labels: userTotalsLabels,
141 datasets: [
142 {
143 data: userTotalsData,
144 backgroundColor: [
145 "#330000",
146 "rgba(100, 200, 192, 0.2)"
147 ]
148 }
149 ]
150 },
151 options: {
152 responsive: true,
153 maintainAspectRatio: false,
154 scales: {
155 y: {
156 beginAtZero: true,
157 suggestedMax: 10,
158 ticks: {
159 autoSkip: true,
160 stepSize: 1
161 }
162 }
163 },
164 plugins: {
165 legend: {
166 display: false
167 },
168 title: {
169 display: true,
170 text: "Daily Total"
171 },
172 subtitle: {
173 display: true,
174 text: "Water Drank Today",
175 padding: {bottom: 10}
176 }
177 }
178 }
179 });
180 }
181
182 function updateWeeklyTotalsChart(result: any) {
183 [, lastSevenDaysData] = result;
184 lineChart.data.datasets[0].data = lastSevenDaysData;
185 lineChart.update();
186 }
187
188 function updateDailyUserTotalsChart(result: any) {
189 [, userTotalsData] = result;
190 barChart.data.datasets[0].data = userTotalsData;
191 barChart.update();
192 }
193
194 onMount(() => {
195 fetchData();
196 fetchWeeklyTotals().then(setupWeeklyTotalsChart);
197 fetchDailyUserStatistics().then(setupDailyUserTotalsChart);
198 });
199
200 onDestroy(() => {
201 if (barChart) barChart.destroy();
202 if (lineChart) lineChart.destroy();
203 barChart = null;
204 lineChart = null;
205 });
206</script>
207
208<Column --width="500px">
209 <Card --height="300px">
210 <!--<Chart />-->
211 <canvas bind:this={barCanvasRef} />
212 </Card>
213 <Card --height="300px">
214 <canvas bind:this={lineCanvasRef} />
215 </Card>
216</Column>
217
218<AddForm open={$addFormOpen} on:submit={onStatisticAdd} on:close={closeDialog} />
219<Column>
220 <Card>
221 {#await json then data}
222 <Table {data} nofooter />
223 {:catch error}
224 <p>{error}</p>
225 {/await}
226 </Card>
227</Column>
228<!-- <Chart /> -->
diff --git a/fe/src/lib/Layout.svelte b/fe/src/lib/Layout.svelte
new file mode 100644
index 0000000..2728dd3
--- /dev/null
+++ b/fe/src/lib/Layout.svelte
@@ -0,0 +1,74 @@
1<script>
2 import { authenticated, token, user, preferences } from "../stores/auth";
3 import PreferencesForm from "./PreferencesForm.svelte";
4 import { addFormOpen } from "../stores/forms";
5
6 const logout = () => {
7 preferences.reset();
8 user.reset();
9 token.unauthenticate();
10 }
11 let preferenceFormOpen = false;
12
13 function showPreferencesDialog() {
14 preferenceFormOpen = true;
15 }
16
17 function closePreferenceDialog() {
18 preferenceFormOpen = false;
19 }
20
21 function showAddDialog() {
22 addFormOpen.set(true);
23 }
24</script>
25
26<div class="layout">
27 {#if $authenticated}
28 <nav>
29 <div>
30 <h1>Water</h1>
31 </div>
32 <div>
33 <button on:click={showAddDialog}>Log Water</button>
34 <button on:click={showPreferencesDialog}>Preference</button>
35 <button on:click={logout}>Logout</button>
36 </div>
37 </nav>
38 <PreferencesForm open={preferenceFormOpen} on:close={closePreferenceDialog} />
39 {/if}
40 <div id="content">
41 <slot />
42 </div>
43</div>
44
45<style>
46 .layout {
47 height: 100vh;
48 }
49
50 nav {
51 display: flex;
52 flex-direction: row;
53 align-items: center;
54 justify-content: space-between;
55 height: 64px;
56 padding: 0 2em;
57 }
58
59 nav div {
60 width: fit-content;
61 }
62
63 nav div h1 {
64 font-size: 1.75em;
65 }
66
67 #content {
68 display: flex;
69 flex-direction: row;
70 justify-content: center;
71 gap: 2em;
72 margin-top: 4em;
73 }
74</style>
diff --git a/fe/src/lib/LoginForm.svelte b/fe/src/lib/LoginForm.svelte
new file mode 100644
index 0000000..cf5febf
--- /dev/null
+++ b/fe/src/lib/LoginForm.svelte
@@ -0,0 +1,86 @@
1<script lang="ts">
2 import { token, user, preferences } from "../stores/auth";
3 import Card from "./Card.svelte";
4 import { apiURL } from "../utils";
5
6 let credentials: CredentialObject = {
7 username: "",
8 password: "",
9 };
10
11 let error: string | null = null;
12
13 interface CredentialObject {
14 username: string;
15 password: string;
16 }
17
18 function prepareCredentials({
19 username,
20 password,
21 }: CredentialObject): string {
22 return btoa(`${username}:${password}`);
23 }
24
25 async function onSubmit(e: Event) {
26 if (!credentials.username || !credentials.password) {
27 error = "please enter your username and password";
28 return;
29 }
30 const auth = prepareCredentials(credentials);
31
32 const response = await fetch(apiURL("auth"), {
33 method: "POST",
34 headers: {
35 Authorization: `Basic ${auth}`,
36 },
37 });
38
39 if (response.status === 401) {
40 error = "Your username or password is wrong";
41 return;
42 }
43
44 if (response.ok) {
45 const {
46 token: apiToken,
47 user: userData,
48 preferences: userPreferences,
49 } = await response.json();
50 user.setUser(userData);
51 preferences.setPreference(userPreferences);
52 token.authenticate(apiToken);
53 }
54
55 error = null;
56 }
57</script>
58
59<Card>
60 <form class="form" on:submit|preventDefault={onSubmit}>
61 <div class="form input group">
62 <label for="username">Username</label>
63 <input
64 bind:value={credentials.username}
65 id="username"
66 name="username"
67 type="text"
68 autocomplete="username"
69 />
70 </div>
71 <div class="form input group">
72 <label for="password">Password</label>
73 <input
74 bind:value={credentials.password}
75 id="password"
76 name="password"
77 type="password"
78 autocomplete="current-password"
79 />
80 </div>
81 {#if error}
82 <p class="error">{error}</p>
83 {/if}
84 <button type="submit">Log in</button>
85 </form>
86</Card>
diff --git a/fe/src/lib/PreferencesForm.svelte b/fe/src/lib/PreferencesForm.svelte
new file mode 100644
index 0000000..74b8a63
--- /dev/null
+++ b/fe/src/lib/PreferencesForm.svelte
@@ -0,0 +1,119 @@
1<script lang="ts">
2 import { user, preferences, token } from "../stores/auth";
3 import { createEventDispatcher, onDestroy, onMount } from "svelte";
4 import type { User } from "../types";
5 import { apiURL } from "../utils";
6
7 export let open: boolean;
8
9 let sizes: Array<any>;
10 let selectedSize: number = 1;
11 let color: string = "#000000";
12
13 const dispatch = createEventDispatcher();
14
15 const unsubscribe = preferences.subscribe(
16 (value: any) => {
17 if (value) {
18 color = value.color;
19 selectedSize = value.size_id;
20 }
21 },
22 );
23
24 function closeDialog() {
25 dispatch("close");
26 }
27
28 async function updateUserPreferences() {
29 const res = await fetch(apiURL("user/preferences"), {
30 method: "PATCH",
31 headers: {
32 Authorization: `Bearer ${$token}`,
33 },
34 body: JSON.stringify($preferences),
35 });
36 }
37
38 async function getUserPreferences() {
39 const res = await fetch(apiURL(`user/${($user as User)!.id}/preferences`),
40 {
41 method: "GET",
42 headers: {
43 Authorization: `Bearer ${$token}`,
44 },
45 },
46 );
47 const updatePreferences = await res.json();
48 preferences.set(updatePreferences);
49 }
50
51 async function onPreferencesSave(): Promise<void> {
52 preferences.update((value) => ({
53 ...value!,
54 size_id: selectedSize,
55 color: color,
56 }));
57
58 await updateUserPreferences();
59 await getUserPreferences();
60
61 dispatch("close");
62 }
63
64 onMount(() => {
65 fetch(apiURL("sizes"), {
66 method: "GET",
67 headers: {
68 Authorization: `Bearer ${$token}`,
69 },
70 })
71 .then((res) => res.json())
72 .then((val) => (sizes = val));
73 });
74
75 onDestroy(() => {
76 unsubscribe();
77 });
78</script>
79
80<dialog {open} on:submit|preventDefault={onPreferencesSave}>
81 <h2>User Preferences</h2>
82 <form method="dialog">
83 <div class="form input group">
84 <label for="color">Color</label>
85 <input
86 id="color"
87 name="color"
88 type="color"
89 bind:value={color}
90 />
91 </div>
92 <div class="form input group">
93 <label for="size">Bottle Size</label>
94 <select
95 bind:value={selectedSize}
96 >
97 {#if sizes}
98 {#each sizes as size}
99 <option value={size.id}>{size.size} {size.unit}</option>
100 {/each}
101 {/if}
102 </select>
103 </div>
104 <button on:click={closeDialog}>Cancel</button>
105 <button type="submit">Save</button>
106 </form>
107</dialog>
108
109<style>
110 dialog {
111 background: white;
112 color: black;
113 }
114
115 input[type="color"] {
116 width: 4em;
117 height: 4em;
118 }
119</style>
diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte
new file mode 100644
index 0000000..621157e
--- /dev/null
+++ b/fe/src/lib/Table.svelte
@@ -0,0 +1,114 @@
1<script lang="ts">
2 export let data: Array<any> | undefined = undefined;
3 export let nofooter: boolean = false;
4 export let noheader: boolean = false;
5 export let omit: string[] = ["id"];
6 export let title: string | undefined = undefined;
7
8 export let sortBy: string = 'date';
9
10 type SortComparator = (a, b) => number
11
12 function getDataKeys(data: any[]): string[] {
13 if (!data || data.length === 0) return [];
14 return Object.keys(data[0])
15 .map((k) => k.split("_").join(" "))
16 .filter((k) => !omit.includes(k));
17 }
18
19 function getRow(row: Record<string, any>): Array<any> {
20 return Object.entries(row).filter((r) => !omit.includes(r[0]));
21 }
22
23 function sort(arr: Array<Record<string, any>>, fn: SortComparator = (a , b) => new Date(b[sortBy]) - new Date(a[sortBy])) {
24 return arr.sort(fn)
25 }
26
27 const formatter = new Intl.DateTimeFormat("en", {
28 year: "numeric",
29 month: "numeric",
30 day: "numeric",
31 hour: "numeric",
32 minute: "2-digit",
33 second: "2-digit",
34 timeZone: "America/New_York"
35 });
36
37 function formatDatum([key, value]: any[]) {
38 if (key === "date") {
39 const parsedDate = new Date(value);
40 return formatter.format(parsedDate);
41 }
42
43 if (key === "user") {
44 return value["name"];
45 }
46
47 return value;
48 }
49</script>
50
51<table>
52 {#if title}
53 <h2>{title}</h2>
54 {/if}
55 {#if !noheader && data}
56 <thead>
57 <tr>
58 {#each getDataKeys(data) as header}
59 <th>{header}</th>
60 {/each}
61 </tr>
62 </thead>
63 {/if}
64 <tbody>
65 {#if data}
66 {#each sort(data) as row}
67 <tr>
68 {#each getRow(row) as datum}
69 <td>{formatDatum(datum)}</td>
70 {/each}
71 </tr>
72 {/each}
73 {:else}
74 <tr> There is not data.</tr>
75 {/if}
76 </tbody>
77 {#if !nofooter}
78 <slot name="footer">
79 <tfoot>
80 <tr>
81 <td>Table Footer</td>
82 </tr>
83 </tfoot>
84 </slot>
85 {/if}
86</table>
87
88<style>
89 table {
90 padding: 16px;
91 margin: 8px;
92 border: solid 1px black;
93 border-collapse: collapse;
94 overflow-y: hidden;
95 }
96
97 th {
98 text-transform: capitalize;
99 }
100
101 thead tr {
102 background: rgba(0, 0, 23, 0.34);
103 }
104
105 tbody tr:nth-child(odd) {
106 background: rgba(0, 0, 23, 0.14);
107 }
108
109 th,
110 td {
111 padding: 1em;
112 border: 1px solid rgba(0, 0, 0, 1);
113 }
114</style>
diff --git a/fe/src/lib/forms/AddForm.svelte b/fe/src/lib/forms/AddForm.svelte
new file mode 100644
index 0000000..f85cce6
--- /dev/null
+++ b/fe/src/lib/forms/AddForm.svelte
@@ -0,0 +1,78 @@
1<script lang='ts'>
2 import { createEventDispatcher } from "svelte";
3 import { token, user } from "../../stores/auth";
4 import type { Statistic } from "../../types";
5 import { apiURL } from "../../utils";
6
7 export let open: boolean;
8
9 const dispatch = createEventDispatcher();
10
11 const statistic: Statistic = newStatistic();
12
13 function newStatistic(): Statistic {
14 let now = new Date(),
15 month,
16 day,
17 year;
18
19 month = `${now.getMonth() + 1}`;
20 day = `${now.getDate()}`;
21 year = now.getFullYear();
22 if (month.length < 2) month = "0" + month;
23 if (day.length < 2) day = "0" + day;
24
25 const date = [year, month, day].join("-");
26
27 return {
28 user_id: $user!.uuid,
29 date,
30 quantity: 1
31 };
32 }
33
34 function closeDialog() {
35 dispatch("close");
36 }
37
38 async function handleSubmitStat()
39 {
40 const { date, quantity } = statistic;
41 await fetch(apiURL("stats"), {
42 method: "POST",
43 headers: {
44 Authorization: `Bearer ${$token}`
45 },
46 body: JSON.stringify({
47 date: new Date(date),
48 user_id: 2,
49 quantity
50 })
51 });
52 dispatch("submit");
53 }
54
55</script>
56
57<dialog {open} on:submit={handleSubmitStat}>
58 <h2>Add Water</h2>
59 <form method="dialog">
60 <div class="form input group">
61 <label for="date">Date:</label>
62 <input bind:value={statistic.date} id="date" name="date" type="date" />
63 </div>
64 <div class="form input group">
65 <label for="quantity">Quantity:</label>
66 <input
67 bind:value={statistic.quantity}
68 id="quantity"
69 name="quantity"
70 type="number"
71 min="0"
72 autocomplete="off"
73 />
74 </div>
75 <button on:click={closeDialog}>Cancel</button>
76 <button type="submit">Submit</button>
77 </form>
78</dialog> \ No newline at end of file
diff --git a/fe/src/main.ts b/fe/src/main.ts
new file mode 100644
index 0000000..ff866d0
--- /dev/null
+++ b/fe/src/main.ts
@@ -0,0 +1,8 @@
1import './app.css'
2import App from './App.svelte'
3
4const app = new App({
5 target: document.getElementById('app') as HTMLElement,
6})
7
8export default app
diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts
new file mode 100644
index 0000000..63f027e
--- /dev/null
+++ b/fe/src/stores/auth.ts
@@ -0,0 +1,90 @@
1import type { Preference, TokenStore, Nullable, UserStore, User, PreferenceStore } from "../types";
2import { writable, derived } from "svelte/store";
3
4
5function createTokenStore(): TokenStore {
6 const storedToken = localStorage.getItem("token");
7 const { subscribe, set } = writable<string | null>(storedToken);
8
9 function authenticate(newToken: string): void {
10 try {
11 localStorage.setItem("token", newToken);
12 set(newToken);
13 } catch (e) {
14 console.error("error", e);
15 }
16 }
17
18 function unauthenticate(): void {
19 localStorage.removeItem("token");
20 set(null);
21 }
22
23 return {
24 subscribe,
25 authenticate,
26 unauthenticate
27 };
28}
29
30function onTokenChange($token: Nullable<string>): boolean {
31 return $token ? true : false;
32}
33
34function createUserStore(): UserStore {
35 const user = localStorage.getItem("user");
36 const userObj: Nullable<User> = user ? JSON.parse(user) : null;
37 const { subscribe, set } = writable<User | null>(userObj);
38
39 const setUser = (user: User) => {
40 localStorage.setItem("user", JSON.stringify(user));
41 set(user);
42 };
43
44 const reset = () => {
45 localStorage.removeItem("user");
46 set(null);
47 };
48
49 return {
50 subscribe,
51 setUser,
52 reset
53 };
54}
55
56
57function createPreferenceStore(): PreferenceStore {
58 const preferences = localStorage.getItem("preferences");
59 const preferenceObj: Preference = preferences ? JSON.parse(preferences) : {
60 id: 0,
61 color: "#FF0000",
62 size_id: 0,
63 user_id: 0
64 };
65
66 const { subscribe, set, update } = writable<Nullable<Preference>>(preferenceObj);
67
68 const setPreference = (preference: Preference) => {
69 localStorage.setItem("preference", JSON.stringify(preference));
70 set(preference);
71 };
72
73 const reset = () => {
74 localStorage.removeItem("preference");
75 set(null);
76 };
77
78 return {
79 set,
80 subscribe,
81 reset,
82 update,
83 setPreference,
84 };
85}
86
87export const token = createTokenStore();
88export const authenticated = derived(token, onTokenChange);
89export const user = createUserStore();
90export const preferences = createPreferenceStore();
diff --git a/fe/src/stores/forms.ts b/fe/src/stores/forms.ts
new file mode 100644
index 0000000..daf9181
--- /dev/null
+++ b/fe/src/stores/forms.ts
@@ -0,0 +1,6 @@
1import type { Writable } from "svelte/store";
2import { writable } from "svelte/store";
3
4
5export const preferencesFormOpen: Writable<boolean> = writable<boolean>(false);
6export const addFormOpen: Writable<boolean> = writable<boolean>(false); \ No newline at end of file
diff --git a/fe/src/types.ts b/fe/src/types.ts
new file mode 100644
index 0000000..c8f2f00
--- /dev/null
+++ b/fe/src/types.ts
@@ -0,0 +1,53 @@
1import type { Invalidator, Subscriber, Unsubscriber, Updater } from "svelte/store";
2
3export interface Preference {
4 id: number;
5 color: string;
6 size_id: number;
7 user_id: number;
8}
9
10export interface Size {
11 size: number;
12 unit: string;
13}
14
15export interface User {
16 id: number;
17 name: string;
18 uuid: string;
19}
20
21export interface Statistic {
22 user_id: string;
23 date: string;
24 quantity: number;
25}
26
27export type Nullable<T> = T | null;
28
29export interface User {
30 uuid: string;
31 username: string;
32}
33
34export interface TokenStore {
35 subscribe: (run: Subscriber<Nullable<string>>, invalidate?: Invalidator<Nullable<string>>) => Unsubscriber,
36 authenticate: (newToken: string) => void,
37 unauthenticate: () => void
38}
39
40
41export interface UserStore {
42 subscribe: (run: Subscriber<Nullable<User>>, invalidate?: Invalidator<Nullable<User>>) => Unsubscriber,
43 setUser: (user: User) => void,
44 reset: () => void
45}
46
47export interface PreferenceStore {
48 set: (this: void, value: Preference) => void;
49 subscribe: (this: void, run: Subscriber<Nullable<Preference>>, invalidate?: Invalidator<Nullable<Preference>>) => Unsubscriber;
50 reset: () => void;
51 update: (this: void, updater: Updater<Nullable<Preference>>) => void;
52 setPreference: (user: Preference) => void;
53}
diff --git a/fe/src/utils.ts b/fe/src/utils.ts
new file mode 100644
index 0000000..9fddf41
--- /dev/null
+++ b/fe/src/utils.ts
@@ -0,0 +1,14 @@
1export function processFormInput(form: HTMLFormElement) {
2 const formData: FormData = new FormData(form);
3 const data: Record<string, any> = {};
4 for (let field of formData) {
5 const [key, value] = field;
6 data[key] = value;
7 }
8 return data;
9}
10
11export function apiURL (path: string): string {
12 const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? "http://localhost:8080/api/v1";
13 return `${baseUrl}${path}`
14}
diff --git a/fe/src/vite-env.d.ts b/fe/src/vite-env.d.ts
new file mode 100644
index 0000000..4078e74
--- /dev/null
+++ b/fe/src/vite-env.d.ts
@@ -0,0 +1,2 @@
1/// <reference types="svelte" />
2/// <reference types="vite/client" />
diff --git a/fe/svelte.config.js b/fe/svelte.config.js
new file mode 100644
index 0000000..b29bf40
--- /dev/null
+++ b/fe/svelte.config.js
@@ -0,0 +1,8 @@
1import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
2
3export default {
4 // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5 // for more information about preprocessors
6 preprocess: vitePreprocess(),
7 dev: true
8}
diff --git a/fe/tsconfig.json b/fe/tsconfig.json
new file mode 100644
index 0000000..5fb548f
--- /dev/null
+++ b/fe/tsconfig.json
@@ -0,0 +1,20 @@
1{
2 "extends": "@tsconfig/svelte/tsconfig.json",
3 "compilerOptions": {
4 "target": "ESNext",
5 "useDefineForClassFields": true,
6 "module": "ESNext",
7 "resolveJsonModule": true,
8 /**
9 * Typecheck JS in `.svelte` and `.js` files by default.
10 * Disable checkJs if you'd like to use dynamic types in JS.
11 * Note that setting allowJs false does not prevent the use
12 * of JS in `.svelte` files.
13 */
14 "allowJs": true,
15 "checkJs": true,
16 "isolatedModules": true
17 },
18 "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
19 "references": [{ "path": "./tsconfig.node.json" }]
20}
diff --git a/fe/tsconfig.node.json b/fe/tsconfig.node.json
new file mode 100644
index 0000000..d02c37d
--- /dev/null
+++ b/fe/tsconfig.node.json
@@ -0,0 +1,10 @@
1{
2 "compilerOptions": {
3 "composite": true,
4 "skipLibCheck": true,
5 "module": "ESNext",
6 "moduleResolution": "bundler",
7 "strict": true
8 },
9 "include": ["vite.config.ts"]
10}
diff --git a/fe/vite.config.ts b/fe/vite.config.ts
new file mode 100644
index 0000000..d701969
--- /dev/null
+++ b/fe/vite.config.ts
@@ -0,0 +1,7 @@
1import { defineConfig } from 'vite'
2import { svelte } from '@sveltejs/vite-plugin-svelte'
3
4// https://vitejs.dev/config/
5export default defineConfig({
6 plugins: [svelte()],
7})