diff --git a/.gitea/workflows/build-and-test.yaml b/.gitea/workflows/build-and-test.yaml new file mode 100644 index 0000000..c32ddf7 --- /dev/null +++ b/.gitea/workflows/build-and-test.yaml @@ -0,0 +1,49 @@ +name: Build and Test + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +jobs: + build-and-test-backend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.24.5" + + - name: Tidy and Vendor + run: cd backend && go mod tidy && go mod vendor + + - name: build + run: cd backend && go build -v ./... + + - name: test + run: cd backend && go test -v ./... + + build-and-test-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-verion: "24.1.0" + + - name: Install dependencies + run: cd frontend && npm ci + + - name: Build + run: cd frontend && npm run build diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..5e63467 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +go.sum +backend diff --git a/backend/api/Accessible.go b/backend/api/Accessible.go new file mode 100644 index 0000000..a3f8e86 --- /dev/null +++ b/backend/api/Accessible.go @@ -0,0 +1,17 @@ +package api + +import ( + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt" +) + +func Accessible(c *fiber.Ctx) error { + return c.SendString("Accessible") +} + +func Restricted(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + name := claims["name"].(string) + return c.SendString("Welcome " + name) +} diff --git a/backend/api/GetAllCategories.go b/backend/api/GetAllCategories.go new file mode 100644 index 0000000..b26bd0e --- /dev/null +++ b/backend/api/GetAllCategories.go @@ -0,0 +1,17 @@ +package api + +import ( + "backend/internal/category" + + "github.com/gofiber/fiber/v2" +) + +func GetAllCategories(c *fiber.Ctx) error { + categories, err := category.GetAllCategories() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + return c.JSON(categories) +} diff --git a/backend/api/GetAllOperation.go b/backend/api/GetAllOperation.go new file mode 100644 index 0000000..8e03bc8 --- /dev/null +++ b/backend/api/GetAllOperation.go @@ -0,0 +1,17 @@ +package api + +import ( + "backend/internal/operation" + + "github.com/gofiber/fiber/v2" +) + +func GetAllOperation(c *fiber.Ctx) error { + operations, err := operation.GetAllOperation() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + return c.JSON(operations) +} diff --git a/backend/api/Login.go b/backend/api/Login.go new file mode 100644 index 0000000..e5e20d5 --- /dev/null +++ b/backend/api/Login.go @@ -0,0 +1,36 @@ +package api + +import ( + "backend/model" + "fmt" + "os" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt" +) + +func Login(c *fiber.Ctx) error { + var requestdata model.AuthRequest + err := c.BodyParser(&requestdata) + if err != nil { + return fmt.Errorf("Error parse body, %s", err) + } + if requestdata.Username != "tola" || requestdata.Password != "2808" { + return c.SendStatus(fiber.StatusUnauthorized) + } + + claims := jwt.MapClaims{ + "username": "tola", + "is_superuser": true, + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + t, err := token.SignedString([]byte(os.Getenv("JWT_SECRET_KEY"))) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.JSON(fiber.Map{"token": t}) +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..1ca3944 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,26 @@ +module backend + +go 1.24.5 + +require ( + github.com/gofiber/contrib/jwt v1.1.2 + github.com/gofiber/fiber/v2 v2.52.9 + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.28.0 // indirect +) diff --git a/backend/internal/category/GetAllCategories.go b/backend/internal/category/GetAllCategories.go new file mode 100644 index 0000000..e58bff1 --- /dev/null +++ b/backend/internal/category/GetAllCategories.go @@ -0,0 +1,48 @@ +package category + +import "backend/model" + +func GetAllCategories() ([]model.Category, error) { + return []model.Category{ + { + ID: "1", + Title: "Electronics", + Icon: "😀", + Type: "expense", + }, + { + ID: "2", + Title: "Clothing", + Icon: "😁", + Type: "expense", + }, + { + ID: "3", + Title: "Books", + Icon: "😂", + Subcategories: []model.Subcategory{ + { + ID: "1", + Title: "Fiction", + }, + { + ID: "2", + Title: "Non-Fiction", + }, + }, + Type: "expense", + }, + { + ID: "4", + Title: "Salary", + Icon: "😀", + Type: "income", + }, + { + ID: "5", + Title: "asdf", + Icon: "😀", + Type: "income", + }, + }, nil +} diff --git a/backend/internal/operation/GetAllOperation.go b/backend/internal/operation/GetAllOperation.go new file mode 100644 index 0000000..152f404 --- /dev/null +++ b/backend/internal/operation/GetAllOperation.go @@ -0,0 +1,73 @@ +package operation + +import ( + "backend/model" + "time" +) + +func GetAllOperation() ([]model.Operation, error) { + return []model.Operation{ + { + ID: "1", + Title: "Operation 1", + Amount: 100, + Category: "Category 1", + SubCategory: model.Subcategory{ + ID: "1", + Title: "subcategory", + }, + Date: "2022-01-01T00:00:00Z", + Type: "income", + Icon: "😀", + }, + { + ID: "2", + Title: "Operation 2", + Amount: 200, + Category: "Category 2", + Date: "2022-01-02T00:00:00Z", + Type: "expense", + Icon: "😂", + }, + { + ID: "2", + Title: "Operation 2", + Amount: 200, + Category: "Category 2", + Date: time.Now().Format(time.RFC3339), + Type: "expense", + Icon: "😂", + }, + { + ID: "1", + Title: "Operation 1", + Amount: 100, + Category: "Category 1", + SubCategory: model.Subcategory{ + ID: "1", + Title: "subcategory", + }, + Date: "2022-01-02T00:00:00Z", + Type: "income", + Icon: "😀", + }, + { + ID: "2", + Title: "Operation 2", + Amount: 200, + Category: "Category 2", + Date: "2022-06-02T00:00:00Z", + Type: "expense", + Icon: "😂", + }, + { + ID: "2", + Title: "Operation 2", + Amount: 200, + Category: "Category 2", + Date: "2022-06-22T00:00:00Z", + Type: "expense", + Icon: "😂", + }, + }, nil +} diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..1c8cffc --- /dev/null +++ b/backend/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "backend/server" + "log" + "os" + "strconv" + + "github.com/gofiber/fiber/v2" +) + +func main() { + app := fiber.New() + + server.Configure(app) + + enableTls, err := strconv.ParseBool(os.Getenv("ENABLE_TLS")) + if err != nil { + log.Fatalf("error read tls configuration, %v", err) + } + if enableTls { + log.Fatal( + app.ListenTLS( + os.Getenv("HTTP_ADDRESS"), + os.Getenv("CERT_FILE"), + os.Getenv("KEY_FILE"), + ), + ) + } else { + log.Fatal( + app.Listen(os.Getenv("HTTP_ADDRESS")), + ) + } +} diff --git a/backend/model/category.go b/backend/model/category.go new file mode 100644 index 0000000..52abbfa --- /dev/null +++ b/backend/model/category.go @@ -0,0 +1,9 @@ +package model + +type Category struct { + ID string `json:"id"` + Icon string `json:"icon"` + Title string `json:"title"` + Subcategories []Subcategory `json:"subcategories"` + Type string `json:"type"` +} diff --git a/backend/model/operation.go b/backend/model/operation.go new file mode 100644 index 0000000..1e0e4c5 --- /dev/null +++ b/backend/model/operation.go @@ -0,0 +1,13 @@ +package model + +type Operation struct { + ID string `json:"id"` + Title string `json:"title"` + Amount float64 `json:"amount"` + Category string `json:"category"` + SubCategory Subcategory `json:"subcategory,omitzero"` + Note string `json:"note,omitempty"` + Icon string `json:"icon"` + Type string `json:"type"` + Date string `json:"date"` +} diff --git a/backend/model/subcategory.go b/backend/model/subcategory.go new file mode 100644 index 0000000..73bf613 --- /dev/null +++ b/backend/model/subcategory.go @@ -0,0 +1,6 @@ +package model + +type Subcategory struct { + ID string `json:"id"` + Title string `json:"title"` +} diff --git a/backend/model/user.go b/backend/model/user.go new file mode 100644 index 0000000..bedcc61 --- /dev/null +++ b/backend/model/user.go @@ -0,0 +1,14 @@ +package model + +type AuthRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type User struct { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` + Group string `json:"group"` + IsSuperuser bool `json:"is_superuser"` +} diff --git a/backend/server/configure.go b/backend/server/configure.go new file mode 100644 index 0000000..d4b462a --- /dev/null +++ b/backend/server/configure.go @@ -0,0 +1,37 @@ +package server + +import ( + "backend/api" + "backend/utils" + "os" + + jwtware "github.com/gofiber/contrib/jwt" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func Configure(app *fiber.App) { + utils.LoadEnv("wallet.conf") + log.SetLevel(log.LevelDebug) + app.Use(cors.New(cors.Config{ + AllowOrigins: "http://localhost:3000", + AllowHeaders: "*", + })) + app.Static("/", "../frontend/build") + apiGroup := app.Group("/api") + + apiGroup.Post("/token", api.Login) + apiGroup.Get("/accessible", api.Accessible) + app.Use(jwtware.New(jwtware.Config{ + SigningKey: jwtware.SigningKey{ + Key: []byte(os.Getenv("JWT_SECRET_KEY")), + }, + })) + apiGroup.Get("/rescticted", api.Restricted) + operationGroup := apiGroup.Group("/operation") + operationGroup.Get("/", api.GetAllOperation) + + categoryGroup := apiGroup.Group("/category") + categoryGroup.Get("/", api.GetAllCategories) +} diff --git a/backend/utils/LoadEnv.go b/backend/utils/LoadEnv.go new file mode 100644 index 0000000..8a3820a --- /dev/null +++ b/backend/utils/LoadEnv.go @@ -0,0 +1,14 @@ +package utils + +import ( + "log" + + "github.com/joho/godotenv" +) + +func LoadEnv(filename string) { + err := godotenv.Load(filename) + if err != nil { + log.Fatal("Error loading .env file") + } +} diff --git a/backend/wallet.conf b/backend/wallet.conf new file mode 100644 index 0000000..c9dd73b --- /dev/null +++ b/backend/wallet.conf @@ -0,0 +1,5 @@ +JWT_SECRET_KEY=secret +HTTP_ADDRESS=0.0.0.0:43243 +ENABLE_TLS=true +CERT_FILE=/home/tola/certs/localhost/localhost.crt +KEY_FILE=/home/tola/certs/localhost/localhost.key diff --git a/.gitignore b/frontend/.gitignore similarity index 97% rename from .gitignore rename to frontend/.gitignore index 61ef65f..c8e674e 100644 --- a/.gitignore +++ b/frontend/.gitignore @@ -23,3 +23,4 @@ yarn-debug.log* yarn-error.log* bun.lock bun.lockb +deno.lock diff --git a/README.md b/frontend/README.md similarity index 100% rename from README.md rename to frontend/README.md diff --git a/package-lock.json b/frontend/package-lock.json similarity index 96% rename from package-lock.json rename to frontend/package-lock.json index 0f71696..1861b92 100644 --- a/package-lock.json +++ b/frontend/package-lock.json @@ -12,8 +12,17 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.11.0", + "bootstrap": "^5.3.7", + "chart.js": "^4.5.0", + "classnames": "^2.5.1", + "emoji-picker-react": "^4.13.2", + "faker": "^6.6.6", "react": "^19.1.0", + "react-bootstrap": "^2.10.10", + "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", + "react-router-dom": "^7.7.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" } @@ -2945,6 +2954,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -3075,6 +3090,85 @@ } } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz", + "integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@popperjs/core": "^2.11.8", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.5.0", + "@types/warning": "^3.0.3", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.4", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/@restart/hooks": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz", + "integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.14.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3423,46 +3517,46 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", - "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", + "picocolors": "1.1.1", "pretty-format": "^27.0.2" }, "engines": { "node": ">=18" } }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", - "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz", + "integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==", "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", - "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", + "picocolors": "^1.1.1", "redent": "^3.0.0" }, "engines": { @@ -3471,19 +3565,6 @@ "yarn": ">=1" } }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", @@ -3590,12 +3671,12 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/body-parser": { @@ -3771,12 +3852,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/node-forge": { @@ -3800,6 +3881,12 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "license": "MIT" }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, "node_modules/@types/q": { "version": "1.5.8", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", @@ -3818,6 +3905,24 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", + "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -3890,6 +3995,12 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -4559,15 +4670,12 @@ } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -4608,12 +4716,12 @@ } }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" + "dependencies": { + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -4908,6 +5016,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -5086,9 +5205,9 @@ "license": "MIT" }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -5108,7 +5227,7 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { @@ -5293,6 +5412,25 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "node_modules/bootstrap": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz", + "integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -5489,9 +5627,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001733", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001733.tgz", + "integrity": "sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==", "funding": [ { "type": "opencollective", @@ -5533,6 +5671,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -5542,6 +5695,18 @@ "node": ">=10" } }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -5614,6 +5779,12 @@ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "license": "MIT" }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -5907,12 +6078,12 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/cookie-signature": { @@ -5922,9 +6093,9 @@ "license": "MIT" }, "node_modules/core-js": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz", - "integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz", + "integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -5933,9 +6104,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", - "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz", + "integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==", "license": "MIT", "dependencies": { "browserslist": "^4.25.1" @@ -5946,9 +6117,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.44.0.tgz", - "integrity": "sha512-gvMQAGB4dfVUxpYD0k3Fq8J+n5bB6Ytl15lqlZrOIXFzxOhtPaObfkQGHtMRdyjIf7z2IeNULwi1jEwyS+ltKQ==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.0.tgz", + "integrity": "sha512-OtwjqcDpY2X/eIIg1ol/n0y/X8A9foliaNt1dSK0gV3J2/zw+89FcNG3mPK+N8YWts4ZFUPxnrAzsxs/lf8yDA==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -6376,6 +6547,12 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "license": "MIT" }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6702,6 +6879,16 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -6852,9 +7039,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.190", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", - "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==", + "version": "1.5.199", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", + "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", "license": "ISC" }, "node_modules/emittery": { @@ -6869,6 +7056,21 @@ "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, + "node_modules/emoji-picker-react": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.13.2.tgz", + "integrity": "sha512-azaJQLTshEOZVhksgU136izJWJyZ4Clx6xQ6Vctzk1gOdPPAUbTa/JYDwZJ8rh97QxnjpyeftXl99eRlYr3vNA==", + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -6894,9 +7096,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -7436,6 +7638,15 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -7888,6 +8099,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7903,6 +8123,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/faker": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/faker/-/faker-6.6.6.tgz", + "integrity": "sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8159,6 +8385,12 @@ "node": ">=8" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "license": "MIT" + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -8180,9 +8412,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -8340,16 +8572,16 @@ } }, "node_modules/form-data": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", - "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", - "mime-types": "^2.1.35" + "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" @@ -9263,6 +9495,15 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -9923,15 +10164,14 @@ } }, "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "license": "Apache-2.0", "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", + "async": "^3.2.6", "filelist": "^1.0.4", - "minimatch": "^3.1.2" + "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" @@ -10582,18 +10822,6 @@ "@types/yargs-parser": "*" } }, - "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/jest-watch-typeahead/node_modules/emittery": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", @@ -10911,6 +11139,22 @@ } } }, + "node_modules/jsdom/node_modules/form-data": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -11079,13 +11323,13 @@ } }, "node_modules/launch-editor": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", - "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", + "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", "license": "MIT", "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" } }, "node_modules/leven": { @@ -11411,9 +11655,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", - "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.3.tgz", + "integrity": "sha512-tRA0+PsS4kLVijnN1w9jUu5lkxBwUk9E8SbgEB5dBJqchE6pVYdawROG6uQtpmAri7tdCK9i7b1bULeVWqS6Ag==", "license": "MIT", "dependencies": { "schema-utils": "^4.0.0", @@ -11639,9 +11883,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", "license": "MIT" }, "node_modules/object-assign": { @@ -12736,9 +12980,9 @@ } }, "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -13556,18 +13800,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -13607,6 +13839,25 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "license": "MIT", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "node_modules/prop-types-extra/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -13635,6 +13886,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -13763,9 +14020,9 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13788,6 +14045,47 @@ "node": ">=14" } }, + "node_modules/react-bootstrap": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz", + "integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.9.4", + "@types/prop-types": "^15.7.12", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -13894,15 +14192,15 @@ } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.1.1" } }, "node_modules/react-error-overlay": { @@ -13917,6 +14215,12 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -13926,6 +14230,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz", + "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz", + "integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==", + "license": "MIT", + "dependencies": { + "react-router": "7.8.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -13999,6 +14341,22 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -14832,6 +15190,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -16384,6 +16748,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/underscore": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", @@ -16391,9 +16770,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -16636,6 +17015,15 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", @@ -16674,9 +17062,9 @@ } }, "node_modules/webpack": { - "version": "5.100.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.2.tgz", - "integrity": "sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==", + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.0.tgz", + "integrity": "sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", @@ -17197,6 +17585,7 @@ "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" @@ -17430,6 +17819,36 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/frontend/package.json similarity index 92% rename from package.json rename to frontend/package.json index 49490c5..600a6ba 100644 --- a/package.json +++ b/frontend/package.json @@ -7,8 +7,11 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.11.0", "bootstrap": "^5.3.7", "chart.js": "^4.5.0", + "classnames": "^2.5.1", + "emoji-picker-react": "^4.13.2", "faker": "^6.6.6", "react": "^19.1.0", "react-bootstrap": "^2.10.10", diff --git a/public/favicon.ico b/frontend/public/favicon.ico similarity index 100% rename from public/favicon.ico rename to frontend/public/favicon.ico diff --git a/public/index.html b/frontend/public/index.html similarity index 100% rename from public/index.html rename to frontend/public/index.html diff --git a/public/logo192.png b/frontend/public/logo192.png similarity index 100% rename from public/logo192.png rename to frontend/public/logo192.png diff --git a/public/logo512.png b/frontend/public/logo512.png similarity index 100% rename from public/logo512.png rename to frontend/public/logo512.png diff --git a/public/manifest.json b/frontend/public/manifest.json similarity index 100% rename from public/manifest.json rename to frontend/public/manifest.json diff --git a/public/robots.txt b/frontend/public/robots.txt similarity index 100% rename from public/robots.txt rename to frontend/public/robots.txt diff --git a/src/App.jsx b/frontend/src/App.jsx similarity index 71% rename from src/App.jsx rename to frontend/src/App.jsx index 8aece72..59886a1 100644 --- a/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,6 +3,7 @@ import NavigationBar from "./components/NavigationBar/NavigationBar"; import { useTheme } from "./contexts/ThemeContext"; import { useEffect } from "react"; import Home from "./pages/Home"; +import Login from "./pages/Login"; import { BrowserRouter as Router, Routes, @@ -10,6 +11,7 @@ import { Navigate, } from "react-router-dom"; import NotFound from "./pages/NotFound"; +import PrivateRoute from "./components/PrivateRoute"; function App() { const { theme } = useTheme(); @@ -23,7 +25,15 @@ function App() { <> - } /> + + + + } + /> + } /> } /> } /> diff --git a/src/components/AppointmentDashboard/AppointmentDashboard.jsx b/frontend/src/components/AppointmentDashboard/AppointmentDashboard.jsx similarity index 100% rename from src/components/AppointmentDashboard/AppointmentDashboard.jsx rename to frontend/src/components/AppointmentDashboard/AppointmentDashboard.jsx diff --git a/src/components/BalanceBlock/BalanceBlock.jsx b/frontend/src/components/BalanceBlock/BalanceBlock.jsx similarity index 100% rename from src/components/BalanceBlock/BalanceBlock.jsx rename to frontend/src/components/BalanceBlock/BalanceBlock.jsx diff --git a/frontend/src/components/DayTitle.jsx b/frontend/src/components/DayTitle.jsx new file mode 100644 index 0000000..11e37e7 --- /dev/null +++ b/frontend/src/components/DayTitle.jsx @@ -0,0 +1,29 @@ +const DayTitle = ({ date, income, expense, ...props }) => { + return ( +
+

+ {/* 25 Jul 2025*/} + {date} +

+
+
+ Income: {income} ₽ +
+
+ Expense: {expense} ₽ +
+
+
+ ); +}; + +export default DayTitle; diff --git a/src/components/FinancialDashboard/FinancialDashboard.jsx b/frontend/src/components/FinancialDashboard/FinancialDashboard.jsx similarity index 100% rename from src/components/FinancialDashboard/FinancialDashboard.jsx rename to frontend/src/components/FinancialDashboard/FinancialDashboard.jsx diff --git a/src/components/FinancialDashboard/FinancialDashboard.module.css b/frontend/src/components/FinancialDashboard/FinancialDashboard.module.css similarity index 100% rename from src/components/FinancialDashboard/FinancialDashboard.module.css rename to frontend/src/components/FinancialDashboard/FinancialDashboard.module.css diff --git a/frontend/src/components/InputTitle.jsx b/frontend/src/components/InputTitle.jsx new file mode 100644 index 0000000..dc59e50 --- /dev/null +++ b/frontend/src/components/InputTitle.jsx @@ -0,0 +1,12 @@ +const InputTitle = ({ text, ...props }) => { + return ( +

+ {text} +

+ ); +}; +export default InputTitle; diff --git a/src/components/NavigationBar/NavigationBar.jsx b/frontend/src/components/NavigationBar/NavigationBar.jsx similarity index 80% rename from src/components/NavigationBar/NavigationBar.jsx rename to frontend/src/components/NavigationBar/NavigationBar.jsx index 1fa9177..9837f63 100644 --- a/src/components/NavigationBar/NavigationBar.jsx +++ b/frontend/src/components/NavigationBar/NavigationBar.jsx @@ -4,14 +4,14 @@ import SettingsModal from "../../modals/SettingsModal/SettingsModal"; const NavigationBar = () => { return ( - Wallet - + ); }; diff --git a/src/components/NavigationBar/NavigationBar.module.css b/frontend/src/components/NavigationBar/NavigationBar.module.css similarity index 100% rename from src/components/NavigationBar/NavigationBar.module.css rename to frontend/src/components/NavigationBar/NavigationBar.module.css diff --git a/frontend/src/components/PrivateRoute.jsx b/frontend/src/components/PrivateRoute.jsx new file mode 100644 index 0000000..db0fd2a --- /dev/null +++ b/frontend/src/components/PrivateRoute.jsx @@ -0,0 +1,7 @@ +import isAuthenticated from "../logic/isAuthenticated"; +import Login from "../pages/Login"; + +const PrivateRoute = ({ children }) => { + return isAuthenticated() ? children : ; +}; +export default PrivateRoute; diff --git a/src/components/ThemeToggle.jsx b/frontend/src/components/ThemeToggle.jsx similarity index 100% rename from src/components/ThemeToggle.jsx rename to frontend/src/components/ThemeToggle.jsx diff --git a/src/contexts/ThemeContext.js b/frontend/src/contexts/ThemeContext.js similarity index 100% rename from src/contexts/ThemeContext.js rename to frontend/src/contexts/ThemeContext.js diff --git a/src/css/App.css b/frontend/src/css/App.css similarity index 100% rename from src/css/App.css rename to frontend/src/css/App.css diff --git a/src/css/index.css b/frontend/src/css/index.css similarity index 100% rename from src/css/index.css rename to frontend/src/css/index.css diff --git a/src/index.js b/frontend/src/index.js similarity index 100% rename from src/index.js rename to frontend/src/index.js diff --git a/frontend/src/logic/category/GetAllCategories.js b/frontend/src/logic/category/GetAllCategories.js new file mode 100644 index 0000000..f960ced --- /dev/null +++ b/frontend/src/logic/category/GetAllCategories.js @@ -0,0 +1,18 @@ +import routes from "../../routes"; + +const fetchCategories = async () => { + try { + const response = await fetch(routes.categories, { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + const data = await response.json(); + return data; + } catch (error) { + console.error("Error fetching categories:", error); + return []; + } +}; + +export default fetchCategories; diff --git a/frontend/src/logic/getDateString.js b/frontend/src/logic/getDateString.js new file mode 100644 index 0000000..31c2238 --- /dev/null +++ b/frontend/src/logic/getDateString.js @@ -0,0 +1,8 @@ +const getDateString = (date) => { + const dateData = new Date(date); + return [ + dateData.getHours().toString().padStart(2, "0"), + dateData.getMinutes().toString().padStart(2, "0"), + ].join(":"); +}; +export default getDateString; diff --git a/frontend/src/logic/isAuthenticated.js b/frontend/src/logic/isAuthenticated.js new file mode 100644 index 0000000..9fcb178 --- /dev/null +++ b/frontend/src/logic/isAuthenticated.js @@ -0,0 +1,7 @@ +const isAuthenticated = () => { + if (localStorage.getItem("token") === null) { + return false; + } + return true; +}; +export default isAuthenticated; diff --git a/frontend/src/logic/login.js b/frontend/src/logic/login.js new file mode 100644 index 0000000..997c809 --- /dev/null +++ b/frontend/src/logic/login.js @@ -0,0 +1,33 @@ +import routes from "../routes"; + +const login = async (userdata, e) => { + e.preventDefault(); + console.log({ + username: userdata.username, + password: userdata.password, + }); + const response = await fetch(routes.token, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: userdata.username, + password: userdata.password, + }), + }); + if (response.status === 200) { + const data = await response.json(); + localStorage.setItem("token", data.token); + window.location.reload(); + return true; + } + if (response.status === 401) { + return false; + } + if (response.status === 500) { + return false; + } +}; + +export default login; diff --git a/frontend/src/logic/logout.js b/frontend/src/logic/logout.js new file mode 100644 index 0000000..43872e2 --- /dev/null +++ b/frontend/src/logic/logout.js @@ -0,0 +1,6 @@ +const logout = () => { + localStorage.clear(); + window.location.reload(); +}; + +export default logout; diff --git a/frontend/src/logic/operation/GetAllOperation.js b/frontend/src/logic/operation/GetAllOperation.js new file mode 100644 index 0000000..41b44b5 --- /dev/null +++ b/frontend/src/logic/operation/GetAllOperation.js @@ -0,0 +1,13 @@ +import routes from "../../routes"; + +const GetAllOperation = async () => { + const response = await fetch(routes.operations, { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + const data = await response.json(); + return data; +}; + +export default GetAllOperation; diff --git a/frontend/src/modals/AddCategoryModal/AddCategoryModal.jsx b/frontend/src/modals/AddCategoryModal/AddCategoryModal.jsx new file mode 100644 index 0000000..439cd2b --- /dev/null +++ b/frontend/src/modals/AddCategoryModal/AddCategoryModal.jsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import { useTheme } from "../../contexts/ThemeContext"; +import { Modal, Button, Card, Form } from "react-bootstrap"; +import EmojiPickerButton from "./components/EmojiPickerButton/EmojiPickerButton"; + +const AddCategoryModal = ({ variant, ...props }) => { + const { theme } = useTheme(); + const [show, setShow] = useState(false); + const handleClose = () => setShow(false); + const handleShow = () => setShow(true); + const [title, setTitle] = useState(""); + return ( + <> + + + + + New Category + + + +
+ +
+ setTitle(e.target.value)} + className="mb-3" + /> + + +
+ SUBCATEGORIES +
+ + + +
+
+
+
+ + ); +}; +export default AddCategoryModal; diff --git a/frontend/src/modals/AddCategoryModal/components/EmojiPickerButton/EmojiPickerButton.jsx b/frontend/src/modals/AddCategoryModal/components/EmojiPickerButton/EmojiPickerButton.jsx new file mode 100644 index 0000000..7e5c241 --- /dev/null +++ b/frontend/src/modals/AddCategoryModal/components/EmojiPickerButton/EmojiPickerButton.jsx @@ -0,0 +1,58 @@ +import React, { useState, useRef } from "react"; +import { Button, Overlay, Popover, InputGroup } from "react-bootstrap"; +import EmojiPicker from "emoji-picker-react"; +import { useTheme } from "../../../../contexts/ThemeContext"; + +const EmojiPickerButton = () => { + const { theme } = useTheme(); + const [showPicker, setShowPicker] = useState(false); + const [inputValue, setInputValue] = useState("😀"); + const target = useRef(null); + + const handleEmojiSelect = (emoji) => { + setInputValue(emoji.emoji); + setShowPicker(false); + }; + + return ( +
+ + + + setShowPicker(false)} + > + + + + + + + +
+ ); +}; + +export default EmojiPickerButton; diff --git a/src/modals/AddOperationModal/AddOperationModal.jsx b/frontend/src/modals/AddOperationModal/AddOperationModal.jsx similarity index 100% rename from src/modals/AddOperationModal/AddOperationModal.jsx rename to frontend/src/modals/AddOperationModal/AddOperationModal.jsx diff --git a/src/modals/AnalyticsModal/AnalyticsModal.jsx b/frontend/src/modals/AnalyticsModal/AnalyticsModal.jsx similarity index 98% rename from src/modals/AnalyticsModal/AnalyticsModal.jsx rename to frontend/src/modals/AnalyticsModal/AnalyticsModal.jsx index f93193d..c0eae45 100644 --- a/src/modals/AnalyticsModal/AnalyticsModal.jsx +++ b/frontend/src/modals/AnalyticsModal/AnalyticsModal.jsx @@ -145,7 +145,7 @@ const AnalyticsModal = () => { - +
diff --git a/src/modals/AnalyticsModal/AnalyticsModal.module.css b/frontend/src/modals/AnalyticsModal/AnalyticsModal.module.css similarity index 100% rename from src/modals/AnalyticsModal/AnalyticsModal.module.css rename to frontend/src/modals/AnalyticsModal/AnalyticsModal.module.css diff --git a/frontend/src/modals/CategoriesModal/CategoriesModal.jsx b/frontend/src/modals/CategoriesModal/CategoriesModal.jsx new file mode 100644 index 0000000..1e3ef48 --- /dev/null +++ b/frontend/src/modals/CategoriesModal/CategoriesModal.jsx @@ -0,0 +1,148 @@ +import { useEffect, useState, useMemo, useCallback } from "react"; +import { useTheme } from "../../contexts/ThemeContext"; +import { + Modal, + Button, + Form, + InputGroup, + Tabs, + Tab, + Dropdown, + DropdownButton, +} from "react-bootstrap"; +import styles from "./CategoriesModal.module.css"; +import CategorySign from "./components/CategorySign/CategorySign"; +import AddCategoryModal from "../../modals/AddCategoryModal/AddCategoryModal"; +import fetchCategories from "../../logic/category/GetAllCategories"; + +const CategoriesModal = ({ variant }) => { + let [expenseCategories, setExpenseCategories] = useState([]); + const [incomeCategories, setIncomeCategories] = useState([]); + const { theme } = useTheme(); + const [show, setShow] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const handleClose = () => setShow(false); + const handleShow = () => setShow(true); + useEffect(() => { + fetchCategories().then((data) => { + setExpenseCategories( + data.filter((category) => category.type === "expense"), + ); + setIncomeCategories( + data.filter((category) => category.type === "income"), + ); + console.log(data); + }); + }, []); + const filteredExpenseCategories = useMemo(() => { + if (!searchTerm) return expenseCategories; + const term = searchTerm.toLowerCase(); + return expenseCategories.filter((category) => + category.title.toLowerCase().includes(term), + ); + }, [expenseCategories, searchTerm]); + const filteredIncomeCategories = useMemo(() => { + if (!searchTerm) return incomeCategories; + const term = searchTerm.toLowerCase(); + return incomeCategories.filter((category) => + category.title.toLowerCase().includes(term), + ); + }, [incomeCategories, searchTerm]); + const handleSearch = useCallback((e) => { + setSearchTerm(e.target.value); + }, []); + return ( + <> + + + +
+
+ + Categories +
+ + + + + Reorder + +
+ + +
☌
+
+ +
+
+
+ + + +
+ {filteredIncomeCategories.map((category) => ( + + ))} +
+
+ +
+ {filteredExpenseCategories.map((category) => ( + + ))} +
+
+
+
+
+ + ); +}; +export default CategoriesModal; diff --git a/frontend/src/modals/CategoriesModal/CategoriesModal.module.css b/frontend/src/modals/CategoriesModal/CategoriesModal.module.css new file mode 100644 index 0000000..8626f1c --- /dev/null +++ b/frontend/src/modals/CategoriesModal/CategoriesModal.module.css @@ -0,0 +1,11 @@ +.scrollableContent { + height: calc(100vh - 220px); + bottom: 0; + overflow-y: auto; +} + +@media (max-width: 768px) { + .scrollableContent { + height: calc(100vh - 280px); + } +} diff --git a/frontend/src/modals/CategoriesModal/components/CategorySign/CategorySign.jsx b/frontend/src/modals/CategoriesModal/components/CategorySign/CategorySign.jsx new file mode 100644 index 0000000..0a4361a --- /dev/null +++ b/frontend/src/modals/CategoriesModal/components/CategorySign/CategorySign.jsx @@ -0,0 +1,66 @@ +import { useTheme } from "../../../../contexts/ThemeContext"; +import { Button } from "react-bootstrap"; +import styles from "./CategorySign.module.css"; +import classNames from "classnames"; + +const CategorySign = ({ icon, title, subcategories }) => { + const { theme } = useTheme(); + return ( + + ); +}; + +export default CategorySign; diff --git a/frontend/src/modals/CategoriesModal/components/CategorySign/CategorySign.module.css b/frontend/src/modals/CategoriesModal/components/CategorySign/CategorySign.module.css new file mode 100644 index 0000000..f199a05 --- /dev/null +++ b/frontend/src/modals/CategoriesModal/components/CategorySign/CategorySign.module.css @@ -0,0 +1,7 @@ +.hideScrollbar { + scrollbar-width: none; + -ms-overflow-style: none; +} +.hideScrollbar::-webkit-scrollbar { + display: none; +} diff --git a/frontend/src/modals/HistoryModal/HistoryModal.jsx b/frontend/src/modals/HistoryModal/HistoryModal.jsx new file mode 100644 index 0000000..c2a7a85 --- /dev/null +++ b/frontend/src/modals/HistoryModal/HistoryModal.jsx @@ -0,0 +1,186 @@ +import { useState, useEffect } from "react"; +import { useTheme } from "../../contexts/ThemeContext"; +import { ListGroup, Tabs, Tab, Modal, Button } from "react-bootstrap"; +import styles from "./HistoryModal.module.css"; +import AddOperationModal from "../AddOperationModal/AddOperationModal"; +import GetAllOperation from "../../logic/operation/GetAllOperation"; +import OperationSign from "./components/OperationSign/OperationSign"; +import DayTitle from "../../components/DayTitle"; +import { + processMonthlyOperations, + processOperations, + processWeeklyOperations, + processYearlyOperations, +} from "./logic/groupOperations"; +import { formatWeekRange } from "./logic/week"; + +const HistoryModal = () => { + const { theme } = useTheme(); + const [show, setShow] = useState(false); + const handleClose = () => setShow(false); + const handleShow = () => setShow(true); + const [grouped, setGrouped] = useState({}); + const [sortedGroupKeys, setSortedGroupKeys] = useState([]); + const [groupedWeek, setGroupedWeek] = useState({}); + const [sortedGroupKeysWeek, setSortedGroupKeysWeek] = useState([]); + const [groupedMonth, setGroupedMonth] = useState({}); + const [sortedGroupKeysMonth, setSortedGroupKeysMonth] = useState([]); + const [groupedYear, setGroupedYear] = useState({}); + const [sortedGroupKeysYear, setSortedGroupKeysYear] = useState([]); + useEffect(() => { + GetAllOperation().then((data) => { + console.log(data); + const processop = processOperations(data); + setGrouped(processop.grouped); + setSortedGroupKeys(processop.sortedGroupKeys); + console.log(processop, processOperations(data)); + const processopWeek = processWeeklyOperations(data); + setGroupedWeek(processopWeek.grouped); + setSortedGroupKeysWeek(processopWeek.sortedKeys); + console.log(processopWeek, processWeeklyOperations(data)); + const processopMonth = processMonthlyOperations(data); + setGroupedMonth(processopMonth.grouped); + setSortedGroupKeysMonth(processopMonth.sortedKeys); + console.log(processopMonth, processMonthlyOperations(data)); + const processopYear = processYearlyOperations(data); + setGroupedYear(processopYear.grouped); + setSortedGroupKeysYear(processopYear.sortedKeys); + console.log(processopYear, processYearlyOperations(data)); + }); + }, []); + return ( + <> + + + + + + History + + {/* */} + + + + +
+ {sortedGroupKeys.map((dateKey) => { + const group = grouped[dateKey]; + return ( +
+ + + {group.operations.map((operation) => ( + + + + ))} + +
+ ); + })} +
+
+ +
+ {sortedGroupKeysWeek.map((weekKey) => { + const group = groupedWeek[weekKey]; + return ( +
+ + + {group.operations.map((operation) => ( + + + + ))} + +
+ ); + })} +
+
+ +
+ {sortedGroupKeysMonth.map((monthKey) => { + const group = groupedMonth[monthKey]; + return ( +
+ + + {group.operations.map((operation) => ( + + + + ))} + +
+ ); + })} +
+
+ +
+ {sortedGroupKeysYear.map((yearKey) => { + const group = groupedYear[yearKey]; + return ( +
+ + + {group.operations.map((operation) => ( + + + + ))} + +
+ ); + })} +
+
+
+
+
+ + ); +}; +export default HistoryModal; diff --git a/src/modals/HistoryModal/HistoryModal.module.css b/frontend/src/modals/HistoryModal/HistoryModal.module.css similarity index 100% rename from src/modals/HistoryModal/HistoryModal.module.css rename to frontend/src/modals/HistoryModal/HistoryModal.module.css diff --git a/frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.jsx b/frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.jsx new file mode 100644 index 0000000..330ad3f --- /dev/null +++ b/frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.jsx @@ -0,0 +1,60 @@ +import getDateString from "../../../../logic/getDateString"; +// import { Button } from "react-bootstrap"; +// import classNames from "classnames"; +// import styles from "./OperationSign.module.css"; + +const OperationSign = ({ operation }) => { + return ( +
+
+
+
😀
+
+
+

+ {operation.type === "income" ? "" : "-"} + {operation.amount} ₽ +

+
+ {operation.category} +
+ {!operation.subcategory ? ( + "" + ) : ( +
+ {operation.subcategory.title} +
+ )} +
+ {operation.note && operation.note !== "" + ? operation.note + : "No note"} +
+
+
+
+ {getDateString(operation.date)} +
+
+
+
+ ); +}; + +export default OperationSign; diff --git a/frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.module.css b/frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.module.css new file mode 100644 index 0000000..f199a05 --- /dev/null +++ b/frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.module.css @@ -0,0 +1,7 @@ +.hideScrollbar { + scrollbar-width: none; + -ms-overflow-style: none; +} +.hideScrollbar::-webkit-scrollbar { + display: none; +} diff --git a/frontend/src/modals/HistoryModal/logic/groupOperations.js b/frontend/src/modals/HistoryModal/logic/groupOperations.js new file mode 100644 index 0000000..28bdc5d --- /dev/null +++ b/frontend/src/modals/HistoryModal/logic/groupOperations.js @@ -0,0 +1,175 @@ +import { getMonday } from "./week"; + +const groupOperationsByDate = (operations) => { + const groups = {}; + operations.forEach((operation) => { + const date = new Date(operation.date); + + const dateKey = [ + date.getFullYear(), + String(date.getMonth() + 1).padStart(2, "0"), + String(date.getDate()).padStart(2, "0"), + ].join("-"); + + if (!groups[dateKey]) { + groups[dateKey] = { + dateObj: new Date(date.getFullYear(), date.getMonth(), date.getDate()), + operations: [], + income: 0, + expense: 0, + }; + } + groups[dateKey].operations.push(operation); + operation.type === "income" + ? (groups[dateKey].income += operation.amount) + : (groups[dateKey].expense += operation.amount); + }); + return groups; +}; + +const processOperations = (operations) => { + const grouped = groupOperationsByDate(operations); + const sortedGroupKeys = Object.keys(grouped).sort( + (a, b) => new Date(b) - new Date(a), + ); + + sortedGroupKeys.forEach((key) => { + grouped[key].operations.sort((a, b) => new Date(b.date) - new Date(a.date)); + }); + return { grouped, sortedGroupKeys }; +}; + +const groupOperationsByWeek = (operations) => { + const groups = {}; + + operations.forEach((operation) => { + const date = new Date(operation.date); + const monday = getMonday(date); + const weekKey = monday.toISOString().split("T")[0]; // YYYY-MM-DD + + if (!groups[weekKey]) { + groups[weekKey] = { + monday: new Date(monday), // Сохраняем объект Date + operations: [], + income: 0, + expense: 0, + }; + } + + groups[weekKey].operations.push(operation); + operation.type === "income" + ? (groups[weekKey].income += operation.amount) + : (groups[weekKey].expense += operation.amount); + }); + + return groups; +}; + +const processWeeklyOperations = (operations) => { + const grouped = groupOperationsByWeek(operations); + + // Сортировка недель (от новых к старым) + const sortedKeys = Object.keys(grouped).sort( + (a, b) => new Date(b) - new Date(a), + ); + + // Сортировка транзакций внутри недель (от новых к старым) + sortedKeys.forEach((key) => { + grouped[key].operations.sort((a, b) => new Date(b.date) - new Date(a.date)); + }); + + return { grouped, sortedKeys }; +}; + +// Группировка транзакций по месяцу и году +const groupOperationsByMonth = (operations) => { + const groups = {}; + + operations.forEach((operation) => { + const date = new Date(operation.date); + // Формируем ключ в формате "Год-Месяц" (например: "2025-08") + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; + + if (!groups[monthKey]) { + groups[monthKey] = { + monthDate: new Date(date.getFullYear(), date.getMonth(), 1), // Первый день месяца + operations: [], + income: 0, + expense: 0, + }; + } + + groups[monthKey].operations.push(operation); + operation.type === "income" + ? (groups[monthKey].income += operation.amount) + : (groups[monthKey].expense += operation.amount); + }); + + return groups; +}; + +// Обработка и сортировка транзакций +const processMonthlyOperations = (operations) => { + const grouped = groupOperationsByMonth(operations); + + // Сортировка месяцев (от новых к старым) + const sortedKeys = Object.keys(grouped).sort( + (a, b) => new Date(b) - new Date(a), + ); + + // Сортировка транзакций внутри месяцев (от новых к старым) + sortedKeys.forEach((key) => { + grouped[key].operations.sort((a, b) => new Date(b.date) - new Date(a.date)); + }); + + return { grouped, sortedKeys }; +}; + +// Группировка транзакций по году +const groupOperationsByYear = (operations) => { + const groups = {}; + + operations.forEach((operation) => { + const date = new Date(operation.date); + const year = date.getFullYear(); + const yearKey = String(year); + + if (!groups[yearKey]) { + groups[yearKey] = { + year, + operations: [], + income: 0, + expense: 0, + }; + } + + groups[yearKey].operations.push(operation); + operation.type === "income" + ? (groups[yearKey].income += operation.amount) + : (groups[yearKey].expense += operation.amount); + }); + + return groups; +}; + +// Обработка и сортировка транзакций +const processYearlyOperations = (operations) => { + const grouped = groupOperationsByYear(operations); + + // Сортировка годов (от новых к старым) + const sortedKeys = Object.keys(grouped).sort((a, b) => b - a); + + // Сортировка транзакций внутри годов (от новых к старым) + sortedKeys.forEach((key) => { + grouped[key].operations.sort((a, b) => new Date(b.date) - new Date(a.date)); + }); + + return { grouped, sortedKeys }; +}; + +export { + processOperations, + processWeeklyOperations, + processMonthlyOperations, + processYearlyOperations, +}; diff --git a/frontend/src/modals/HistoryModal/logic/week.js b/frontend/src/modals/HistoryModal/logic/week.js new file mode 100644 index 0000000..da67bbd --- /dev/null +++ b/frontend/src/modals/HistoryModal/logic/week.js @@ -0,0 +1,29 @@ +// Получение даты понедельника для любой даты +const getMonday = (date) => { + const d = new Date(date); + const day = d.getDay(); // 0 (воскресенье) - 6 (суббота) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Коррекция для воскресенья + return new Date(d.setDate(diff)); +}; + +// Форматирование диапазона недели +const formatWeekRange = (monday) => { + const start = new Date(monday); + const end = new Date(monday); + end.setDate(start.getDate() + 6); + + const startStr = start.toLocaleDateString("ru-RU", { + day: "numeric", + month: "short", + }); + + const endStr = end.toLocaleDateString("ru-RU", { + day: "numeric", + month: "short", + year: "numeric", + }); + + return `${startStr} - ${endStr}`; +}; + +export { getMonday, formatWeekRange }; diff --git a/src/modals/SettingsModal/SettingsModal.jsx b/frontend/src/modals/SettingsModal/SettingsModal.jsx similarity index 60% rename from src/modals/SettingsModal/SettingsModal.jsx rename to frontend/src/modals/SettingsModal/SettingsModal.jsx index d2ae274..00f346a 100644 --- a/src/modals/SettingsModal/SettingsModal.jsx +++ b/frontend/src/modals/SettingsModal/SettingsModal.jsx @@ -2,6 +2,8 @@ import { useState } from "react"; import { useTheme } from "../../contexts/ThemeContext"; import { ListGroup, Modal, Button } from "react-bootstrap"; import ThemeToggle from "../../components/ThemeToggle"; +import CategoriesModal from "../../modals/CategoriesModal/CategoriesModal"; +import logout from "../../logic/logout"; const SettingsModal = () => { const { theme } = useTheme(); @@ -39,11 +41,36 @@ const SettingsModal = () => { +

+ DATA +

+ + + + + +

+ APPLICATION +

+

+ ACCOUNT +

+ + + + +
diff --git a/src/modals/SettingsModal/SettingsModal.module.css b/frontend/src/modals/SettingsModal/SettingsModal.module.css similarity index 100% rename from src/modals/SettingsModal/SettingsModal.module.css rename to frontend/src/modals/SettingsModal/SettingsModal.module.css diff --git a/src/pages/Home.jsx b/frontend/src/pages/Home.jsx similarity index 100% rename from src/pages/Home.jsx rename to frontend/src/pages/Home.jsx diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000..caf73ac --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,58 @@ +import { useState } from "react"; +import { Modal, Card, Button, Form } from "react-bootstrap"; +import InputTitle from "../components/InputTitle"; +import login from "../logic/login"; + +const Login = () => { + const [show, setShow] = useState(true); + // const handleClose = () => setShow(false); + const [userdata, setUserdata] = useState({ username: "", password: "" }); + return ( + + + Login + + + + +
{ + if (await login(userdata, e)) { + setShow(false); + } + }} + > + + + setUserdata({ ...userdata, username: e.target.value }) + } + className="mb-3" + required + /> + + + setUserdata({ ...userdata, password: e.target.value }) + } + className="mb-3" + required + /> + + +
+
+
+
+ ); +}; + +export default Login; diff --git a/src/pages/NotFound.jsx b/frontend/src/pages/NotFound.jsx similarity index 100% rename from src/pages/NotFound.jsx rename to frontend/src/pages/NotFound.jsx diff --git a/src/reportWebVitals.js b/frontend/src/reportWebVitals.js similarity index 100% rename from src/reportWebVitals.js rename to frontend/src/reportWebVitals.js diff --git a/frontend/src/routes.js b/frontend/src/routes.js new file mode 100644 index 0000000..b68cdac --- /dev/null +++ b/frontend/src/routes.js @@ -0,0 +1,8 @@ +const host = window.location.origin; +const prefix = "/api"; +const routes = { + token: `${host}${prefix}/token`, + categories: `${host}${prefix}/category`, + operations: `${host}${prefix}/operation`, +}; +export default routes; diff --git a/src/setupTests.js b/frontend/src/setupTests.js similarity index 100% rename from src/setupTests.js rename to frontend/src/setupTests.js diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 1f03afe..0000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/modals/HistoryModal/HistoryModal.jsx b/src/modals/HistoryModal/HistoryModal.jsx deleted file mode 100644 index 29ef4de..0000000 --- a/src/modals/HistoryModal/HistoryModal.jsx +++ /dev/null @@ -1,652 +0,0 @@ -import { useState } from "react"; -import { useTheme } from "../../contexts/ThemeContext"; -import { ListGroup, Tabs, Tab, Modal, Button } from "react-bootstrap"; -import styles from "./HistoryModal.module.css"; -import AddOperationModal from "../AddOperationModal/AddOperationModal"; - -const HistoryModal = () => { - const { theme } = useTheme(); - const [show, setShow] = useState(false); - const handleClose = () => setShow(false); - const handleShow = () => setShow(true); - return ( - <> - - - - - - History - - {/* */} - - - - -
-
-
-

- 25 Jul 2025 -

-
-
- Income: 0,00 ₽ -
-
- Expense: 0,00 ₽ -
-
-
-
- - -
-
-
😀
-
-
-

- -63.00 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
- -
-
-
🗼
-
-
-

- 18000 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
- -
-
-
😀
-
-
-

- -63.00 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
-
-
-
-

- 25 Jul 2025 -

-
-
- Income: 0,00 ₽ -
-
- Expense: 0,00 ₽ -
-
-
-
- - -
-
-
😀
-
-
-

- -63.00 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
- -
-
-
🗼
-
-
-

- 18000 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
- -
-
-
😀
-
-
-

- -63.00 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
-
-
-
-

- 25 Jul 2025 -

-
-
- Income: 0,00 ₽ -
-
- Expense: 0,00 ₽ -
-
-
-
- - -
-
-
😀
-
-
-

- -63.00 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
- -
-
-
🗼
-
-
-

- 18000 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
- -
-
-
😀
-
-
-

- -63.00 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
-
-
-
-

- 25 Jul 2025 -

-
-
- Income: 0,00 ₽ -
-
- Expense: 0,00 ₽ -
-
-
-
- - -
-
-
😀
-
-
-

- -63.00 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
- -
-
-
🗼
-
-
-

- 18000 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
- -
-
-
😀
-
-
-

- -63.00 ₽ -

-
- Transport -
-
- Note -
-
-
-
- 09:14 -
-
-
-
-
-
-
- - - -
-
-
- - ); -}; -export default HistoryModal;