diff options
| author | zberwaldt <17715430+zberwaldt@users.noreply.github.com> | 2024-03-15 22:03:11 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-03-15 22:03:11 -0400 |
| commit | 6f8cfbd6cc3d5adbda38e74013c68e3d4745766d (patch) | |
| tree | b3f045cd06d6622e23441b442e8f3861050ed444 | |
| parent | fac21fa0a72d4a7f1a01ccd44e3acf9c90fd95bd (diff) | |
| parent | fd1332a3df191577e91c6d846a8b5db1747099fd (diff) | |
Merge pull request #1 from zberwaldt/staging
Staging to Prod
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) | ||
| 18 | vendor/ | ||
| 19 | |||
| 20 | # Go workspace file | ||
| 21 | go.work | ||
| 22 | |||
| 23 | |||
| 24 | # Logs | ||
| 25 | logs | ||
| 26 | *.log | ||
| 27 | npm-debug.log* | ||
| 28 | yarn-debug.log* | ||
| 29 | yarn-error.log* | ||
| 30 | lerna-debug.log* | ||
| 31 | .pnpm-debug.log* | ||
| 32 | |||
| 33 | # Dependency directories | ||
| 34 | node_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 | ||
| 2 | TEST_USER=user1 | ||
| 3 | # test user password | ||
| 4 | TEST_PASS=12345 | ||
| 5 | # database path | ||
| 6 | DB_PATH="path/to/database/file" | ||
| 7 | # database driver | ||
| 8 | DB_DRIVER="sqlite3" | ||
| 9 | # port | ||
| 10 | PORT=":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 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "log" | ||
| 5 | "water/api/internal/config" | ||
| 6 | "water/api/internal/router" | ||
| 7 | ) | ||
| 8 | |||
| 9 | func 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 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 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 | |||
| 15 | func 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 | |||
| 26 | func 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 | |||
| 53 | func 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 @@ | |||
| 1 | module water/api | ||
| 2 | |||
| 3 | go 1.18 | ||
| 4 | |||
| 5 | require ( | ||
| 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 | |||
| 14 | require ( | ||
| 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 @@ | |||
| 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= | ||
| 2 | github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= | ||
| 3 | github.com/bytedance/sonic v1.11.0 h1:FwNNv6Vu4z2Onf1++LNzxB/QhitD8wuTdpZzMTGITWo= | ||
| 4 | github.com/bytedance/sonic v1.11.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= | ||
| 5 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | ||
| 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | ||
| 7 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= | ||
| 8 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= | ||
| 9 | github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= | ||
| 10 | github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= | ||
| 11 | github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= | ||
| 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
| 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
| 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= | ||
| 15 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
| 16 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= | ||
| 17 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= | ||
| 18 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= | ||
| 19 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= | ||
| 20 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= | ||
| 21 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||
| 22 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||
| 23 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= | ||
| 24 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= | ||
| 25 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||
| 26 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||
| 27 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||
| 28 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||
| 29 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||
| 30 | github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= | ||
| 31 | github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= | ||
| 32 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | ||
| 33 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | ||
| 34 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||
| 35 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||
| 36 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||
| 37 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||
| 38 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | ||
| 39 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | ||
| 40 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||
| 41 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||
| 42 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||
| 43 | github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= | ||
| 44 | github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||
| 45 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= | ||
| 46 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||
| 47 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||
| 48 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= | ||
| 49 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= | ||
| 50 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= | ||
| 51 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= | ||
| 52 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||
| 53 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||
| 54 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= | ||
| 55 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= | ||
| 56 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= | ||
| 57 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= | ||
| 58 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||
| 59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | ||
| 60 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||
| 61 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||
| 62 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||
| 63 | github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= | ||
| 64 | github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= | ||
| 65 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
| 66 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= | ||
| 67 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
| 68 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= | ||
| 69 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= | ||
| 70 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= | ||
| 71 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= | ||
| 72 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= | ||
| 73 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= | ||
| 74 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= | ||
| 75 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= | ||
| 76 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= | ||
| 77 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= | ||
| 78 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= | ||
| 79 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= | ||
| 80 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||
| 81 | github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= | ||
| 82 | github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= | ||
| 83 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
| 84 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||
| 85 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||
| 86 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||
| 87 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||
| 88 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||
| 89 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||
| 90 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||
| 91 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||
| 92 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||
| 93 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= | ||
| 94 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= | ||
| 95 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||
| 96 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||
| 97 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | ||
| 98 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||
| 99 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= | ||
| 100 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||
| 101 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= | ||
| 102 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= | ||
| 103 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||
| 104 | golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= | ||
| 105 | golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= | ||
| 106 | golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= | ||
| 107 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | ||
| 108 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= | ||
| 109 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= | ||
| 110 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= | ||
| 111 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= | ||
| 112 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| 113 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| 114 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= | ||
| 115 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||
| 116 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= | ||
| 117 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||
| 118 | google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= | ||
| 119 | google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= | ||
| 120 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
| 121 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= | ||
| 122 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= | ||
| 123 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||
| 124 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||
| 125 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
| 126 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||
| 127 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= | ||
| 128 | rsc.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 @@ | |||
| 1 | package config | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "github.com/spf13/viper" | ||
| 6 | ) | ||
| 7 | |||
| 8 | func 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 @@ | |||
| 1 | package controllers | ||
| 2 | |||
| 3 | import ( | ||
| 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. | ||
| 22 | func 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. | ||
| 72 | func 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 @@ | |||
| 1 | package controllers | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "github.com/gin-gonic/gin" | ||
| 5 | "net/http" | ||
| 6 | "database/sql" | ||
| 7 | "water/api/internal/database" | ||
| 8 | "water/api/internal/models" | ||
| 9 | ) | ||
| 10 | |||
| 11 | func 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 @@ | |||
| 1 | package controllers | ||
| 2 | |||
| 3 | import ( | ||
| 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. | ||
| 15 | func 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 | |||
| 54 | func 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 | |||
| 85 | func 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 | |||
| 120 | func 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 | |||
| 158 | func GetUserStatistics(c *gin.Context) { | ||
| 159 | c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) | ||
| 160 | } | ||
| 161 | |||
| 162 | func UpdateUserStatistic(c *gin.Context) { | ||
| 163 | c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) | ||
| 164 | } | ||
| 165 | |||
| 166 | func 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 @@ | |||
| 1 | package controllers | ||
| 2 | |||
| 3 | import ( | ||
| 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 | |||
| 14 | func GetUser(c *gin.Context) { | ||
| 15 | c.JSON(http.StatusOK, gin.H{"message": "User found"}) | ||
| 16 | } | ||
| 17 | func 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 | |||
| 43 | func 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 @@ | |||
| 1 | package database | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "database/sql" | ||
| 5 | _ "github.com/mattn/go-sqlite3" | ||
| 6 | "log" | ||
| 7 | "path/filepath" | ||
| 8 | "water/api/internal/config" | ||
| 9 | ) | ||
| 10 | |||
| 11 | func 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 @@ | |||
| 1 | package middleware | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "errors" | ||
| 5 | "log" | ||
| 6 | "net/http" | ||
| 7 | "strings" | ||
| 8 | |||
| 9 | "github.com/gin-gonic/gin" | ||
| 10 | ) | ||
| 11 | |||
| 12 | func 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 | |||
| 26 | func 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 | |||
| 43 | func 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 @@ | |||
| 1 | package models | ||
| 2 | |||
| 3 | import "time" | ||
| 4 | |||
| 5 | type 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 @@ | |||
| 1 | package models | ||
| 2 | |||
| 3 | type 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 | |||
| 10 | type 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 @@ | |||
| 1 | package models | ||
| 2 | |||
| 3 | import "time" | ||
| 4 | |||
| 5 | type 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 | |||
| 12 | type StatisticPost struct { | ||
| 13 | Date time.Time `json:"date"` | ||
| 14 | Quantity int64 `json:"quantity"` | ||
| 15 | UserID int64 `json:"user_id"` | ||
| 16 | } | ||
| 17 | |||
| 18 | type WeeklyStatistic struct { | ||
| 19 | Date string `json:"date"` | ||
| 20 | Total int64 `json:"total"` | ||
| 21 | } | ||
| 22 | |||
| 23 | type 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 @@ | |||
| 1 | package models | ||
| 2 | |||
| 3 | import "github.com/google/uuid" | ||
| 4 | |||
| 5 | type 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 @@ | |||
| 1 | package router | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "github.com/gin-gonic/gin" | ||
| 5 | "water/api/internal/controllers" | ||
| 6 | "water/api/internal/middleware" | ||
| 7 | ) | ||
| 8 | |||
| 9 | func 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. | ||
| 2 | CREATE 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 | ||
| 10 | CREATE 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. | ||
| 18 | CREATE 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. | ||
| 28 | CREATE TABLE IF NOT EXISTS Sizes ( | ||
| 29 | id INTEGER PRIMARY KEY, | ||
| 30 | size INT NOT NULL, | ||
| 31 | unit TEXT DEFAULT "oz" | ||
| 32 | ); | ||
| 33 | |||
| 34 | CREATE 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. | ||
| 42 | INSERT OR IGNORE INTO Sizes (id, size) VALUES (1, 8), (2, 16), (3, 24), (4, 32), (5, 40), (6, 48); | ||
| 43 | |||
| 44 | -- create default users. | ||
| 45 | INSERT 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. | ||
| 56 | INSERT OR IGNORE INTO Preferences (user_id) VALUES (1), (2); | ||
| 57 | |||
| 58 | CREATE TRIGGER IF NOT EXISTS enforce_size_id | ||
| 59 | BEFORE INSERT ON Preferences | ||
| 60 | BEGIN | ||
| 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; | ||
| 68 | END; | ||
| 69 | |||
| 70 | -- | ||
| 71 | CREATE 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 | |||
| 74 | CREATE VIEW IF NOT EXISTS `DailyUserStatistics` AS | ||
| 75 | SELECT users.name, IFNULL(SUM(statistics.quantity), 0) as total, preferences.color as color | ||
| 76 | FROM users | ||
| 77 | LEFT JOIN statistics ON users.id = statistics.user_id AND DATE(statistics.date) = DATE('now', '-1 day') | ||
| 78 | LEFT JOIN preferences ON users.id = preferences.user_id | ||
| 79 | GROUP BY users.name; | ||
| 80 | |||
| 81 | |||
| 82 | CREATE 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 | ) | ||
| 91 | SELECT DateSequence.Dates as 'date', | ||
| 92 | IFNULL(SUM(statistics.quantity), 0) AS 'total' | ||
| 93 | FROM DateSequence | ||
| 94 | LEFT JOIN statistics | ||
| 95 | ON Date(statistics.date) = DateSequence.Dates | ||
| 96 | GROUP BY DateSequence.Dates | ||
| 97 | ORDER 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 | ||
| 2 | logs | ||
| 3 | *.log | ||
| 4 | npm-debug.log* | ||
| 5 | yarn-debug.log* | ||
| 6 | yarn-error.log* | ||
| 7 | pnpm-debug.log* | ||
| 8 | lerna-debug.log* | ||
| 9 | |||
| 10 | node_modules | ||
| 11 | dist | ||
| 12 | dist-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 | |||
| 3 | This 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 | |||
| 11 | Check 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 | |||
| 20 | This 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 | |||
| 22 | Should 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 | |||
| 26 | Setting `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 | |||
| 30 | Other 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 | |||
| 34 | While `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 | |||
| 38 | HMR 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 | |||
| 40 | If 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 | ||
| 45 | import { writable } from 'svelte/store' | ||
| 46 | export 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 | |||
| 18 | a { | ||
| 19 | font-weight: 500; | ||
| 20 | color: #646cff; | ||
| 21 | text-decoration: inherit; | ||
| 22 | } | ||
| 23 | |||
| 24 | a:hover { | ||
| 25 | color: #535bf2; | ||
| 26 | } | ||
| 27 | |||
| 28 | body { | ||
| 29 | margin: 0; | ||
| 30 | display: flex; | ||
| 31 | place-items: center; | ||
| 32 | min-width: 320px; | ||
| 33 | min-height: 100vh; | ||
| 34 | } | ||
| 35 | |||
| 36 | h1 { | ||
| 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 | |||
| 51 | button { | ||
| 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 | |||
| 63 | button:hover { | ||
| 64 | border-color: #646cff; | ||
| 65 | } | ||
| 66 | |||
| 67 | button:focus, | ||
| 68 | button: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 @@ | |||
| 1 | export 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 @@ | |||
| 1 | let instance; | ||
| 2 | const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? "http://localhost:8080/api/v1"; | ||
| 3 | |||
| 4 | class 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 | |||
| 83 | interface IHttpParameters { | ||
| 84 | endpoint: string; | ||
| 85 | body: Record<string, any>; | ||
| 86 | authenticated: boolean; | ||
| 87 | headers: Headers; | ||
| 88 | } | ||
| 89 | |||
| 90 | let http: Readonly<HttpClient> = Object.freeze(new HttpClient(baseUrl)); | ||
| 91 | |||
| 92 | export 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"> | ||
| 2 | import { onDestroy } from "svelte"; | ||
| 3 | import ChartJS from "chart.js/auto"; | ||
| 4 | |||
| 5 | export let data; | ||
| 6 | export let labels; | ||
| 7 | export let type = 'bar'; | ||
| 8 | |||
| 9 | let ref: HTMLCanvasElement; | ||
| 10 | let chart | ||
| 11 | |||
| 12 | function 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 @@ | |||
| 1 | import './app.css' | ||
| 2 | import App from './App.svelte' | ||
| 3 | |||
| 4 | const app = new App({ | ||
| 5 | target: document.getElementById('app') as HTMLElement, | ||
| 6 | }) | ||
| 7 | |||
| 8 | export 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 @@ | |||
| 1 | import type { Preference, TokenStore, Nullable, UserStore, User, PreferenceStore } from "../types"; | ||
| 2 | import { writable, derived } from "svelte/store"; | ||
| 3 | |||
| 4 | |||
| 5 | function 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 | |||
| 30 | function onTokenChange($token: Nullable<string>): boolean { | ||
| 31 | return $token ? true : false; | ||
| 32 | } | ||
| 33 | |||
| 34 | function 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 | |||
| 57 | function 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 | |||
| 87 | export const token = createTokenStore(); | ||
| 88 | export const authenticated = derived(token, onTokenChange); | ||
| 89 | export const user = createUserStore(); | ||
| 90 | export 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 @@ | |||
| 1 | import type { Writable } from "svelte/store"; | ||
| 2 | import { writable } from "svelte/store"; | ||
| 3 | |||
| 4 | |||
| 5 | export const preferencesFormOpen: Writable<boolean> = writable<boolean>(false); | ||
| 6 | export 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 @@ | |||
| 1 | import type { Invalidator, Subscriber, Unsubscriber, Updater } from "svelte/store"; | ||
| 2 | |||
| 3 | export interface Preference { | ||
| 4 | id: number; | ||
| 5 | color: string; | ||
| 6 | size_id: number; | ||
| 7 | user_id: number; | ||
| 8 | } | ||
| 9 | |||
| 10 | export interface Size { | ||
| 11 | size: number; | ||
| 12 | unit: string; | ||
| 13 | } | ||
| 14 | |||
| 15 | export interface User { | ||
| 16 | id: number; | ||
| 17 | name: string; | ||
| 18 | uuid: string; | ||
| 19 | } | ||
| 20 | |||
| 21 | export interface Statistic { | ||
| 22 | user_id: string; | ||
| 23 | date: string; | ||
| 24 | quantity: number; | ||
| 25 | } | ||
| 26 | |||
| 27 | export type Nullable<T> = T | null; | ||
| 28 | |||
| 29 | export interface User { | ||
| 30 | uuid: string; | ||
| 31 | username: string; | ||
| 32 | } | ||
| 33 | |||
| 34 | export interface TokenStore { | ||
| 35 | subscribe: (run: Subscriber<Nullable<string>>, invalidate?: Invalidator<Nullable<string>>) => Unsubscriber, | ||
| 36 | authenticate: (newToken: string) => void, | ||
| 37 | unauthenticate: () => void | ||
| 38 | } | ||
| 39 | |||
| 40 | |||
| 41 | export interface UserStore { | ||
| 42 | subscribe: (run: Subscriber<Nullable<User>>, invalidate?: Invalidator<Nullable<User>>) => Unsubscriber, | ||
| 43 | setUser: (user: User) => void, | ||
| 44 | reset: () => void | ||
| 45 | } | ||
| 46 | |||
| 47 | export 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 @@ | |||
| 1 | export 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 | |||
| 11 | export 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 @@ | |||
| 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' | ||
| 2 | |||
| 3 | export 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 @@ | |||
| 1 | import { defineConfig } from 'vite' | ||
| 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' | ||
| 3 | |||
| 4 | // https://vitejs.dev/config/ | ||
| 5 | export default defineConfig({ | ||
| 6 | plugins: [svelte()], | ||
| 7 | }) | ||
