diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..bcb332f
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,45 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2021: true },
+ extends: [
+ 'airbnb',
+ 'airbnb-typescript',
+ 'airbnb/hooks',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react/recommended',
+ 'prettier',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs', 'vite.config.ts'],
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ project: './tsconfig.json',
+ },
+ plugins: ['react', '@typescript-eslint', 'prettier'],
+ rules: {
+ 'class-methods-use-this': 'off',
+ '@typescript-eslint/no-explicit-any': 'error',
+ '@typescript-eslint/lines-between-class-members': 'off',
+ '@typescript-eslint/array-type': ['error', { default: 'array' }],
+ '@typescript-eslint/explicit-member-accessibility': [
+ 'error',
+ {
+ accessibility: 'explicit',
+ overrides: {
+ accessors: 'explicit',
+ constructors: 'no-public',
+ methods: 'explicit',
+ properties: 'explicit',
+ parameterProperties: 'explicit',
+ },
+ },
+ ],
+ '@typescript-eslint/explicit-function-return-type': 'error',
+ '@typescript-eslint/no-unnecessary-type-assertion': 'error',
+ '@typescript-eslint/no-non-null-assertion': 'error',
+ '@typescript-eslint/no-inferrable-types': 'off',
+ 'jsx-a11y/click-events-have-key-events': 'off',
+ 'jsx-a11y/no-noninteractive-element-interactions': 'off',
+ },
+};
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..7ac5680
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,32 @@
+#### Description 📑
+
+###### Please include a summary of the changes.
+
+#### Related task 🔍
+
+ID of the task: #TASK_NUMBER - SUBTASK_NUMBER
+
+Link to the task: (Optional)
+
+###### If you have completed the task in one PR, don't include the subtask number
+
+#### Reason for change
+
+###### Please include a reason for changes.
+
+###### If changes related to the task, ignore this.
+
+
+#### Type of change ❔
+
+- [ ] New feature
+- [ ] Bug fix
+- [ ] Refactoring
+- [ ] Other
+
+#### Checklist ✅
+
+- [ ] My code follows the style guidelines of this project
+- [ ] I have performed a self-review of my code
+- [ ] My changes generate no new warnings
+- [ ] New and existing unit tests pass locally with my changes
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..57fd229
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,5 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npm run lint
+npm run format
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..557ddc7
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,2 @@
+*.md
+*.json
\ No newline at end of file
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..70b042d
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "semi": true,
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "singleQuote": true,
+ "useTabs": false,
+ "printWidth": 120
+}
diff --git a/README.md b/README.md
index 8e2b489..e524182 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,61 @@
-# eCommerce-Application
-RS School eCommerce project
+# Project "Tech Hub"
+RS School eCommerce project - is a team task in which the team needs to develop an eCommerce application.
+
+## Deploy - https://techhub-rss.netlify.app/ (till January 27 2024)
+
+### Project description
+It's a web application that allows users to browse, select, add to basket and buy various digital appliances. The main goal of the project is to learn how to use frontend technologies and provide a convenient platform for buying products.
+
+### Project goals
+#### Main goals of the project are:
+* Register and login systems
+* User-friendly interface for browsing and selecting products
+* Filter product by characteristics and categories
+* Cart and promocodes
+* Detailed page of every product
+
+
+### Technology stack
+1. Frontend:
+ * HTML/CSS/TypeScript
+ * React - for user interface creation
+ * Axios - for HTTP request to the server
+2. Backend:
+ * commercetools
+3. Additional instruments and technologies:
+ * Vite - project builder
+ * SASS - CSS framework with additional features
+ * Prettier - automatic code formatting to a single style
+ * ESLint - detecting errors and enforcing a consistent code style
+ * Jest - code testing
+ * Husky - running certain scripts before commits/pushes
+ * Git - for version control and project repository management
+ * GitHub - for hosting the repository
+ * VS Code - code editor
+
+### Project team
+* [Howl](https://github.com/Howl404)
+* [Mikhail Ignatovich](https://github.com/academeg1)
+* [Rashit Safiev](https://github.com/capapa)
+
+### Scripts for running ESLint, Prettier, Jest, and initializing Husky:
+* ESLint - npm run lint to check the code, npm run lint:fix will automatically fix possible errors after the check
+* Prettier - npm run format for automatic formatting of the entire codebase
+* Jest - npm run test to run tests, npm run test:watch runs tests in watch mode, allowing interaction with Jest and restarting tests on code changes
+* Husky - npm run prepare to initialize Husky
+
+### Project Installation and Launch
+1. Clone the project repository to your computer: git clone https://github.com/Howl404/eCommerce-Application.git
+
+2. Install project dependencies with the command: npm install
+
+3. To run the application, execute the command: npm run dev
+
+### Project build
+1. Perform steps 1 and 2 from [Project Installation and Launch](#project-installation-and-launch)
+
+2. Build the project with the command: npm run build
+
+3. Use npm run preview to launch the project
+
+#### Before commits, run the script for [Husky initialization](#scripts-for-running-eslint-prettier-jest-and-initializing-husky)
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6ce8525
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Tech Hub
+
+
+
+
+
+
diff --git a/jest.config.ts b/jest.config.ts
new file mode 100644
index 0000000..9ce255b
--- /dev/null
+++ b/jest.config.ts
@@ -0,0 +1,23 @@
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'jsdom',
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
+ moduleNameMapper: {
+ '^@/(.*)$': '/src/$1',
+ '\\.(scss|css)$': '/src/styleMock.ts',
+ '\\.(png|svg)$': '/src/assetsMock.ts',
+ '^@src/(.*)$': '/src/$1',
+ '^@components/(.*)$': '/src/components/$1',
+ '^@pages/(.*)$': '/src/pages/$1',
+ '^@services/(.*)$': '/src/services/$1',
+ '^@interfaces/(.*)$': '/src/interfaces/$1',
+ '^@assets/(.*)$': '/src/assets/$1',
+ },
+ transform: {
+ '^.+\\.(ts|tsx)$': 'ts-jest',
+ },
+ testMatch: ['**/__tests__/**/*.(ts|tsx|js)'],
+ collectCoverageFrom: ['src/**/*.(ts|tsx)'],
+};
+
+export {};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..c0e2d3d
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,7704 @@
+{
+ "name": "ecommerce-application",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ecommerce-application",
+ "version": "0.0.0",
+ "dependencies": {
+ "@types/js-cookie": "^3.0.3",
+ "axios": "^1.4.0",
+ "js-cookie": "^3.0.5",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-icons": "^4.10.1",
+ "react-paginate": "^8.2.0",
+ "react-router-dom": "^6.14.2",
+ "react-slider": "^2.0.6",
+ "react-spinners": "^0.13.8",
+ "swiper": "^10.2.0",
+ "toastify-js": "^1.12.0"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.1.2",
+ "@testing-library/react": "^14.0.0",
+ "@types/jest": "^29.5.3",
+ "@types/react": "^18.2.15",
+ "@types/react-dom": "^18.2.7",
+ "@types/react-slider": "^1.3.1",
+ "@types/toastify-js": "^1.12.0",
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
+ "@typescript-eslint/parser": "^6.0.0",
+ "@vitejs/plugin-react": "^4.0.0",
+ "axios-mock-adapter": "^1.21.5",
+ "eslint": "^8.2.0",
+ "eslint-config-airbnb": "^19.0.4",
+ "eslint-config-airbnb-typescript": "^17.1.0",
+ "eslint-config-prettier": "^8.9.0",
+ "eslint-plugin-import": "^2.25.3",
+ "eslint-plugin-jsx-a11y": "^6.5.1",
+ "eslint-plugin-prettier": "^5.0.0",
+ "eslint-plugin-react": "^7.28.0",
+ "eslint-plugin-react-hooks": "^4.3.0",
+ "eslint-plugin-react-refresh": "^0.4.3",
+ "husky": "^8.0.0",
+ "jest": "^29.6.2",
+ "jest-environment-jsdom": "^29.6.2",
+ "prettier": "^3.0.0",
+ "resize-observer-polyfill": "^1.5.1",
+ "sass": "^1.64.2",
+ "ts-jest": "^29.1.1",
+ "ts-node": "^10.9.1",
+ "typescript": "^5.0.2",
+ "vite": "^4.4.5"
+ }
+ },
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz",
+ "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==",
+ "dev": true
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/highlight": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.22.9",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.22.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.22.5",
+ "@babel/generator": "^7.22.9",
+ "@babel/helper-compilation-targets": "^7.22.9",
+ "@babel/helper-module-transforms": "^7.22.9",
+ "@babel/helpers": "^7.22.6",
+ "@babel/parser": "^7.22.7",
+ "@babel/template": "^7.22.5",
+ "@babel/traverse": "^7.22.8",
+ "@babel/types": "^7.22.5",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.2",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/core/node_modules/json5": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.22.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "@jridgewell/trace-mapping": "^0.3.17",
+ "jsesc": "^2.5.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.22.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.22.9",
+ "@babel/helper-validator-option": "^7.22.5",
+ "browserslist": "^4.21.9",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@babel/helper-environment-visitor": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-function-name": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.22.5",
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-hoist-variables": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.22.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-module-imports": "^7.22.5",
+ "@babel/helper-simple-access": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/helper-validator-identifier": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-simple-access": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.22.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.22.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.22.5",
+ "@babel/traverse": "^7.22.6",
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.5",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.22.7",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-bigint": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-properties": {
+ "version": "7.12.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+ "version": "7.10.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "version": "7.14.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz",
+ "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz",
+ "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.22.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.13.11"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/runtime-corejs3": {
+ "version": "7.22.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-js-pure": "^3.30.2",
+ "regenerator-runtime": "^0.13.11"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.22.5",
+ "@babel/parser": "^7.22.5",
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.22.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.22.5",
+ "@babel/generator": "^7.22.7",
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.22.7",
+ "@babel/types": "^7.22.5",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/globals": {
+ "version": "11.12.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.5",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.18.17",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.4.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.6.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.0",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
+ "version": "1.0.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/console": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.6.2",
+ "jest-util": "^29.6.2",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.6.2",
+ "@jest/reporters": "^29.6.2",
+ "@jest/test-result": "^29.6.2",
+ "@jest/transform": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-changed-files": "^29.5.0",
+ "jest-config": "^29.6.2",
+ "jest-haste-map": "^29.6.2",
+ "jest-message-util": "^29.6.2",
+ "jest-regex-util": "^29.4.3",
+ "jest-resolve": "^29.6.2",
+ "jest-resolve-dependencies": "^29.6.2",
+ "jest-runner": "^29.6.2",
+ "jest-runtime": "^29.6.2",
+ "jest-snapshot": "^29.6.2",
+ "jest-util": "^29.6.2",
+ "jest-validate": "^29.6.2",
+ "jest-watcher": "^29.6.2",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.6.2",
+ "slash": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/environment": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "@types/node": "*",
+ "jest-mock": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/expect": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "^29.6.2",
+ "jest-snapshot": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/expect-utils": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.4.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/fake-timers": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.1",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@types/node": "*",
+ "jest-message-util": "^29.6.2",
+ "jest-mock": "^29.6.2",
+ "jest-util": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/globals": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.6.2",
+ "@jest/expect": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "jest-mock": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "^29.6.2",
+ "@jest/test-result": "^29.6.2",
+ "@jest/transform": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "exit": "^0.1.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^5.1.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-message-util": "^29.6.2",
+ "jest-util": "^29.6.2",
+ "jest-worker": "^29.6.2",
+ "slash": "^3.0.0",
+ "string-length": "^4.0.1",
+ "strip-ansi": "^6.0.0",
+ "v8-to-istanbul": "^9.0.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "29.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/source-map": {
+ "version": "29.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-result": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "^29.6.2",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.6.2",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/transform": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^29.6.1",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.6.2",
+ "jest-regex-util": "^29.4.3",
+ "jest-util": "^29.6.2",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/types": {
+ "version": "29.6.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.0",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.18",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "3.1.0",
+ "@jridgewell/sourcemap-codec": "1.4.14"
+ }
+ },
+ "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pkgr/utils": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "fast-glob": "^3.3.0",
+ "is-glob": "^4.0.3",
+ "open": "^9.1.0",
+ "picocolors": "^1.0.0",
+ "tslib": "^2.6.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.8.0.tgz",
+ "integrity": "sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/@swc/core": {
+ "version": "1.3.73",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.3.73",
+ "@swc/core-darwin-x64": "1.3.73",
+ "@swc/core-linux-arm-gnueabihf": "1.3.73",
+ "@swc/core-linux-arm64-gnu": "1.3.73",
+ "@swc/core-linux-arm64-musl": "1.3.73",
+ "@swc/core-linux-x64-gnu": "1.3.73",
+ "@swc/core-linux-x64-musl": "1.3.73",
+ "@swc/core-win32-arm64-msvc": "1.3.73",
+ "@swc/core-win32-ia32-msvc": "1.3.73",
+ "@swc/core-win32-x64-msvc": "1.3.73"
+ },
+ "peerDependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.3.73",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz",
+ "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.1.3",
+ "chalk": "^4.1.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@testing-library/dom/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==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/aria-query": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
+ "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
+ "dev": true,
+ "dependencies": {
+ "deep-equal": "^2.0.5"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.1.2.tgz",
+ "integrity": "sha512-NP9jl1Q2qDDtx+cqogowtQtmgD2OVs37iMSIsTv5eN5ETRkf26Kj6ugVwA93/gZzzFWQAsgkKkcftDe91BJCkQ==",
+ "dev": true,
+ "dependencies": {
+ "@adobe/css-tools": "^4.3.0",
+ "@babel/runtime": "^7.9.2",
+ "aria-query": "^5.0.0",
+ "chalk": "^3.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.5.6",
+ "lodash": "^4.17.15",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ },
+ "peerDependencies": {
+ "@jest/globals": ">= 28",
+ "@types/jest": ">= 28",
+ "jest": ">= 28",
+ "vitest": ">= 0.32"
+ },
+ "peerDependenciesMeta": {
+ "@jest/globals": {
+ "optional": true
+ },
+ "@types/jest": {
+ "optional": true
+ },
+ "jest": {
+ "optional": true
+ },
+ "vitest": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/jest-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==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@testing-library/react": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz",
+ "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@testing-library/dom": "^9.0.0",
+ "@types/react-dom": "^18.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
+ "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==",
+ "dev": true
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/graceful-fs": {
+ "version": "4.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "node_modules/@types/jest": {
+ "version": "29.5.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "^29.0.0",
+ "pretty-format": "^29.0.0"
+ }
+ },
+ "node_modules/@types/js-cookie": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz",
+ "integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww=="
+ },
+ "node_modules/@types/jsdom": {
+ "version": "20.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "parse5": "^7.0.0"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.12",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.4.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.2.18",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-slider": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@types/react-slider/-/react-slider-1.3.1.tgz",
+ "integrity": "sha512-4X2yK7RyCIy643YCFL+bc6XNmcnBtt8n88uuyihvcn5G7Lut23eNQU3q3KmwF7MWIfKfsW5NxCjw0SeDZRtgaA==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/semver": {
+ "version": "7.5.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/toastify-js": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/@types/toastify-js/-/toastify-js-1.12.0.tgz",
+ "integrity": "sha512-fqpDHaKhFukN9KRm24bbH0wozvHmSwjvkaLjBUrWcSfSS4zysIwTYqNLG3XbSNhRlsTNRNLGS23tp/VhPwsfHQ==",
+ "dev": true
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/yargs": {
+ "version": "17.0.24",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "6.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.2.1",
+ "@typescript-eslint/type-utils": "6.2.1",
+ "@typescript-eslint/utils": "6.2.1",
+ "@typescript-eslint/visitor-keys": "6.2.1",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "natural-compare-lite": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "6.2.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "6.2.1",
+ "@typescript-eslint/types": "6.2.1",
+ "@typescript-eslint/typescript-estree": "6.2.1",
+ "@typescript-eslint/visitor-keys": "6.2.1",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "6.2.1",
+ "@typescript-eslint/visitor-keys": "6.2.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "6.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "6.2.1",
+ "@typescript-eslint/utils": "6.2.1",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "6.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.2.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "6.2.1",
+ "@typescript-eslint/visitor-keys": "6.2.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "6.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.2.1",
+ "@typescript-eslint/types": "6.2.1",
+ "@typescript-eslint/typescript-estree": "6.2.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "6.2.1",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.4.tgz",
+ "integrity": "sha512-7wU921ABnNYkETiMaZy7XqpueMnpu5VxvVps13MjmCo+utBdD79sZzrApHawHtVX66cCJQQTXFcjH0y9dSUK8g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.22.9",
+ "@babel/plugin-transform-react-jsx-self": "^7.22.5",
+ "@babel/plugin-transform-react-jsx-source": "^7.22.5",
+ "react-refresh": "^0.14.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0"
+ }
+ },
+ "node_modules/abab": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/acorn": {
+ "version": "8.10.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-globals": {
+ "version": "7.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.1.0",
+ "acorn-walk": "^8.0.2"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-query": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.10.2",
+ "@babel/runtime-corejs3": "^7.10.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-array-buffer": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "get-intrinsic": "^1.2.1",
+ "is-array-buffer": "^3.0.2",
+ "is-shared-array-buffer": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ast-types-flow": {
+ "version": "0.0.7",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "license": "MIT"
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.7.2",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.4.0",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios-mock-adapter": {
+ "version": "1.21.5",
+ "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.5.tgz",
+ "integrity": "sha512-5NI1V/VK+8+JeTF8niqOowuysA4b8mGzdlMN/QnTnoXbYh4HZSNiopsDclN2g/m85+G++IrEtUdZaQ3GnaMsSA==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "is-buffer": "^2.0.5"
+ },
+ "peerDependencies": {
+ "axios": ">= 0.17.0"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/babel-jest": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/transform": "^29.6.2",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^29.5.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "version": "6.1.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-jest-hoist": {
+ "version": "29.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-preset-current-node-syntax": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.8.3",
+ "@babel/plugin-syntax-import-meta": "^7.8.3",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.8.3",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-top-level-await": "^7.8.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-preset-jest": {
+ "version": "29.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^29.5.0",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/big-integer": {
+ "version": "1.6.51",
+ "dev": true,
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bplist-parser": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "big-integer": "^1.6.44"
+ },
+ "engines": {
+ "node": ">= 5.10.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.21.10",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001517",
+ "electron-to-chromium": "^1.4.477",
+ "node-releases": "^2.0.13",
+ "update-browserslist-db": "^1.0.11"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bs-logger": {
+ "version": "0.2.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-json-stable-stringify": "2.x"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/bser": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bundle-name": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001518",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/char-regex": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "3.8.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/collect-v8-coverage": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/confusing-browser-globals": {
+ "version": "1.0.11",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/core-js-pure": {
+ "version": "3.32.0",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true
+ },
+ "node_modules/cssom": {
+ "version": "0.5.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssom": "~0.3.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cssstyle/node_modules/cssom": {
+ "version": "0.3.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/data-urls": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.4.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dedent": {
+ "version": "1.5.1",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-equal": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz",
+ "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.2",
+ "es-get-iterator": "^1.1.3",
+ "get-intrinsic": "^1.2.1",
+ "is-arguments": "^1.1.1",
+ "is-array-buffer": "^3.0.2",
+ "is-date-object": "^1.0.5",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "isarray": "^2.0.5",
+ "object-is": "^1.1.5",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.5.0",
+ "side-channel": "^1.0.4",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/default-browser": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^3.0.0",
+ "default-browser-id": "^3.0.0",
+ "execa": "^7.1.1",
+ "titleize": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bplist-parser": "^0.2.0",
+ "untildify": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/diff-sequences": {
+ "version": "29.4.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true
+ },
+ "node_modules/domexception": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.479",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emittery": {
+ "version": "0.13.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/enquirer": {
+ "version": "2.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.1",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.22.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "arraybuffer.prototype.slice": "^1.0.1",
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.1",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.2.1",
+ "get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "is-array-buffer": "^3.0.2",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.10",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.5.0",
+ "safe-array-concat": "^1.0.0",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trim": "^1.2.7",
+ "string.prototype.trimend": "^1.0.6",
+ "string.prototype.trimstart": "^1.0.6",
+ "typed-array-buffer": "^1.0.0",
+ "typed-array-byte-length": "^1.0.0",
+ "typed-array-byte-offset": "^1.0.0",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-get-iterator": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
+ "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "is-arguments": "^1.1.1",
+ "is-map": "^2.0.2",
+ "is-set": "^2.0.2",
+ "is-string": "^1.0.7",
+ "isarray": "^2.0.5",
+ "stop-iteration-iterator": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has": "^1.0.3"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.18.17",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.18.17",
+ "@esbuild/android-arm64": "0.18.17",
+ "@esbuild/android-x64": "0.18.17",
+ "@esbuild/darwin-arm64": "0.18.17",
+ "@esbuild/darwin-x64": "0.18.17",
+ "@esbuild/freebsd-arm64": "0.18.17",
+ "@esbuild/freebsd-x64": "0.18.17",
+ "@esbuild/linux-arm": "0.18.17",
+ "@esbuild/linux-arm64": "0.18.17",
+ "@esbuild/linux-ia32": "0.18.17",
+ "@esbuild/linux-loong64": "0.18.17",
+ "@esbuild/linux-mips64el": "0.18.17",
+ "@esbuild/linux-ppc64": "0.18.17",
+ "@esbuild/linux-riscv64": "0.18.17",
+ "@esbuild/linux-s390x": "0.18.17",
+ "@esbuild/linux-x64": "0.18.17",
+ "@esbuild/netbsd-x64": "0.18.17",
+ "@esbuild/openbsd-x64": "0.18.17",
+ "@esbuild/sunos-x64": "0.18.17",
+ "@esbuild/win32-arm64": "0.18.17",
+ "@esbuild/win32-ia32": "0.18.17",
+ "@esbuild/win32-x64": "0.18.17"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint/eslintrc": "^1.0.4",
+ "@humanwhocodes/config-array": "^0.6.0",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "enquirer": "^2.3.5",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^6.0.0",
+ "eslint-utils": "^3.0.0",
+ "eslint-visitor-keys": "^3.0.0",
+ "espree": "^9.0.0",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^6.0.1",
+ "globals": "^13.6.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "progress": "^2.0.0",
+ "regexpp": "^3.2.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-airbnb": {
+ "version": "19.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-config-airbnb-base": "^15.0.0",
+ "object.assign": "^4.1.2",
+ "object.entries": "^1.1.5"
+ },
+ "engines": {
+ "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.32.0 || ^8.2.0",
+ "eslint-plugin-import": "^2.25.3",
+ "eslint-plugin-jsx-a11y": "^6.5.1",
+ "eslint-plugin-react": "^7.28.0",
+ "eslint-plugin-react-hooks": "^4.3.0"
+ }
+ },
+ "node_modules/eslint-config-airbnb-base": {
+ "version": "15.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "confusing-browser-globals": "^1.0.10",
+ "object.assign": "^4.1.2",
+ "object.entries": "^1.1.5",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.32.0 || ^8.2.0",
+ "eslint-plugin-import": "^2.25.2"
+ }
+ },
+ "node_modules/eslint-config-airbnb-base/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-config-airbnb-typescript": {
+ "version": "17.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-config-airbnb-base": "^15.0.0"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/eslint-plugin": "^5.13.0 || ^6.0.0",
+ "@typescript-eslint/parser": "^5.0.0 || ^6.0.0",
+ "eslint": "^7.32.0 || ^8.2.0",
+ "eslint-plugin-import": "^2.25.3"
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "8.9.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.11.0",
+ "resolve": "^1.22.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.8.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.25.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.4",
+ "array.prototype.flat": "^1.2.5",
+ "debug": "^2.6.9",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-module-utils": "^2.7.1",
+ "has": "^1.0.3",
+ "is-core-module": "^2.8.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.0.4",
+ "object.values": "^1.1.5",
+ "resolve": "^1.20.0",
+ "tsconfig-paths": "^3.11.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "2.6.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/ms": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.5.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.16.3",
+ "aria-query": "^4.2.2",
+ "array-includes": "^3.1.4",
+ "ast-types-flow": "^0.0.7",
+ "axe-core": "^4.3.5",
+ "axobject-query": "^2.2.0",
+ "damerau-levenshtein": "^1.0.7",
+ "emoji-regex": "^9.2.2",
+ "has": "^1.0.3",
+ "jsx-ast-utils": "^3.2.1",
+ "language-tags": "^1.0.5",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.8.5"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.28.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.4",
+ "array.prototype.flatmap": "^1.2.5",
+ "doctrine": "^2.1.0",
+ "estraverse": "^5.3.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.0.4",
+ "object.entries": "^1.1.5",
+ "object.fromentries": "^2.0.5",
+ "object.hasown": "^1.1.0",
+ "object.values": "^1.1.5",
+ "prop-types": "^15.7.2",
+ "resolve": "^2.0.0-next.3",
+ "semver": "^6.3.0",
+ "string.prototype.matchall": "^4.0.6"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.3",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=7"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/doctrine": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ignore": {
+ "version": "4.0.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.1",
+ "human-signals": "^4.3.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^3.0.7",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expect": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "^29.6.2",
+ "@types/node": "*",
+ "jest-get-type": "^29.4.3",
+ "jest-matcher-utils": "^29.6.2",
+ "jest-message-util": "^29.6.2",
+ "jest-util": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fb-watchman": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bser": "2.1.1"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.2",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0",
+ "functions-have-names": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functional-red-black-tree": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.20.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "4.3.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/husky": {
+ "version": "8.0.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "husky": "lib/bin.js"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.2.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "4.3.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-arguments": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+ "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-buffer": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+ "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.12.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-fn": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
+ "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
+ "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.11"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
+ "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz",
+ "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-wsl/node_modules/is-docker": {
+ "version": "2.2.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "5.2.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.6",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "import-local": "^3.0.2",
+ "jest-cli": "^29.6.2"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-changed-files": {
+ "version": "29.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^5.0.0",
+ "p-limit": "^3.1.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/execa": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/human-signals": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/is-stream": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/onetime": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-changed-files/node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jest-circus": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.6.2",
+ "@jest/expect": "^29.6.2",
+ "@jest/test-result": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "dedent": "^1.0.0",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^29.6.2",
+ "jest-matcher-utils": "^29.6.2",
+ "jest-message-util": "^29.6.2",
+ "jest-runtime": "^29.6.2",
+ "jest-snapshot": "^29.6.2",
+ "jest-util": "^29.6.2",
+ "p-limit": "^3.1.0",
+ "pretty-format": "^29.6.2",
+ "pure-rand": "^6.0.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "^29.6.2",
+ "@jest/test-result": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "chalk": "^4.0.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "import-local": "^3.0.2",
+ "jest-config": "^29.6.2",
+ "jest-util": "^29.6.2",
+ "jest-validate": "^29.6.2",
+ "prompts": "^2.0.1",
+ "yargs": "^17.3.1"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-config": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/test-sequencer": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "babel-jest": "^29.6.2",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "deepmerge": "^4.2.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-circus": "^29.6.2",
+ "jest-environment-node": "^29.6.2",
+ "jest-get-type": "^29.4.3",
+ "jest-regex-util": "^29.4.3",
+ "jest-resolve": "^29.6.2",
+ "jest-runner": "^29.6.2",
+ "jest-util": "^29.6.2",
+ "jest-validate": "^29.6.2",
+ "micromatch": "^4.0.4",
+ "parse-json": "^5.2.0",
+ "pretty-format": "^29.6.2",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-diff": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.4.3",
+ "jest-get-type": "^29.4.3",
+ "pretty-format": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-docblock": {
+ "version": "29.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-each": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.1",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.4.3",
+ "jest-util": "^29.6.2",
+ "pretty-format": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.6.2",
+ "@jest/fake-timers": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "@types/jsdom": "^20.0.0",
+ "@types/node": "*",
+ "jest-mock": "^29.6.2",
+ "jest-util": "^29.6.2",
+ "jsdom": "^20.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-environment-node": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.6.2",
+ "@jest/fake-timers": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "@types/node": "*",
+ "jest-mock": "^29.6.2",
+ "jest-util": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-get-type": {
+ "version": "29.4.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-haste-map": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.1",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.4.3",
+ "jest-util": "^29.6.2",
+ "jest-worker": "^29.6.2",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-leak-detector": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.4.3",
+ "pretty-format": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.6.2",
+ "jest-get-type": "^29.4.3",
+ "pretty-format": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-message-util": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.1",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.6.2",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-mock": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.1",
+ "@types/node": "*",
+ "jest-util": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-regex-util": {
+ "version": "29.4.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.6.2",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^29.6.2",
+ "jest-validate": "^29.6.2",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^2.0.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-regex-util": "^29.4.3",
+ "jest-snapshot": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.6.2",
+ "@jest/environment": "^29.6.2",
+ "@jest/test-result": "^29.6.2",
+ "@jest/transform": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "emittery": "^0.13.1",
+ "graceful-fs": "^4.2.9",
+ "jest-docblock": "^29.4.3",
+ "jest-environment-node": "^29.6.2",
+ "jest-haste-map": "^29.6.2",
+ "jest-leak-detector": "^29.6.2",
+ "jest-message-util": "^29.6.2",
+ "jest-resolve": "^29.6.2",
+ "jest-runtime": "^29.6.2",
+ "jest-util": "^29.6.2",
+ "jest-watcher": "^29.6.2",
+ "jest-worker": "^29.6.2",
+ "p-limit": "^3.1.0",
+ "source-map-support": "0.5.13"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runtime": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.6.2",
+ "@jest/fake-timers": "^29.6.2",
+ "@jest/globals": "^29.6.2",
+ "@jest/source-map": "^29.6.0",
+ "@jest/test-result": "^29.6.2",
+ "@jest/transform": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.6.2",
+ "jest-message-util": "^29.6.2",
+ "jest-mock": "^29.6.2",
+ "jest-regex-util": "^29.4.3",
+ "jest-resolve": "^29.6.2",
+ "jest-snapshot": "^29.6.2",
+ "jest-util": "^29.6.2",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/strip-bom": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-snapshot": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-jsx": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^29.6.2",
+ "@jest/transform": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^29.6.2",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^29.6.2",
+ "jest-get-type": "^29.4.3",
+ "jest-matcher-utils": "^29.6.2",
+ "jest-message-util": "^29.6.2",
+ "jest-util": "^29.6.2",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^29.6.2",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-util": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-validate": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.1",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.4.3",
+ "leven": "^3.1.0",
+ "pretty-format": "^29.6.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-validate/node_modules/camelcase": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watcher": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "^29.6.2",
+ "@jest/types": "^29.6.1",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "emittery": "^0.13.1",
+ "jest-util": "^29.6.2",
+ "string-length": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.6.2",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "20.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "acorn": "^8.8.1",
+ "acorn-globals": "^7.0.0",
+ "cssom": "^0.5.0",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^3.0.2",
+ "decimal.js": "^10.4.2",
+ "domexception": "^4.0.0",
+ "escodegen": "^2.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.2",
+ "parse5": "^7.1.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.1.2",
+ "w3c-xmlserializer": "^4.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0",
+ "ws": "^8.11.0",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "2.5.2",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.22",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ }
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "node_modules/lodash.memoize": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/makeerror": {
+ "version": "1.0.12",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tmpl": "1.0.5"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/natural-compare-lite": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.13",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-is": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
+ "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.hasown": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/open": {
+ "version": "9.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^4.0.0",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-locate/node_modules/p-limit": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.27",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "29.6.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.0",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/pretty-format/node_modules/react-is": {
+ "version": "18.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "license": "MIT"
+ },
+ "node_modules/psl": {
+ "version": "1.9.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pure-rand": {
+ "version": "6.0.2",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "node_modules/react-icons": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz",
+ "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "license": "MIT"
+ },
+ "node_modules/react-paginate": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-8.2.0.tgz",
+ "integrity": "sha512-sJCz1PW+9PNIjUSn919nlcRVuleN2YPoFBOvL+6TPgrH/3lwphqiSOgdrLafLdyLDxsgK+oSgviqacF4hxsDIw==",
+ "dependencies": {
+ "prop-types": "^15"
+ },
+ "peerDependencies": {
+ "react": "^16 || ^17 || ^18"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
+ "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.15.0.tgz",
+ "integrity": "sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==",
+ "dependencies": {
+ "@remix-run/router": "1.8.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.15.0.tgz",
+ "integrity": "sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==",
+ "dependencies": {
+ "@remix-run/router": "1.8.0",
+ "react-router": "6.15.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/react-slider": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.6.tgz",
+ "integrity": "sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA==",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "react": "^16 || ^17 || ^18"
+ }
+ },
+ "node_modules/react-spinners": {
+ "version": "0.13.8",
+ "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz",
+ "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==",
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "functions-have-names": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpp": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/resize-observer-polyfill": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+ "dev": true
+ },
+ "node_modules/resolve": {
+ "version": "1.22.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.11.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-cwd/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve.exports": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "3.27.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-applescript": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-applescript/node_modules/execa": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/run-applescript/node_modules/human-signals": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/run-applescript/node_modules/is-stream": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-applescript/node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/run-applescript/node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/run-applescript/node_modules/onetime": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-applescript/node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.0",
+ "has-symbols": "^1.0.3",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sass": {
+ "version": "1.64.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.0",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.5.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
+ "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==",
+ "dev": true,
+ "dependencies": {
+ "internal-slot": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string-length": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.3",
+ "regexp.prototype.flags": "^1.4.3",
+ "side-channel": "^1.0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/swiper": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/swiper/-/swiper-10.2.0.tgz",
+ "integrity": "sha512-nktQsOtBInJjr3f5DicxC8eHYGcLXDVIGPSon0QoXRaO6NjKnATCbQ8SZsD3dN1Ph1RH4EhVPwSYCcuDRFWHGQ==",
+ "funding": [
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/swiperjs"
+ },
+ {
+ "type": "open_collective",
+ "url": "http://opencollective.com/swiper"
+ }
+ ],
+ "engines": {
+ "node": ">= 4.7.0"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/synckit": {
+ "version": "0.8.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/utils": "^2.3.1",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/titleize": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/tmpl": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toastify-js": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
+ "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ=="
+ },
+ "node_modules/tough-cookie": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.13.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/ts-jest": {
+ "version": "29.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bs-logger": "0.x",
+ "fast-json-stable-stringify": "2.x",
+ "jest-util": "^29.0.0",
+ "json5": "^2.2.3",
+ "lodash.memoize": "4.x",
+ "make-error": "1.x",
+ "semver": "^7.5.3",
+ "yargs-parser": "^21.0.1"
+ },
+ "bin": {
+ "ts-jest": "cli.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": ">=7.0.0-beta.0 <8",
+ "@jest/types": "^29.0.0",
+ "babel-jest": "^29.0.0",
+ "jest": "^29.0.0",
+ "typescript": ">=4.3 <6"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "@jest/types": {
+ "optional": true
+ },
+ "babel-jest": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ts-jest/node_modules/json5": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.14.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.6.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.1.6",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/untildify": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.0.11",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "node_modules/v8-compile-cache": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-to-istanbul": {
+ "version": "9.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/v8-to-istanbul/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "4.4.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.18.10",
+ "postcss": "^8.4.26",
+ "rollup": "^3.25.2"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/walker": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "makeerror": "1.0.12"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "11.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz",
+ "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==",
+ "dev": true,
+ "dependencies": {
+ "is-map": "^2.0.1",
+ "is-set": "^2.0.1",
+ "is-weakmap": "^2.0.1",
+ "is-weakset": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/write-file-atomic": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.7"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.13.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..cc1244c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,65 @@
+{
+ "name": "ecommerce-application",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx",
+ "lint:fix": "eslint . --ext ts,tsx --fix",
+ "preview": "vite preview",
+ "prepare": "husky install",
+ "format": "prettier . --write",
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "test:coverage": "jest --coverage"
+ },
+ "dependencies": {
+ "@types/js-cookie": "^3.0.3",
+ "axios": "^1.4.0",
+ "js-cookie": "^3.0.5",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-icons": "^4.10.1",
+ "react-paginate": "^8.2.0",
+ "react-router-dom": "^6.14.2",
+ "react-slider": "^2.0.6",
+ "react-spinners": "^0.13.8",
+ "swiper": "^10.2.0",
+ "toastify-js": "^1.12.0"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.1.2",
+ "@testing-library/react": "^14.0.0",
+ "@types/jest": "^29.5.3",
+ "@types/react": "^18.2.15",
+ "@types/react-dom": "^18.2.7",
+ "@types/react-slider": "^1.3.1",
+ "@types/toastify-js": "^1.12.0",
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
+ "@typescript-eslint/parser": "^6.0.0",
+ "@vitejs/plugin-react": "^4.0.0",
+ "axios-mock-adapter": "^1.21.5",
+ "eslint": "^8.2.0",
+ "eslint-config-airbnb": "^19.0.4",
+ "eslint-config-airbnb-typescript": "^17.1.0",
+ "eslint-config-prettier": "^8.9.0",
+ "eslint-plugin-import": "^2.25.3",
+ "eslint-plugin-jsx-a11y": "^6.5.1",
+ "eslint-plugin-prettier": "^5.0.0",
+ "eslint-plugin-react": "^7.28.0",
+ "eslint-plugin-react-hooks": "^4.3.0",
+ "eslint-plugin-react-refresh": "^0.4.3",
+ "husky": "^8.0.0",
+ "jest": "^29.6.2",
+ "jest-environment-jsdom": "^29.6.2",
+ "prettier": "^3.0.0",
+ "resize-observer-polyfill": "^1.5.1",
+ "sass": "^1.64.2",
+ "ts-jest": "^29.1.1",
+ "ts-node": "^10.9.1",
+ "typescript": "^5.0.2",
+ "vite": "^4.4.5"
+ }
+}
diff --git a/src/App.scss b/src/App.scss
new file mode 100644
index 0000000..9a31b37
--- /dev/null
+++ b/src/App.scss
@@ -0,0 +1,6 @@
+.centered-loader {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..730a772
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,112 @@
+import React, { useState, useEffect } from 'react';
+import { Route, Routes, useNavigate } from 'react-router-dom';
+import './App.scss';
+import LoginPage from '@pages/Login/LoginPage';
+import NotFound from '@pages/NotFound/NotFound';
+import RegistrationPage from '@pages/Register/RegistrationPage';
+import Cookies from 'js-cookie';
+import CatalogPage from '@pages/Catalog/CatalogPage';
+import Header from '@components/Header/Header';
+import ProductPage from '@pages/Product/ProductPage';
+import AccountDashboard from '@pages/AccountDashboard/AccountDashboard';
+import { getClientAccessToken, getCustomerId } from '@services/AuthService/AuthService';
+import Home from '@pages/Home/Home';
+import Basket from '@pages/Basket/Basket';
+import ClipLoader from 'react-spinners/ClipLoader';
+import AboutPage from '@pages/About/AboutPage';
+import { getCartByCustomerId } from '@services/CartService/CartService';
+import returnCartPrice from '@src/utilities/returnCartPrice';
+
+function App(): JSX.Element {
+ const [isLoading, setIsLoading] = useState(true);
+ const navigate = useNavigate();
+ const [auth, setIsAuth] = useState(false);
+ const [totalSumInCart, setTotalSumInCart] = useState(0);
+
+ const onLogOut = (): void => {
+ setIsLoading(true);
+ Object.keys(Cookies.get()).forEach((item) => {
+ Cookies.remove(item);
+ });
+ getClientAccessToken().then((result) => {
+ Cookies.set('access-token', result.accessToken, { expires: 2 });
+ Cookies.set('auth-type', 'anon', { expires: 2 });
+ setIsLoading(false);
+ });
+ setTotalSumInCart(0);
+ navigate('/');
+ setIsAuth(false);
+ };
+
+ const checkLogIn = async (): Promise => {
+ setIsLoading(true);
+ if (Cookies.get('auth-type') !== undefined || Cookies.get('auth-type') !== 'anon') setIsAuth(true);
+
+ getCustomerId().then(async (item) => {
+ const token = Cookies.get('access-token');
+ if (token) {
+ try {
+ const result = await getCartByCustomerId(token, item.id);
+ Cookies.set('cart-id', result.id, { expires: 2 });
+
+ const cartPrice = await returnCartPrice();
+ if (cartPrice !== false) {
+ setTotalSumInCart(cartPrice);
+ }
+ } catch (error) {
+ // no cart found
+ }
+ }
+ });
+
+ setIsLoading(false);
+ };
+
+ useEffect(() => {
+ async function fetchData(): Promise {
+ setIsLoading(true);
+ const accessToken = Cookies.get('access-token');
+ const authType = Cookies.get('auth-type');
+ if (authType === 'password') {
+ setIsAuth(true);
+ } else if (!accessToken) {
+ const result = await getClientAccessToken();
+ Cookies.set('access-token', result.accessToken, { expires: 2 });
+ Cookies.set('auth-type', 'anon', { expires: 2 });
+ }
+ const cartPrice = await returnCartPrice();
+ if (cartPrice !== false) {
+ setTotalSumInCart(cartPrice);
+ }
+
+ setIsLoading(false);
+ }
+ fetchData();
+ }, []);
+
+ return isLoading ? (
+
+
+
+ ) : (
+ <>
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ }
+ />
+ } />
+ } />
+
+ >
+ );
+}
+
+export default App;
diff --git a/src/AppContext.ts b/src/AppContext.ts
new file mode 100644
index 0000000..5d591bb
--- /dev/null
+++ b/src/AppContext.ts
@@ -0,0 +1,5 @@
+import React from 'react';
+
+const AppContext = React.createContext(0);
+
+export default AppContext;
diff --git a/src/assets/academeg.png b/src/assets/academeg.png
new file mode 100644
index 0000000..d313217
Binary files /dev/null and b/src/assets/academeg.png differ
diff --git a/src/assets/capapa.jpg b/src/assets/capapa.jpg
new file mode 100644
index 0000000..a5a8ad4
Binary files /dev/null and b/src/assets/capapa.jpg differ
diff --git a/src/assets/cart-plus-solid.svg b/src/assets/cart-plus-solid.svg
new file mode 100644
index 0000000..0307b77
--- /dev/null
+++ b/src/assets/cart-plus-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/cart-shopping-solid.svg b/src/assets/cart-shopping-solid.svg
new file mode 100644
index 0000000..9c479c1
--- /dev/null
+++ b/src/assets/cart-shopping-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/cart.svg b/src/assets/cart.svg
new file mode 100644
index 0000000..30a158d
--- /dev/null
+++ b/src/assets/cart.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/assets/chevron-down-solid.svg b/src/assets/chevron-down-solid.svg
new file mode 100644
index 0000000..b5ea577
--- /dev/null
+++ b/src/assets/chevron-down-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/favicon.ico b/src/assets/favicon.ico
new file mode 100644
index 0000000..c9f9fe5
Binary files /dev/null and b/src/assets/favicon.ico differ
diff --git a/src/assets/howl.png b/src/assets/howl.png
new file mode 100644
index 0000000..dc997e6
Binary files /dev/null and b/src/assets/howl.png differ
diff --git a/src/assets/logo.svg b/src/assets/logo.svg
new file mode 100644
index 0000000..a73dcee
--- /dev/null
+++ b/src/assets/logo.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/assets/react.svg b/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/search.svg b/src/assets/search.svg
new file mode 100644
index 0000000..9513c8d
--- /dev/null
+++ b/src/assets/search.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/assets/vite.svg b/src/assets/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/src/assets/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assetsMock.ts b/src/assetsMock.ts
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/src/assetsMock.ts
@@ -0,0 +1 @@
+export {};
diff --git a/src/components/AboutCard/AboutCard.scss b/src/components/AboutCard/AboutCard.scss
new file mode 100644
index 0000000..81ec0bf
--- /dev/null
+++ b/src/components/AboutCard/AboutCard.scss
@@ -0,0 +1,96 @@
+.about-card,
+.about-card-inverted {
+ display: flex;
+ gap: 60px;
+
+ img {
+ // width: 150px;
+ // height: 225px;
+ max-width: 200px;
+ max-height: 210px;
+ width: 100%;
+ height: 100%;
+ }
+
+ .content__name {
+ margin: 0 0 -5px 0;
+ color: #000;
+ font-family: Oswald;
+ font-size: 20px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ }
+
+ .content__role-github {
+ color: #000;
+ font-family: Oswald;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ gap: 5px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+
+ .role-github__github {
+ font-size: 14px;
+ text-transform: none;
+ text-decoration: none;
+ color: rgba(0, 0, 0, 0.822);
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .content__vertical-line {
+ height: 42px;
+ width: 1px;
+ background: #000;
+ }
+
+ .content__bio,
+ .contributions-container__item {
+ color: #000;
+ text-align: center;
+ font-family: Oswald;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ }
+
+ .content__contributions-container {
+ justify-content: center;
+ margin-top: 10px;
+ display: flex;
+ gap: 10px;
+ .contributions-container__item {
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ padding: 4px;
+ background: #d9d9d9;
+ }
+ }
+}
+
+.about-card-inverted {
+ flex-direction: row-reverse;
+ .content__role-github {
+ justify-content: flex-end;
+ }
+
+ .content__vertical-line {
+ margin-left: calc(100% - 1px);
+ }
+
+ .content__name {
+ text-align: right;
+ }
+}
diff --git a/src/components/AboutCard/AboutCard.tsx b/src/components/AboutCard/AboutCard.tsx
new file mode 100644
index 0000000..75218d0
--- /dev/null
+++ b/src/components/AboutCard/AboutCard.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import './AboutCard.scss';
+
+export default function AboutCard({
+ member,
+}: {
+ member: {
+ image: string;
+ name: string;
+ role: string;
+ github: string;
+ bio: string;
+ contributions: string[];
+ inverted: boolean;
+ };
+}): JSX.Element {
+ return (
+
+
+
+
{member.name}
+ {member.inverted ? (
+
+
+ @{member.github}
+
+
{member.role}
+
+ ) : (
+
+
{member.role}
+
+ @{member.github}
+
+
+ )}
+
+
+
{member.bio}
+
+ {member.contributions.map((contribution) => (
+
+ {contribution}
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/AccountMenu/AccountMenu.module.scss b/src/components/AccountMenu/AccountMenu.module.scss
new file mode 100644
index 0000000..b03a85b
--- /dev/null
+++ b/src/components/AccountMenu/AccountMenu.module.scss
@@ -0,0 +1,27 @@
+.dashboard__menu {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.link {
+ text-decoration: none;
+ color: var(--gray-1);
+}
+
+.btn__dashboard {
+ display: block;
+ width: 288px;
+ height: 44px;
+ text-align: left;
+ font-family: 'Oswald', sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ color: inherit;
+ font-weight: 500;
+}
+
+.btn__dashboard:hover {
+ background: var(--light-blue-active);
+ color: #000;
+}
diff --git a/src/components/AccountMenu/AccountMenu.tsx b/src/components/AccountMenu/AccountMenu.tsx
new file mode 100644
index 0000000..dbbf3ea
--- /dev/null
+++ b/src/components/AccountMenu/AccountMenu.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+import styles from './AccountMenu.module.scss';
+import './style.scss';
+
+function AccountMenu(): JSX.Element {
+ const buttonsData = [
+ { name: 'Account Dashboard', label: 'Account Dashboard', path: '/MyAccount/Profile' },
+ { name: 'Account Information', label: 'Account Information', path: '/MyAccount/Information' },
+ { name: 'Address Book', label: 'Address Book', path: '/MyAccount/Address' },
+ { name: 'My Orders', label: 'My Orders', path: '/MyAccount/Order' },
+ ];
+ // ${name === 'Account Dashboard' ? styles.active_btn : ''}
+ const buttons = buttonsData.map(({ name, label, path }) => (
+
+
+ {label}
+
+
+ ));
+ return {buttons}
;
+}
+
+export default AccountMenu;
diff --git a/src/components/AccountMenu/style.scss b/src/components/AccountMenu/style.scss
new file mode 100644
index 0000000..8c86f97
--- /dev/null
+++ b/src/components/AccountMenu/style.scss
@@ -0,0 +1,14 @@
+:root {
+ --gray-1: #828282;
+ --divider: #c4c4c4;
+ --light-blue-hover: #f0f2f2;
+ --black-2: #3f3f3f;
+ --light-blue-active: #c2c2c2;
+}
+.active {
+ background: var(--light-blue-hover);
+ color: #000;
+ &:hover {
+ background: var(--light-blue-active);
+ }
+}
diff --git a/src/components/BillingAddress/BillingAddress.tsx b/src/components/BillingAddress/BillingAddress.tsx
new file mode 100644
index 0000000..2cb01fc
--- /dev/null
+++ b/src/components/BillingAddress/BillingAddress.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import styles from './BillingAddresss.module.scss';
+
+function BillingAddress({
+ city,
+ country,
+ postalCode,
+ name,
+ streetName,
+}: {
+ city: string;
+ country: string;
+ postalCode: string;
+ name: string;
+ streetName: string;
+}): JSX.Element {
+ return (
+
+
{name}
+
{`${streetName}`}
+
{`${city}`}
+
{`${postalCode}`}
+
{`${country}`}
+
+ );
+}
+
+export default BillingAddress;
diff --git a/src/components/BillingAddress/BillingAddresss.module.scss b/src/components/BillingAddress/BillingAddresss.module.scss
new file mode 100644
index 0000000..54bfc67
--- /dev/null
+++ b/src/components/BillingAddress/BillingAddresss.module.scss
@@ -0,0 +1,7 @@
+div {
+ font-size: inherit;
+ color: inherit;
+ font-family: inherit;
+ font-style: inherit;
+ font-weight: inherit;
+}
diff --git a/src/components/BrandFilter/BrandFilter.scss b/src/components/BrandFilter/BrandFilter.scss
new file mode 100644
index 0000000..f2228d8
--- /dev/null
+++ b/src/components/BrandFilter/BrandFilter.scss
@@ -0,0 +1,20 @@
+.brand-list {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 5px;
+ margin: 5px;
+ font-family: 'Oswald', sans-serif;
+ color: #333;
+
+ .brand-div {
+ width: 170px;
+ label {
+ display: flex;
+ gap: 10px;
+ }
+ }
+}
diff --git a/src/components/BrandFilter/BrandFilter.tsx b/src/components/BrandFilter/BrandFilter.tsx
new file mode 100644
index 0000000..981d1b6
--- /dev/null
+++ b/src/components/BrandFilter/BrandFilter.tsx
@@ -0,0 +1,72 @@
+import { ProductCatalog } from '@src/interfaces/Product';
+import React, { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import './BrandFilter.scss';
+
+function BrandFilter({
+ products,
+ onChange,
+ clearBrand,
+}: {
+ products: ProductCatalog[];
+ onChange: (brands: string) => void;
+ clearBrand: () => void;
+}): JSX.Element {
+ const [selectedBrands, setSelectedBrands] = useState([]);
+
+ const brandCounts: { [key: string]: number } = {};
+
+ products.forEach((product) => {
+ const brand = product.masterVariant.attributes[0].value;
+
+ if (brandCounts[brand]) {
+ brandCounts[brand] += 1;
+ } else {
+ brandCounts[brand] = 1;
+ }
+ });
+
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ setSelectedBrands([]);
+ clearBrand();
+ }, [navigate, clearBrand]);
+
+ const handleBrandChange = (brand: string): void => {
+ if (selectedBrands.includes(brand)) {
+ const updatedBrands = selectedBrands.filter((selectedBrand) => selectedBrand !== brand);
+ onChange('');
+ setSelectedBrands(updatedBrands);
+ } else {
+ onChange(brand);
+ setSelectedBrands([...selectedBrands, brand]);
+ }
+ };
+
+ const brandCountEntries = Object.entries(brandCounts);
+
+ brandCountEntries.sort();
+
+ return (
+
+ {brandCountEntries.map(([brand, count]) => (
+
+
+ handleBrandChange(brand)}
+ disabled={selectedBrands.length > 0 && !selectedBrands.includes(brand)}
+ />
+ {brand} ({count})
+
+
+ ))}
+
+ );
+}
+
+export default BrandFilter;
diff --git a/src/components/Breadcrumb/Breadcrumb.scss b/src/components/Breadcrumb/Breadcrumb.scss
new file mode 100644
index 0000000..7bd58c2
--- /dev/null
+++ b/src/components/Breadcrumb/Breadcrumb.scss
@@ -0,0 +1,8 @@
+.breadcrumb {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 13px;
+ font-weight: normal;
+}
diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx
new file mode 100644
index 0000000..6c5c00f
--- /dev/null
+++ b/src/components/Breadcrumb/Breadcrumb.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import './Breadcrumb.scss';
+
+function Breadcrumb({ breadcrumb }: { breadcrumb: { name: string; slug: string }[] }): JSX.Element {
+ return (
+
+ {breadcrumb.map((product, index) => (
+
+ /
+ item.slug)
+ .join('/')}`}
+ >
+ {product.name}
+
+
+ ))}
+
+ );
+}
+
+export default Breadcrumb;
diff --git a/src/components/Breadcrumb/__tests__/Breadcrumb.test.tsx b/src/components/Breadcrumb/__tests__/Breadcrumb.test.tsx
new file mode 100644
index 0000000..83deb0b
--- /dev/null
+++ b/src/components/Breadcrumb/__tests__/Breadcrumb.test.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import Breadcrumb from '../Breadcrumb';
+import '@testing-library/jest-dom';
+
+// Mock data for testing
+const mockBreadcrumb = [
+ { name: 'Home', slug: 'home' },
+ { name: 'Category 1', slug: 'category1' },
+ { name: 'Subcategory 1', slug: 'subcategory1' },
+];
+
+test('Breadcrumb renders correctly', () => {
+ const { getByText, container } = render(
+
+
+ ,
+ );
+
+ mockBreadcrumb.forEach((item) => {
+ const breadcrumbLink = getByText(item.name);
+ expect(breadcrumbLink).toBeInTheDocument();
+ });
+
+ const separatorSpan = container.querySelector('span');
+
+ expect(separatorSpan).toBeInTheDocument();
+});
+
+test('Breadcrumb link URLs are generated correctly', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ const homeLink = getByText('Home');
+ expect(homeLink).toHaveAttribute('href', '/catalog/home');
+
+ const categoryLink = getByText('Category 1');
+ expect(categoryLink).toHaveAttribute('href', '/catalog/home/category1');
+
+ const subcategoryLink = getByText('Subcategory 1');
+ expect(subcategoryLink).toHaveAttribute('href', '/catalog/home/category1/subcategory1');
+});
diff --git a/src/components/Breadcrumbs/Breadcrumbs.module.scss b/src/components/Breadcrumbs/Breadcrumbs.module.scss
new file mode 100644
index 0000000..9ecb858
--- /dev/null
+++ b/src/components/Breadcrumbs/Breadcrumbs.module.scss
@@ -0,0 +1,20 @@
+.link {
+ text-decoration: none;
+ color: var(--gray-1);
+ &:hover {
+ color: black;
+ }
+}
+
+.crumbs {
+ font-family: 'Roboto', sans-serif;
+ color: var(--gray-1);
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ display: flex;
+ justify-content: center;
+ margin: 13px 0;
+}
diff --git a/src/components/Breadcrumbs/Breadcrumbs.tsx b/src/components/Breadcrumbs/Breadcrumbs.tsx
new file mode 100644
index 0000000..31ca6c8
--- /dev/null
+++ b/src/components/Breadcrumbs/Breadcrumbs.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import styles from './Breadcrumbs.module.scss';
+
+function Breadcrumbs(): JSX.Element {
+ const location = useLocation();
+
+ let result = '';
+
+ if (location.pathname.includes('Profile')) {
+ result = 'Profile';
+ } else if (location.pathname.includes('Information')) {
+ result = 'Information';
+ } else if (location.pathname.includes('Address')) {
+ result = 'Address';
+ } else if (location.pathname.includes('Order')) {
+ result = 'Order';
+ } else if (location.pathname.includes('basket')) {
+ result = 'Basket';
+ }
+
+ return (
+
+
+ Home
+
+ {/*
/ */}
+ {/*
My Dashboard
*/}
+
/
+
{result}
+
+ );
+}
+
+export default Breadcrumbs;
diff --git a/src/components/Button/Button.scss b/src/components/Button/Button.scss
new file mode 100644
index 0000000..f12407c
--- /dev/null
+++ b/src/components/Button/Button.scss
@@ -0,0 +1,59 @@
+.btn {
+ cursor: pointer;
+}
+
+.btn:disabled {
+ cursor: default;
+ background: #646464;
+ color: #fff;
+ transition: all 0.3s;
+ border-radius: 0;
+}
+
+.btn-enabled {
+ border: none;
+ margin-right: 66px;
+ background: #000;
+ color: #fff;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ width: 270px;
+ height: 50px;
+ transition: all 0.3s;
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.291);
+ transition: all 0.3s;
+ border-radius: 10px;
+ }
+}
+
+.btn-disabled {
+ color: black;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ width: 270px;
+ height: 50px;
+ border: 1px solid black;
+ transition: all 0.3s;
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.291);
+ transition: all 0.3s;
+ border-radius: 10px;
+ }
+}
+
+.mg-0 {
+ margin: 0;
+}
diff --git a/src/components/ButtonsAccount/ButtonsAccount.tsx b/src/components/ButtonsAccount/ButtonsAccount.tsx
new file mode 100644
index 0000000..43a28b6
--- /dev/null
+++ b/src/components/ButtonsAccount/ButtonsAccount.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+function ButtonsAccount(): JSX.Element {
+ return (
+ <>
+
+
+ LOG IN
+
+
+
+
+ REGISTER
+
+
+ >
+ );
+}
+
+export default ButtonsAccount;
diff --git a/src/components/ButtonsAccount/__tests__/ButtonsAccount.test.tsx b/src/components/ButtonsAccount/__tests__/ButtonsAccount.test.tsx
new file mode 100644
index 0000000..95af231
--- /dev/null
+++ b/src/components/ButtonsAccount/__tests__/ButtonsAccount.test.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { BrowserRouter as Router } from 'react-router-dom';
+import ButtonsAccount from '../ButtonsAccount';
+
+test('renders login and register buttons with correct links', () => {
+ render(
+
+
+ ,
+ );
+
+ const loginButton = screen.getByRole('link', { name: /LOG IN/i });
+ expect(loginButton).toHaveAttribute('href', '/login');
+
+ const registerButton = screen.getByRole('link', { name: /REGISTER/i });
+ expect(registerButton).toHaveAttribute('href', '/register');
+});
diff --git a/src/components/CartItem/CartItem.scss b/src/components/CartItem/CartItem.scss
new file mode 100644
index 0000000..e334f1b
--- /dev/null
+++ b/src/components/CartItem/CartItem.scss
@@ -0,0 +1,123 @@
+@import url('https://fonts.googleapis.com/css2?family=Oswald&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=ABeeZee&display=swap');
+
+.cart-image {
+ max-width: 200px;
+ padding: 0 10px;
+ img {
+ width: 100%;
+ height: 100%;
+ }
+}
+
+.cart-title-product {
+ font-family: 'Oswald', sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 500;
+ padding: 0 10px;
+ text-align: center;
+}
+
+.cart-price-product {
+ padding: 0 10px;
+ font-family: 'ABeeZee', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ text-align: center;
+}
+
+.cart-price-orig {
+ padding: 0 10px;
+ font-family: 'ABeeZee', sans-serif;
+ font-size: 12px;
+ font-style: normal;
+ text-align: center;
+ text-decoration: line-through;
+ font-size: 0.8em;
+}
+.cart-price-discount {
+ padding: 0 10px;
+ font-family: 'ABeeZee', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ text-align: center;
+ color: red;
+}
+
+.cart-quantity {
+ display: flex;
+ align-items: center;
+ border: 1px solid #c4c4c4;
+ font-family: 'ABeeZee', sans-serif;
+ height: 45px;
+ justify-content: center;
+ max-width: 105px;
+ margin: 0 auto;
+ .quantity-number {
+ width: 50px;
+ padding: 0 15px;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
+ div:first-child,
+ div:last-child {
+ padding: 0 12px;
+ }
+}
+
+.cart-total-price {
+ padding: 0 10px;
+ font-family: 'ABeeZee', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ text-align: center;
+}
+
+.cart-remove-item {
+ font-size: 27px;
+ transition: all 0.3s;
+ padding: 0 10px;
+ &:hover {
+ transform: rotate(45deg);
+ transition: all 0.3s;
+ }
+}
+
+.cart-item-container > .cart-image {
+ padding-top: 25px;
+}
+
+.button__quantity {
+ height: 100%;
+}
+
+button:disabled {
+ background-color: #dbdbdb;
+}
+
+button[disabled] + .quantity-number {
+ background-color: #e3e3e3;
+}
+
+@media (max-width: 450px) {
+ .cart-title-product {
+ font-size: 15px;
+ }
+ .cart-price-product,
+ .cart-total-price {
+ font-size: 11px;
+ }
+ .cart-quantity {
+ max-height: 40px;
+ text-align: left;
+ max-width: 75px;
+ & .quantity-number {
+ padding: 0px 7px;
+ }
+ }
+}
diff --git a/src/components/CartItem/CartItem.tsx b/src/components/CartItem/CartItem.tsx
new file mode 100644
index 0000000..6512dfb
--- /dev/null
+++ b/src/components/CartItem/CartItem.tsx
@@ -0,0 +1,135 @@
+import React, { MouseEvent, useState } from 'react';
+import './CartItem.scss';
+import { CiSquareRemove } from 'react-icons/ci';
+import { AiOutlinePlus, AiOutlineMinus } from 'react-icons/ai';
+import { addToCart, getCartById, removeFromCart } from '@src/services/CartService/CartService';
+import Cookies from 'js-cookie';
+import { Cart, LinePrice } from '@src/interfaces/Cart';
+import getCookieToken from '@src/utilities/getCookieToken';
+
+function CartItem({
+ id,
+ image,
+ name,
+ setCart,
+ quantity,
+ price,
+ discountedPrice,
+}: {
+ id: string;
+ image: { url: string }[];
+ name: string;
+ setCart: React.Dispatch>;
+ quantity: number;
+ price: {
+ value: LinePrice;
+ discounted?: {
+ value: LinePrice;
+ };
+ };
+ discountedPrice: LinePrice | undefined;
+}): JSX.Element {
+ const handlerRemove = (event: MouseEvent): void => {
+ const cartId = Cookies.get('cart-id');
+ const idTarget = event.currentTarget.id;
+ if (cartId) {
+ getCookieToken().then((token) => {
+ if (token) {
+ getCartById(token, cartId).then((item) => {
+ removeFromCart(token, cartId, idTarget, item.version).then((requestNewCart) => setCart(requestNewCart));
+ });
+ }
+ });
+ }
+ };
+
+ const [disabledButton, setDisabledButton] = useState(false);
+
+ const currency = price.value.currencyCode || 'EUR';
+ const cartPrice = price.value.centAmount / 100;
+ const cartDiscountedPrice = (price.discounted?.value.centAmount || 0) / 100;
+ const cartDiscountedPricePromo = (discountedPrice?.centAmount || 0) / 100;
+ const cartLastPrice = cartDiscountedPricePromo || cartDiscountedPrice || cartPrice;
+ const cartTotalPrice = cartLastPrice * quantity;
+
+ const getFormattedSum = (sum: number): string =>
+ new Intl.NumberFormat('en-IN', {
+ style: 'currency',
+ currency,
+ }).format(sum);
+
+ let elemCartPriceFormated = {getFormattedSum(cartPrice)}
;
+ if (cartPrice !== cartLastPrice) {
+ elemCartPriceFormated = (
+ <>
+ {getFormattedSum(cartLastPrice)}
+ {getFormattedSum(cartPrice)}
+ >
+ );
+ }
+
+ const handlerButtonDec = (): void => {
+ setDisabledButton(true);
+ const cartId = Cookies.get('cart-id');
+
+ if (cartId) {
+ getCookieToken().then((token) => {
+ if (token) {
+ getCartById(token, cartId).then((cart) => {
+ removeFromCart(token, cartId, id, cart.version, 1).then((items) => {
+ setCart(items);
+ setDisabledButton(false);
+ });
+ });
+ }
+ });
+ }
+ };
+
+ const handlerButtonInc = (): void => {
+ setDisabledButton(true);
+ const cartId = Cookies.get('cart-id');
+
+ if (cartId) {
+ getCookieToken().then((token) => {
+ if (token) {
+ getCartById(token, cartId).then((item) => {
+ addToCart(token, cartId, name.split(' ').join('-'), item.version, 1).then((items) => {
+ setCart(items);
+ setDisabledButton(false);
+ });
+ });
+ }
+ });
+ }
+ };
+
+ return (
+
+
+
+
+ {name}
+ {elemCartPriceFormated}
+
+
+
+
+
+
{quantity}
+
+
+
+
+
+ {getFormattedSum(cartTotalPrice)}
+
+
+
+
+
+
+ );
+}
+
+export default CartItem;
diff --git a/src/components/CatalogProductCard/CatalogProductCard.scss b/src/components/CatalogProductCard/CatalogProductCard.scss
new file mode 100644
index 0000000..a8ea4ea
--- /dev/null
+++ b/src/components/CatalogProductCard/CatalogProductCard.scss
@@ -0,0 +1,123 @@
+.product-list {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ column-gap: 15px;
+ row-gap: 30px;
+ font-family: 'Oswald', sans-serif;
+ font-style: normal;
+ font-weight: 400;
+ margin-top: 5px;
+ flex-grow: 2;
+ .product-card {
+ position: relative;
+ color: black;
+ padding: 5px;
+ width: 240px;
+ height: 490px;
+ transition:
+ box-shadow 0.3s ease,
+ border-radius 0.3s ease;
+ border-radius: 8px;
+ text-align: center;
+ img {
+ width: 100%;
+ height: 180px;
+ object-fit: contain;
+ }
+
+ h4 {
+ margin: 0;
+ }
+
+ h3 {
+ height: 80px;
+ font-size: 24px;
+ margin-bottom: 5px;
+ }
+
+ p {
+ color: #3f3f3f;
+ }
+ &:hover {
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 16px;
+ }
+
+ .buttons-container {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ display: grid;
+ grid-template-columns: repeat(2, 100px);
+ a {
+ grid-column: span 2;
+ }
+ .details-button,
+ .add-to-cart,
+ .remove-from-cart {
+ margin: 0;
+ width: 100%;
+ font-size: 12px;
+ border-radius: 8px;
+ &:hover {
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.485);
+ border-radius: 16px;
+ }
+
+ &:disabled {
+ background-color: rgba(128, 128, 128, 0.124);
+ cursor: auto;
+ border-radius: 8px;
+ box-shadow: none;
+
+ .cart-add-img {
+ filter: invert(6%) sepia(4%) saturate(1067%) hue-rotate(314deg) brightness(93%) contrast(83%);
+ }
+
+ .cart-remove-img {
+ filter: invert(6%) sepia(4%) saturate(1067%) hue-rotate(314deg) brightness(93%) contrast(83%);
+ }
+ }
+
+ .cart-remove-img,
+ .cart-add-img {
+ height: 50%;
+ }
+
+ .cart-add-img {
+ filter: invert(23%) sepia(99%) saturate(2030%) hue-rotate(96deg) brightness(97%) contrast(106%);
+ }
+
+ .cart-remove-img {
+ filter: invert(10%) sepia(57%) saturate(7500%) hue-rotate(357deg) brightness(104%) contrast(96%);
+ }
+ }
+ }
+ }
+
+ .price-container {
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ .discounted-price {
+ scale: 1.1;
+ color: red;
+ }
+ .crossed-price {
+ text-decoration: line-through;
+ }
+ }
+}
+
+@media (max-width: 800px) {
+ .product-list {
+ .product-card {
+ width: 130px;
+ .buttons-container {
+ grid-template-columns: repeat(2, 50px);
+ }
+ }
+ }
+}
diff --git a/src/components/CatalogProductCard/CatalogProductCard.tsx b/src/components/CatalogProductCard/CatalogProductCard.tsx
new file mode 100644
index 0000000..fe9c305
--- /dev/null
+++ b/src/components/CatalogProductCard/CatalogProductCard.tsx
@@ -0,0 +1,115 @@
+import './CatalogProductCard.scss';
+import React, { useState } from 'react';
+import { ProductCatalog } from '@src/interfaces/Product';
+import ClipLoader from 'react-spinners/ClipLoader';
+import { Link } from 'react-router-dom';
+
+import cartAdd from '@assets/cart-plus-solid.svg';
+import cartRemove from '@assets/cart-shopping-solid.svg';
+
+function CatalogProductCard({
+ product,
+ cartList,
+ addToCart,
+ removeFromCart,
+}: {
+ product: ProductCatalog;
+ cartList: { productId: string; id: string }[];
+ addToCart: (product: string) => Promise;
+ removeFromCart: (product: string) => Promise;
+}): JSX.Element {
+ const { name, masterVariant } = product;
+ const { images, prices, sku } = masterVariant;
+
+ const [addItemLoading, setAddItemLoading] = useState(false);
+ const [removeItemLoading, setRemoveItemLoading] = useState(false);
+
+ let CartProduct: {
+ productId: string;
+ id: string;
+ } = {
+ productId: '0',
+ id: '0',
+ };
+ if (cartList.length > 0) {
+ const foundProduct = cartList.find((item) => item.productId === product.id);
+ if (foundProduct) {
+ CartProduct = foundProduct;
+ }
+ }
+
+ let price: JSX.Element;
+ if (prices[0].discounted) {
+ price = (
+
+
+ {prices[0].discounted.value.centAmount / 100} {prices[0].value.currencyCode}
+
+
+ {prices[0].value.centAmount / 100} {prices[0].value.currencyCode}
+
+
+ );
+ } else {
+ price = (
+
+
+ {prices[0].value.centAmount / 100} {prices[0].value.currencyCode}
+
+
+ );
+ }
+
+ return (
+
+
+
{name.en}
+ {price}
+
+
{
+ setAddItemLoading(true);
+ addToCart(sku).then(() => {
+ setAddItemLoading(false);
+ });
+ }}
+ disabled={CartProduct.id !== '0'}
+ >
+ {addItemLoading ? (
+
+ ) : (
+
+ )}
+
+
{
+ setRemoveItemLoading(true);
+ removeFromCart(CartProduct.id).then(() => {
+ setRemoveItemLoading(false);
+ });
+ }}
+ disabled={CartProduct.id === '0'}
+ >
+ {removeItemLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+ Details
+
+
+
+
+ );
+}
+
+export default CatalogProductCard;
diff --git a/src/components/CatalogProductCard/__tests__/CatalogProductCard.test.tsx b/src/components/CatalogProductCard/__tests__/CatalogProductCard.test.tsx
new file mode 100644
index 0000000..71b8b89
--- /dev/null
+++ b/src/components/CatalogProductCard/__tests__/CatalogProductCard.test.tsx
@@ -0,0 +1,142 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+
+import '@testing-library/jest-dom';
+import { ProductCatalog } from '@src/interfaces/Product';
+import { BrowserRouter } from 'react-router-dom';
+import CatalogProductCard from '../CatalogProductCard';
+
+const addToCart = async (product: string): Promise => {
+ console.log(product);
+};
+
+const removeFromCart = async (product: string): Promise => {
+ console.log(product);
+};
+
+const mockProductWithDiscount: ProductCatalog = {
+ id: '1',
+ name: { en: 'Product Name' },
+ description: { en: 'Product Description' },
+ key: '1',
+ masterVariant: {
+ id: 123,
+ sku: 'testSKU',
+ images: [{ url: 'image_url' }],
+ prices: [
+ {
+ id: 'price_id',
+ value: {
+ currencyCode: 'USD',
+ centAmount: 1000,
+ fractionDigits: 2,
+ },
+ discounted: {
+ value: {
+ centAmount: 500,
+ },
+ },
+ },
+ ],
+ attributes: [
+ {
+ name: 'brand',
+ value: 'test',
+ },
+ ],
+ },
+ categories: [
+ {
+ typeId: 'test',
+ id: 'test',
+ },
+ ],
+};
+
+const mockProductWithoutDiscount: ProductCatalog = {
+ id: '1',
+ name: { en: 'Product Name' },
+ description: { en: 'Product Description' },
+ key: '1',
+ masterVariant: {
+ id: 123,
+ sku: 'testSKU',
+ images: [{ url: 'image_url' }],
+ prices: [
+ {
+ id: 'price_id',
+ value: {
+ currencyCode: 'USD',
+ centAmount: 1000,
+ fractionDigits: 2,
+ },
+ },
+ ],
+ attributes: [
+ {
+ name: 'brand',
+ value: 'test',
+ },
+ ],
+ },
+ categories: [
+ {
+ typeId: 'test',
+ id: 'test',
+ },
+ ],
+};
+
+describe('CatalogProductCard Component', () => {
+ it('renders product information correctly', () => {
+ const { getByText, getByAltText } = render(
+
+
+ ,
+ );
+
+ expect(getByText('Product Name')).toBeInTheDocument();
+ expect(getByText('10 USD')).toBeInTheDocument();
+ expect(getByText('5 USD')).toBeInTheDocument();
+
+ const productImage = getByAltText('Product Name');
+ expect(productImage).toBeInTheDocument();
+ expect(productImage).toHaveAttribute('src', 'image_url');
+ });
+
+ it('renders product card with discounted price when discounted price exists', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ expect(getByText('5 USD')).toBeInTheDocument();
+ expect(getByText('10 USD')).toBeInTheDocument();
+ });
+
+ it('renders product card with regular price when discounted price does not exist', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ expect(getByText('10 USD')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/CategoryCard/CategoryCard.scss b/src/components/CategoryCard/CategoryCard.scss
new file mode 100644
index 0000000..a4532a2
--- /dev/null
+++ b/src/components/CategoryCard/CategoryCard.scss
@@ -0,0 +1,76 @@
+.category {
+ position: relative;
+ transition: 400ms;
+ border: 1px solid #ddd;
+ padding: 0 5px;
+ height: 30px;
+ margin: 5px;
+ width: 200px;
+ background-color: #f9f9f9;
+ justify-content: center;
+ display: flex;
+
+ font-family: 'Oswald', sans-serif;
+ font-size: 16px;
+ color: #333;
+
+ .category-header {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ button {
+ width: 24px;
+ img {
+ transition: 200ms;
+ }
+ }
+ }
+}
+
+.subcategories {
+ width: 100%;
+ transition: 300ms;
+ position: absolute;
+ background-color: #f9f9f9;
+ border: 1px solid #ddd;
+ padding: 5px;
+ margin: 5px;
+ opacity: 0;
+ left: -5px;
+ top: 25px;
+ z-index: -1;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ div {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+}
+
+.category-open {
+ img {
+ rotate: 180deg;
+ }
+ .subcategories {
+ opacity: 1;
+ z-index: 1;
+ }
+}
+
+.open-categories {
+ display: none;
+ position: relative;
+ transition: 400ms;
+ border: 1px solid #ddd;
+ padding: 0 5px;
+ height: 30px;
+ margin: 5px;
+ width: 200px;
+ background-color: #f9f9f9;
+ font-family: 'Oswald', sans-serif;
+ font-size: 16px;
+ color: #333;
+}
diff --git a/src/components/CategoryCard/CategoryCard.tsx b/src/components/CategoryCard/CategoryCard.tsx
new file mode 100644
index 0000000..632da8c
--- /dev/null
+++ b/src/components/CategoryCard/CategoryCard.tsx
@@ -0,0 +1,36 @@
+import React, { useState } from 'react';
+import './CategoryCard.scss';
+import openIcon from '@assets/chevron-down-solid.svg';
+import { CategoryProps } from '@src/interfaces/Category';
+import { Link } from 'react-router-dom';
+
+function CategoryCard({ category }: CategoryProps): JSX.Element {
+ const { name, slug, ancestors } = category;
+
+ const [isCategoryOpen, setCategoryOpen] = useState(false);
+
+ const handleButtonClick = (): void => {
+ setCategoryOpen(!isCategoryOpen);
+ };
+
+ return (
+
+
+
{name}
+
+
+
+
+
+
+ {ancestors.map((subcategory) => (
+
+ {subcategory.name}
+
+ ))}
+
+
+ );
+}
+
+export default CategoryCard;
diff --git a/src/components/CategoryCard/__tests__/CategoryCard.test.tsx b/src/components/CategoryCard/__tests__/CategoryCard.test.tsx
new file mode 100644
index 0000000..89bb464
--- /dev/null
+++ b/src/components/CategoryCard/__tests__/CategoryCard.test.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { render, getByAltText } from '@testing-library/react';
+import { BrowserRouter } from 'react-router-dom';
+import CategoryCard from '../CategoryCard';
+import '@testing-library/jest-dom';
+
+const mockCategory = {
+ id: 'Test Category',
+ name: 'Test Category',
+ slug: 'test-category',
+ ancestors: [],
+};
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useLocation: (): { pathname: string } => ({ pathname: '/catalog' }),
+}));
+
+test('CategoryCard renders correctly', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ const categoryName = getByText('Test Category');
+ expect(categoryName).toBeInTheDocument();
+
+ const openButton = getByAltText(document.body, 'Open subcategories');
+ expect(openButton).toBeInTheDocument();
+});
diff --git a/src/components/FormAddress/FormAddress.tsx b/src/components/FormAddress/FormAddress.tsx
new file mode 100644
index 0000000..eed5a5d
--- /dev/null
+++ b/src/components/FormAddress/FormAddress.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import FormInput from '@components/FormInput/FormInput';
+import { PostalCodePattern } from '@interfaces/Register';
+import { BaseAddress } from '@interfaces/Customer';
+
+const postalCodePattern: PostalCodePattern = {
+ US: '\\d{5}-\\d{4}|\\d{5}',
+ RU: '\\d{6}',
+ GB: '[A-Za-z]{1,2}\\d{1,2}[A-Za-z]?\\s?\\d[A-Za-z]{2}',
+ DE: '\\d{5}',
+ FR: '\\d{5}',
+};
+
+function FormAddress(props: {
+ prefix: string;
+ city: string;
+ postalCode: string;
+ streetName: string;
+ country: string;
+ disabled?: boolean;
+ onInputChange: (address: Partial) => void;
+}): JSX.Element {
+ const { prefix, city, postalCode, streetName, country, disabled, onInputChange } = props;
+
+ const handleCB = (id: string, value: string): void => {
+ const idWithoutPrefix = id.replace(prefix, '');
+ onInputChange({ [idWithoutPrefix]: value });
+ };
+
+ const handleInputChange = (event: React.ChangeEvent): void => {
+ const { id, value } = event.target;
+ handleCB(id, value);
+ };
+
+ const handleCountryChange = (event: React.ChangeEvent): void => {
+ const { id, value } = event.target;
+ handleCB(id, value);
+ };
+
+ return (
+ <>
+
+
+ Country
+
+
+ Select a country ...
+
+
+ United States
+
+
+ Russia
+
+
+ United Kingdom
+
+
+ Germany
+
+
+ France
+
+
+
+
+
+
+
+ >
+ );
+}
+
+FormAddress.defaultProps = {
+ disabled: false,
+};
+
+export default FormAddress;
diff --git a/src/components/FormAddress/__tests__/FormAddress.test.tsx b/src/components/FormAddress/__tests__/FormAddress.test.tsx
new file mode 100644
index 0000000..8e15d50
--- /dev/null
+++ b/src/components/FormAddress/__tests__/FormAddress.test.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import FormAddress from '../FormAddress';
+
+describe('FormAddress component', () => {
+ test('renders the input element with a label', () => {
+ render(
+ {}}
+ />,
+ );
+
+ const labelElementCity = screen.getByText('City');
+ const inputElementCity = document.querySelector('#city');
+
+ expect(labelElementCity).toBeInTheDocument();
+ expect((inputElementCity as HTMLInputElement).value).toBe('TestCity');
+ });
+
+ test('displays an error message', () => {
+ render(
+ {}}
+ />,
+ );
+
+ const errorElement = screen.getByText('City is not valid');
+ expect(errorElement).toBeInTheDocument();
+ });
+
+ test('calls onChange when input value changes', () => {
+ const onChangeMock = jest.fn();
+
+ render(
+ ,
+ );
+
+ const inputElementCity = document.querySelector('#city') as HTMLInputElement;
+ const inputElementCountry = document.querySelector('#country') as HTMLInputElement;
+
+ fireEvent.change(inputElementCity, { target: { value: 'new' } });
+ fireEvent.change(inputElementCountry, { target: { value: 'US' } });
+
+ expect(onChangeMock).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/src/components/FormErrors/FormErrors.tsx b/src/components/FormErrors/FormErrors.tsx
new file mode 100644
index 0000000..71e098c
--- /dev/null
+++ b/src/components/FormErrors/FormErrors.tsx
@@ -0,0 +1,21 @@
+import { FormErrorsInterface } from '@src/interfaces/Errors';
+import React from 'react';
+
+function FormErrors({ formErrors }: FormErrorsInterface): JSX.Element {
+ return (
+
+ {Object.keys(formErrors).map((fieldName) => {
+ if (formErrors[fieldName].length > 0) {
+ return (
+
+ {fieldName} {formErrors[fieldName]}
+
+ );
+ }
+ return '';
+ })}
+
+ );
+}
+
+export default FormErrors;
diff --git a/src/components/FormErrors/__tests__/FormErrors.test.tsx b/src/components/FormErrors/__tests__/FormErrors.test.tsx
new file mode 100644
index 0000000..7ffb518
--- /dev/null
+++ b/src/components/FormErrors/__tests__/FormErrors.test.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import FormErrors from '../FormErrors';
+
+describe('FormErrors component', () => {
+ test('renders with correct errors', () => {
+ const formErrors = {
+ email: 'Invalid email format',
+ password: 'Password is too short',
+ };
+
+ render( );
+
+ const emailErrorElement = screen.getByText('email Invalid email format');
+ const passwordErrorElement = screen.getByText('password Password is too short');
+
+ expect(emailErrorElement).toBeInTheDocument();
+ expect(passwordErrorElement).toBeInTheDocument();
+ });
+});
diff --git a/src/components/FormInput/FormInput.scss b/src/components/FormInput/FormInput.scss
new file mode 100644
index 0000000..7e38777
--- /dev/null
+++ b/src/components/FormInput/FormInput.scss
@@ -0,0 +1,58 @@
+.form-input {
+ display: flex;
+ justify-content: center;
+ position: relative;
+ font-family: 'Oswald', sans-serif;
+
+ input,
+ select {
+ box-sizing: border-box;
+ width: 175px;
+ height: 40px;
+ padding: 5px;
+ margin: 1rem;
+ border-radius: 8px;
+ border: 1px solid gray;
+
+ &:invalid {
+ border: 1px solid red;
+ }
+
+ &:invalid ~ span,
+ &.error ~ span {
+ visibility: visible;
+ }
+ }
+
+ span {
+ color: red;
+ visibility: hidden;
+ display: block;
+ position: absolute;
+ bottom: 0;
+ font-size: 12px;
+ text-align: left;
+ }
+
+ label {
+ position: relative;
+ width: 25rem;
+ height: 100px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ #password {
+ -webkit-text-security: disc;
+ text-security: disc;
+ }
+}
+
+.buttons-container {
+ display: flex;
+ gap: 20px;
+ justify-content: center;
+ margin-bottom: 20px;
+ align-items: center;
+}
diff --git a/src/components/FormInput/FormInput.tsx b/src/components/FormInput/FormInput.tsx
new file mode 100644
index 0000000..f677883
--- /dev/null
+++ b/src/components/FormInput/FormInput.tsx
@@ -0,0 +1,68 @@
+import React, { ChangeEventHandler, KeyboardEventHandler } from 'react';
+import './FormInput.scss';
+
+function FormInput(props: {
+ label: string;
+ errorMessage: string;
+ onChange: ChangeEventHandler;
+ id: string;
+ pattern: string;
+ type: string;
+ title: string;
+ max?: string;
+ value?: string;
+ disabled?: boolean;
+ button?: React.ReactNode;
+ onKeyDown?: KeyboardEventHandler;
+ // eslint-disable-next-line react/require-default-props
+ placeholderr?: string;
+}): JSX.Element {
+ const {
+ label,
+ errorMessage,
+ onChange,
+ id,
+ type,
+ pattern,
+ title,
+ max,
+ value,
+ disabled,
+ button,
+ onKeyDown,
+ placeholderr,
+ } = props;
+ const spanClass = `${id}-error`;
+
+ return (
+
+
+ {label}
+
+ {button}
+ {errorMessage}
+
+
+ );
+}
+
+FormInput.defaultProps = {
+ max: '',
+ value: '',
+ disabled: false,
+ button: null,
+ onKeyDown: null,
+};
+
+export default FormInput;
diff --git a/src/components/FormInput/__tests__/FormInput.test.tsx b/src/components/FormInput/__tests__/FormInput.test.tsx
new file mode 100644
index 0000000..77fb7e9
--- /dev/null
+++ b/src/components/FormInput/__tests__/FormInput.test.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import FormInput from '../FormInput';
+
+describe('FormInput component', () => {
+ test('renders the input element with a label', () => {
+ render(
+ {}}
+ id="username"
+ type="text"
+ pattern=""
+ title=""
+ />,
+ );
+
+ const labelElement = screen.getByText('Username');
+ const inputElement = screen.getByRole('textbox', { name: 'Username' });
+
+ expect(labelElement).toBeInTheDocument();
+ expect(inputElement).toBeInTheDocument();
+ });
+
+ test('displays an error message', () => {
+ render(
+ {}}
+ id="username"
+ type="text"
+ pattern=""
+ title=""
+ />,
+ );
+
+ const errorElement = screen.getByText('Invalid username');
+ expect(errorElement).toBeInTheDocument();
+ });
+
+ test('calls onChange when input value changes', () => {
+ const onChangeMock = jest.fn();
+
+ render(
+ ,
+ );
+
+ const inputElement = screen.getByRole('textbox', { name: 'Username' });
+
+ fireEvent.change(inputElement, { target: { value: 'newUsername' } });
+
+ expect(onChangeMock).toHaveBeenCalledWith(expect.any(Object));
+ });
+});
diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss
new file mode 100644
index 0000000..ba53091
--- /dev/null
+++ b/src/components/Header/Header.scss
@@ -0,0 +1,287 @@
+@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@600&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=ABeeZee&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@700&display=swap');
+
+:root {
+ --font-s14: 14px;
+ --font-s12: 12px;
+ --font-s22: 22px;
+}
+
+.header {
+ height: 115px;
+ background-color: black;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.burger-menu {
+ display: none;
+}
+
+.container {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ padding: 0 20px;
+ position: relative;
+}
+
+.nav__list {
+ display: flex;
+ gap: 20px;
+ list-style: none;
+ padding: 0;
+}
+
+.nav {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ justify-content: space-around;
+}
+
+.btn__home {
+ color: #fff;
+ font-family: 'Oswald', sans-serif;
+ font-size: var(--font-s14);
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+ text-transform: uppercase;
+ text-decoration: none;
+}
+
+.header__cart {
+ display: flex;
+ gap: 12px;
+}
+
+.header__cart_icon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.header__cart_icon img {
+ width: 18px;
+ height: 20px;
+}
+
+.cart__title {
+ font-family: 'ABeeZee', sans-serif;
+ font-size: var(--font-s12);
+ font-style: normal;
+ font-weight: 400;
+}
+
+.cart__sell {
+ color: #fff;
+ font-family: 'Roboto', sans-serif;
+ font-size: var(--font-s14);
+ font-style: normal;
+ font-weight: 700;
+}
+
+.cart__title_container {
+ font-family: 'Oswald', sans-serif;
+ color: white;
+}
+
+.header__search {
+ width: 50px;
+ height: 50px;
+ background: #41484a;
+ text-align: center;
+ line-height: 45px;
+ padding-right: 55px;
+ transition: all 0.5s ease;
+ border-radius: 5px;
+ position: relative;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
+ .header__search_icon {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ position: absolute;
+ color: #ffffff;
+ right: 15px;
+ top: 15px;
+ font-size: var(--font-s22);
+ cursor: pointer;
+ img {
+ width: 16px;
+ height: 16px;
+ pointer-events: none;
+ }
+ .search__title {
+ font-family: 'Oswald', sans-serif;
+ color: white;
+ font-size: var(--font-s14);
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+ text-transform: uppercase;
+ pointer-events: none;
+ }
+ }
+ .search-box {
+ border: 0px;
+ border-bottom: 2px solid #fff;
+ background: transparent;
+ width: 0%;
+ padding: 5px 0;
+ outline: none;
+ color: #fff;
+ font-weight: bold;
+ transition: all 0.3s ease;
+ }
+ .search-box.active-search {
+ width: 98%;
+ padding-left: 5px;
+ transition: all 0.5s 0.8s ease;
+ }
+
+ .search-box::placeholder {
+ color: #fff;
+ }
+}
+
+.header__search.active-search {
+ width: 250px;
+ padding-left: 25px;
+ padding-right: 100px;
+ transition: all 0.5s ease;
+}
+
+.header__account-info {
+ display: flex;
+ gap: 12px;
+}
+
+.header__account-in,
+.header__account-create {
+ color: #fff;
+ font-family: 'Oswald', sans-serif;
+ font-size: var(--font-s14);
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+ text-transform: uppercase;
+ transition: all 0.3s;
+}
+
+.header__account-in:hover,
+.header__account-create:hover,
+.header__cart_icon:hover,
+.header__account-name:hover {
+ transition: all 0.3s;
+ box-shadow: 0 0 4px 4px rgb(48, 48, 48);
+ color: rgba(255, 255, 255, 0.73);
+}
+
+button {
+ cursor: pointer;
+ border: none;
+ background-color: inherit;
+}
+
+@media (max-width: 950px) {
+ :root {
+ --font-s14: 12px;
+ }
+ .logo {
+ max-width: 90px;
+ }
+ .header__search.active-search {
+ width: 180px;
+ }
+}
+
+@media (max-width: 900px) {
+ :root {
+ --font-s14: 15px;
+ }
+ .header {
+ height: auto;
+ // padding-top: 20px 0;
+ }
+ .container {
+ padding: 37px 0;
+ }
+ .lock {
+ overflow: hidden;
+ }
+ .nav {
+ display: none;
+ pointer-events: auto;
+ }
+ .nav.active_nav {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ justify-content: center;
+ flex-direction: column;
+ position: absolute;
+ z-index: 5;
+ background: #3a4242;
+ height: 100vh;
+ top: 0;
+ left: 0;
+ gap: 45px;
+ }
+ .nav__list,
+ .header__account-info {
+ flex-direction: column;
+ margin: 0;
+ }
+ .header-burger {
+ display: block;
+ position: relative;
+ width: 30px;
+ height: 20px;
+ z-index: 6;
+ span {
+ position: absolute;
+ background: white;
+ left: 0;
+ width: 100%;
+ height: 2px;
+ top: 9px;
+ transition: all 0.5s ease 0s;
+ }
+ &:before,
+ &:after {
+ content: '';
+ background: white;
+ position: absolute;
+ width: 100%;
+ height: 2px;
+ left: 0;
+ transition: all 0.5s ease 0s;
+ }
+ &:before {
+ top: 0;
+ }
+
+ &:after {
+ bottom: 0;
+ }
+
+ &.active_nav:before {
+ transform: rotate(45deg);
+ top: 9px;
+ }
+
+ &.active_nav:after {
+ transform: rotate(-45deg);
+ bottom: 9px;
+ }
+
+ &.active_nav span {
+ transform: scale(0);
+ }
+ }
+}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 0000000..5551cfa
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,92 @@
+import React, { useState, MouseEvent } from 'react';
+import './Header.scss';
+import { Link } from 'react-router-dom';
+import logoIcon from '@assets/logo.svg';
+import cartIcon from '@assets/cart.svg';
+// import searchIcon from '@assets/search.svg';
+import ButtonsAccount from '../ButtonsAccount/ButtonsAccount';
+import NameAccount from '../NameAccount/NameAccount';
+
+const buttonsData = [
+ { name: 'home', label: 'home', path: '/' },
+ { name: 'catalog', label: 'catalog', path: '/catalog' },
+ { name: 'about us', label: 'about us', path: '/about' },
+];
+
+function Header({
+ authh,
+ logOut,
+ totalSumInCart,
+}: {
+ authh: boolean;
+ logOut: () => void;
+ totalSumInCart: number;
+}): JSX.Element {
+ const [isOpen, setIsOpen] = useState(false);
+ // const totalSumCart = useContext(AppContext);
+ const refactorSum =
+ totalSumInCart !== 0 ? `${String(totalSumInCart).slice(0, -2)}.${String(totalSumInCart).slice(-2)}` : '0';
+ const toggleMenu = (event: MouseEvent): void => {
+ const eventTarget = event.target as HTMLElement;
+ if (!eventTarget.className.includes('header__search') && !eventTarget.className.includes('search-box'))
+ setIsOpen(!isOpen);
+ };
+
+ // const onToggleActiveSearch: MouseEventHandler = (event): void => {
+ // const target = event.target as HTMLElement;
+ // target.parentElement?.classList.toggle('active-search');
+ // target.previousElementSibling?.classList.toggle('active-search');
+ // };
+
+ const buttons = buttonsData.map(({ name, label, path }) => (
+
+
+
+ {label}
+
+
+
+ ));
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+
+
+
+ search
+
+
*/}
+
+ {authh ? : }
+
+
+
+
+
+
+
Shopping Cart
+
{`${refactorSum} EUR`}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Header;
diff --git a/src/components/Header/__tests__/Header.test.tsx b/src/components/Header/__tests__/Header.test.tsx
new file mode 100644
index 0000000..58e349e
--- /dev/null
+++ b/src/components/Header/__tests__/Header.test.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { BrowserRouter } from 'react-router-dom';
+import Header from '../Header';
+
+describe('Header component', () => {
+ it('renders logo and navigation buttons', () => {
+ render(
+
+ ,
+ );
+
+ const logo = screen.getByAltText('logo');
+ const homeButton = screen.getByText('home');
+ const catalogButton = screen.getByText('catalog');
+ const aboutUsButton = screen.getByText('about us');
+
+ expect(logo).toBeInTheDocument();
+ expect(homeButton).toBeInTheDocument();
+ expect(catalogButton).toBeInTheDocument();
+ expect(aboutUsButton).toBeInTheDocument();
+ });
+
+ it('toggles the menu when burger button is clicked', () => {
+ render(
+
+ ,
+ );
+
+ const burgerButton = document.querySelector('.header-burger');
+ if (burgerButton) {
+ expect(burgerButton).not.toHaveClass('active_nav');
+
+ fireEvent.click(burgerButton);
+
+ expect(burgerButton).toHaveClass('active_nav');
+
+ fireEvent.click(burgerButton);
+
+ expect(burgerButton).not.toHaveClass('active_nav');
+ }
+ });
+});
diff --git a/src/components/Heading/Heading.scss b/src/components/Heading/Heading.scss
new file mode 100644
index 0000000..6deb3da
--- /dev/null
+++ b/src/components/Heading/Heading.scss
@@ -0,0 +1,16 @@
+.main-heading,
+.sub-heading {
+ color: #000;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-style: normal;
+ font-weight: 400;
+}
+
+.main-heading {
+ font-size: 48px;
+}
+
+.sub-heading {
+ font-size: 32px;
+}
diff --git a/src/components/LoginForm/LoginForm.scss b/src/components/LoginForm/LoginForm.scss
new file mode 100644
index 0000000..98964ce
--- /dev/null
+++ b/src/components/LoginForm/LoginForm.scss
@@ -0,0 +1,71 @@
+@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@600&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@400&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=ABeeZee&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap');
+
+.has-error {
+ box-shadow: 0 0 2px 2px red;
+}
+
+.wrapper-btn {
+ margin-top: 20px;
+ display: flex;
+ justify-content: center;
+}
+
+.login-form {
+ .btn__show-pass {
+ width: 10px;
+ margin: 0;
+ padding: 0;
+ border: 1px solid black;
+ position: absolute;
+ right: 0;
+ &:hover {
+ background-color: #3f3f3f4d;
+ }
+ }
+}
+
+.panel {
+ text-align: center;
+}
+
+@media (max-width: 410px) {
+ .login-form {
+ .btn__show-pass {
+ position: relative;
+ right: -100px;
+ top: -40px;
+ }
+ }
+}
+
+@media (max-width: 615px) {
+ .wrapper-btn {
+ flex-direction: column;
+ gap: 20px;
+ align-items: center;
+ .btn {
+ margin: 0;
+ }
+ }
+}
+
+@media (max-width: 470px) {
+ .form-group {
+ label {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ #email,
+ #password {
+ margin: 0;
+ }
+ .btn__show-pass {
+ margin-top: 10px;
+ }
+ }
+ }
+}
diff --git a/src/components/LoginForm/LoginForm.tsx b/src/components/LoginForm/LoginForm.tsx
new file mode 100644
index 0000000..6e01c95
--- /dev/null
+++ b/src/components/LoginForm/LoginForm.tsx
@@ -0,0 +1,250 @@
+import React, { useState, ChangeEvent, MouseEvent, useEffect, KeyboardEventHandler } from 'react';
+import Cookies from 'js-cookie';
+import { Link, useNavigate } from 'react-router-dom';
+import './LoginForm.scss';
+import '@components/Button/Button.scss';
+import '@components/Heading/Heading.scss';
+import { logInUser, logInUserWithCart } from '@services/AuthService/AuthService';
+import FormInput from '@components/FormInput/FormInput';
+import Toastify from 'toastify-js';
+
+function isValidEmail(email: string): string {
+ const atIndex = email.indexOf('@');
+ const dotIndex = email.lastIndexOf('.');
+
+ if (atIndex < 1) {
+ return 'Missing symbol @';
+ }
+
+ const localPart = email.substring(0, atIndex);
+ const domainPart = email.substring(atIndex + 1);
+
+ if (!localPart.match(/^[a-zA-Z0-9._%+-]+$/)) {
+ return 'Email contains invalid characters';
+ }
+
+ if (!domainPart.match(/^[a-zA-Z0-9.-]+$/)) {
+ return 'The domain part of the address with an error';
+ }
+
+ if (dotIndex === -1 || dotIndex < atIndex + 2) {
+ return 'There is no point between the domain part and the top-level domain';
+ }
+
+ const tld = email.substring(dotIndex + 1);
+ if (!tld.match(/^[a-zA-Z]{2,}$/)) {
+ return 'The top-level domain is written with an error';
+ }
+
+ return 'true';
+}
+
+function isValidatePassword(password: string): string {
+ if (password.length < 8) {
+ return 'The password must be more than 8 characters long';
+ }
+
+ if (!/[A-Z]/.test(password)) {
+ return 'The password must contain capital letter';
+ }
+
+ if (!/[a-z]/.test(password)) {
+ return 'The password must contain a lowercase letter';
+ }
+
+ if (!/\d/.test(password)) {
+ return 'The password must contain a digit';
+ }
+
+ if (/^\s|\s$/.test(password)) {
+ return 'Contains spaces';
+ }
+
+ return 'true';
+}
+
+function SignInForm({ checkLogIn }: { checkLogIn: () => void }): JSX.Element {
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const authType = Cookies.get('auth-type');
+ if (authType === 'password') {
+ navigate('/');
+ Toastify({
+ text: 'Log out to access this page',
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ }
+ }, [navigate]);
+
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [emailValid, setEmailValid] = useState('');
+ const [passwordValid, setPasswordValid] = useState('');
+ const [formValid, setFormValid] = useState(false);
+ const [passwordError, setPasswordError] = useState('');
+ const [emailError, setEmailError] = useState('');
+
+ const validateField = (fieldName: string, value: string, element: HTMLElement): void => {
+ let emailValidate = emailValid;
+ let passwordValidate = passwordValid;
+ if (fieldName === 'email') {
+ setEmail(value);
+ emailValidate = isValidEmail(value);
+ if (emailValidate === 'true') {
+ setEmailError('');
+ setEmailValid(emailValidate);
+ } else {
+ element.classList.add('error');
+ setEmailError(emailValidate);
+ }
+ } else {
+ setPassword(value);
+ passwordValidate = isValidatePassword(value);
+ if (passwordValidate === 'true') {
+ setPasswordError('');
+ setPasswordValid(passwordValidate);
+ } else {
+ element.classList.add('error');
+ setPasswordError(passwordValidate);
+ }
+ }
+
+ if (passwordValidate === 'true' && emailValidate === 'true') {
+ setFormValid(true);
+ } else {
+ setFormValid(false);
+ }
+ };
+
+ const handleUserInput = (e: ChangeEvent): void => {
+ const { id, value } = e.target;
+ validateField(id, value, e.target);
+ if (id === 'email') {
+ setEmail(value);
+ } else {
+ setPassword(value);
+ }
+ };
+
+ const handleKeyboard: KeyboardEventHandler = (event): void => {
+ if (event.key === ' ') {
+ event.preventDefault();
+ }
+ };
+
+ const showPassword = (e: MouseEvent): void => {
+ const targetElement = e.target;
+
+ if (targetElement instanceof HTMLInputElement) {
+ const node = targetElement.previousElementSibling as HTMLInputElement;
+ if (targetElement.checked === true) {
+ node.type = 'text';
+ node.style.setProperty('-webkit-text-security', 'none');
+ node.style.setProperty('text-security', 'none');
+ } else {
+ node.type = 'password';
+ node.style.setProperty('-webkit-text-security', 'disc');
+ node.style.setProperty('text-security', 'disc');
+ }
+ }
+ };
+
+ const onSignIn = (event: MouseEvent): void => {
+ event.preventDefault();
+
+ const cart = Cookies.get('cart-id');
+
+ if (cart) {
+ logInUserWithCart(email, password).then((res) => {
+ if (res) {
+ if (res.cart) {
+ Cookies.set('cart-id', res.cart.id, { expires: 2 });
+ }
+ logInUser(email, password).then((response) => {
+ if (response) {
+ Cookies.set('access-token', response.accessToken, { expires: 2 });
+ Cookies.set('refresh-token', response.refreshToken, { expires: 200 });
+ Cookies.set('auth-type', 'password', { expires: 2 });
+ checkLogIn();
+ navigate('/');
+ }
+ });
+ }
+ });
+ } else {
+ logInUser(email, password).then((response) => {
+ if (response) {
+ Cookies.set('access-token', response.accessToken, { expires: 2 });
+ Cookies.set('refresh-token', response.refreshToken, { expires: 200 });
+ Cookies.set('auth-type', 'password', { expires: 2 });
+ checkLogIn();
+ navigate('/');
+ }
+ });
+ }
+ };
+
+ const showButton = (
+
+ );
+
+ return (
+
+ );
+}
+
+export default SignInForm;
diff --git a/src/components/LoginForm/__tests__/LoginForm.test.tsx b/src/components/LoginForm/__tests__/LoginForm.test.tsx
new file mode 100644
index 0000000..aa93dee
--- /dev/null
+++ b/src/components/LoginForm/__tests__/LoginForm.test.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { BrowserRouter as Router } from 'react-router-dom';
+import SignInForm from '../LoginForm';
+
+describe('SignInForm component', () => {
+ test('toggles password visibility', () => {
+ const mockCheckLogIn = jest.fn();
+ const { getByLabelText, getByRole } = render(
+
+
+ ,
+ );
+ const passwordInput = getByLabelText('Password *') as HTMLInputElement;
+ const visibilityToggleButton = getByRole('checkbox', { name: 'show password' });
+
+ fireEvent.change(passwordInput, { target: { value: 'Password123' } });
+ expect(passwordInput.value).toBe('Password123');
+ expect(passwordInput.type).toBe('password');
+
+ fireEvent.click(visibilityToggleButton);
+ expect(passwordInput.type).toBe('text');
+
+ fireEvent.click(visibilityToggleButton);
+ expect(passwordInput.type).toBe('password');
+ });
+
+ test('blocks spacebar key on email and password input fields', () => {
+ const mockCheckLogIn = jest.fn();
+ const { getByLabelText } = render(
+
+
+ ,
+ );
+ const emailInput = getByLabelText('Email *') as HTMLInputElement;
+ const passwordInput = getByLabelText('Password *') as HTMLInputElement;
+
+ fireEvent.keyDown(emailInput, { key: ' ', code: 'Space' });
+ expect(emailInput.value).toBe('');
+
+ fireEvent.keyDown(passwordInput, { key: ' ', code: 'Space' });
+ expect(passwordInput.value).toBe('');
+ });
+});
diff --git a/src/components/Modal/Modal.scss b/src/components/Modal/Modal.scss
new file mode 100644
index 0000000..3c3fe9b
--- /dev/null
+++ b/src/components/Modal/Modal.scss
@@ -0,0 +1,58 @@
+.modal {
+ width: 100vw;
+ height: 100vh;
+ background-color: rgba(0, 0, 0, 0.4);
+ position: fixed;
+ top: 0;
+ left: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ opacity: 0;
+ pointer-events: none;
+ transition: all 0.5s;
+ // flex-direction: column;
+}
+
+.modal.active__modal {
+ opacity: 1;
+ pointer-events: all;
+}
+
+.modal__content {
+ padding: 20px;
+ border-radius: 10px;
+ background-color: white;
+ // width: 400px;
+ // height: 450px;
+ transform: scale(0.5);
+ transition: all 0.4s;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.modal__content.active__modal {
+ transform: scale(1);
+}
+
+.btn__save {
+ color: #fff;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ width: 150px;
+ height: 50px;
+ background: black;
+ text-transform: uppercase;
+ &:hover {
+ background: rgba(0, 0, 0, 0.685);
+ }
+ &:disabled {
+ cursor: auto;
+ background-color: rgba(0, 0, 0, 0.101);
+ }
+}
diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx
new file mode 100644
index 0000000..d3834a7
--- /dev/null
+++ b/src/components/Modal/Modal.tsx
@@ -0,0 +1,175 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+import React, { useEffect, useState } from 'react';
+import './Modal.scss';
+import { FaRegSave } from 'react-icons/fa';
+import { PostalCodePattern } from '@src/interfaces/Register';
+import { sendData } from '@src/services/AuthService/AuthService';
+import { CustomersId } from '@src/interfaces/Customer';
+import Toastify from 'toastify-js';
+import FormInput from '../FormInput/FormInput';
+
+interface ModalType {
+ active: boolean;
+ userId: string;
+ setActive: (logic: boolean) => void;
+ city: string;
+ postalCode: string;
+ country: string;
+ streetName: string;
+ setUserAccount: React.Dispatch>;
+ userAccount: CustomersId;
+ selectedData: {
+ city: string;
+ postalCode: string;
+ country: string;
+ streetName: string;
+ addressId: string;
+ };
+ setSelectedData: React.Dispatch<
+ React.SetStateAction<{
+ addressId: string;
+ city: string;
+ postalCode: string;
+ country: string;
+ streetName: string;
+ }>
+ >;
+}
+
+const postalCodePattern: PostalCodePattern = {
+ US: '\\d{5}-\\d{4}|\\d{5}',
+ RU: '\\d{6}',
+ GB: '[A-Za-z]{1,2}\\d{1,2}[A-Za-z]?\\s?\\d[A-Za-z]{2}',
+ DE: '\\d{5}',
+ FR: '\\d{5}',
+};
+
+function Modal({
+ active,
+ setActive,
+ city,
+ userId,
+ postalCode,
+ country,
+ streetName,
+ selectedData,
+ setSelectedData,
+ setUserAccount,
+ userAccount,
+}: ModalType): JSX.Element {
+ const [isFormComplete, setIsFormComplete] = useState(false);
+
+ useEffect(() => {
+ [city, country, streetName, postalCode].every((value) => value !== '');
+ if ([city, country, streetName, postalCode].every((value) => value !== '') === true) {
+ setIsFormComplete(true);
+ } else setIsFormComplete(false);
+ }, [city, country, streetName, postalCode]);
+
+ const handleCountryChange = (event: React.ChangeEvent): void => {
+ const { id, value } = event.target;
+ setSelectedData({ ...selectedData, [id]: value });
+ };
+
+ const handleInputChange = (event: React.ChangeEvent): void => {
+ const { id, value } = event.target;
+ setSelectedData({ ...selectedData, [id]: value });
+ };
+
+ return (
+
+ );
+}
+
+export default Modal;
diff --git a/src/components/ModalAccountInformation/ModalAccountInformation.tsx b/src/components/ModalAccountInformation/ModalAccountInformation.tsx
new file mode 100644
index 0000000..5785b44
--- /dev/null
+++ b/src/components/ModalAccountInformation/ModalAccountInformation.tsx
@@ -0,0 +1,29 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+import React from 'react';
+import '../Modal/Modal.scss';
+
+function ModalAccountInformation({
+ active,
+ setActive,
+ children,
+ onSubmit,
+}: {
+ active: boolean;
+ setActive: (value: boolean) => void;
+ children: JSX.Element;
+ onSubmit: (e: React.FormEvent) => void;
+}): JSX.Element {
+ return (
+ setActive(false)}>
+
+
+ );
+}
+
+export default ModalAccountInformation;
diff --git a/src/components/ModalAccountInformationBilling/ModalAccountInformationBilling.tsx b/src/components/ModalAccountInformationBilling/ModalAccountInformationBilling.tsx
new file mode 100644
index 0000000..dc52e57
--- /dev/null
+++ b/src/components/ModalAccountInformationBilling/ModalAccountInformationBilling.tsx
@@ -0,0 +1,26 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+import React from 'react';
+import '../Modal/Modal.scss';
+
+function ModalAccountInformationBilling({
+ active,
+ setActive,
+ children,
+}: {
+ active: boolean;
+ setActive: (value: boolean) => void;
+ children: JSX.Element;
+}): JSX.Element {
+ return (
+ setActive(false)}>
+
e.stopPropagation()}
+ >
+ {children}
+
+
+ );
+}
+
+export default ModalAccountInformationBilling;
diff --git a/src/components/NameAccount/NameAccount.scss b/src/components/NameAccount/NameAccount.scss
new file mode 100644
index 0000000..7b4a1a2
--- /dev/null
+++ b/src/components/NameAccount/NameAccount.scss
@@ -0,0 +1,3 @@
+.header__account-name {
+ color: white;
+}
diff --git a/src/components/NameAccount/NameAccount.tsx b/src/components/NameAccount/NameAccount.tsx
new file mode 100644
index 0000000..2c8a2aa
--- /dev/null
+++ b/src/components/NameAccount/NameAccount.tsx
@@ -0,0 +1,50 @@
+import React, { useEffect, useState } from 'react';
+import { Link } from 'react-router-dom';
+import { CustomersId } from '@src/interfaces/Customer';
+import { getCustomerId } from '@src/services/AuthService/AuthService';
+import './NameAccount.scss';
+
+function NameAccount({ logOut }: { logOut: () => void }): JSX.Element {
+ const [data, setData] = useState({
+ email: '',
+ firstName: '',
+ lastName: '',
+ billingAddressIds: [],
+ shippingAddressIds: [],
+ defaultShippingAddressId: '',
+ defaultBillingAddressId: '',
+ dateOfBirth: '',
+ id: '',
+ addresses: [
+ {
+ city: '',
+ country: '',
+ id: '',
+ postalCode: '',
+ streetName: '',
+ },
+ ],
+ });
+
+ useEffect(() => {
+ getCustomerId().then(async (item) => {
+ setData(item);
+ });
+ }, []);
+
+ return (
+ <>
+ {/* */}
+
+
+ {data.email}
+
+
+
+ Log Out
+
+ >
+ );
+}
+
+export default NameAccount;
diff --git a/src/components/PriceRange/PriceRange.scss b/src/components/PriceRange/PriceRange.scss
new file mode 100644
index 0000000..284067d
--- /dev/null
+++ b/src/components/PriceRange/PriceRange.scss
@@ -0,0 +1,62 @@
+.price-range-slider {
+ position: relative;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 5px;
+ margin: 5px;
+ height: 80px;
+ font-family: 'Oswald', sans-serif;
+ color: #333;
+ width: 228px;
+
+ input,
+ button {
+ font-size: 14px;
+ }
+
+ .label-currency-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-bottom: 10px;
+ column-gap: 5px;
+
+ h3 {
+ margin: 0;
+ font-size: 20px;
+ }
+
+ .price-currency {
+ margin: 0;
+ font-size: 14px;
+ }
+ }
+
+ .price-input {
+ width: 45px;
+ }
+
+ .price-labels {
+ display: flex;
+ justify-content: space-between;
+ font-size: 10px;
+ margin-bottom: 10px;
+ }
+
+ .custom-slider {
+ width: 100%;
+ height: 2px;
+ background-color: #333;
+ border-radius: 4px;
+ }
+
+ .custom-thumb,
+ .custom-thumb:focus-visible {
+ width: 6px;
+ height: 20px;
+ background-color: #333;
+ bottom: -10px;
+ outline: none;
+ cursor: grab;
+ }
+}
diff --git a/src/components/PriceRange/PriceRange.tsx b/src/components/PriceRange/PriceRange.tsx
new file mode 100644
index 0000000..fd0931c
--- /dev/null
+++ b/src/components/PriceRange/PriceRange.tsx
@@ -0,0 +1,64 @@
+import React, { ChangeEvent, useState } from 'react';
+import Slider from 'react-slider';
+import './PriceRange.scss';
+
+function PriceRangeSlider({
+ min,
+ max,
+ onChange,
+}: {
+ min: number;
+ max: number;
+ onChange: (newRange: number[]) => void;
+}): JSX.Element {
+ const [range, setRange] = useState([min, max]);
+
+ const handleSliderChange = (newRange: number[]): void => {
+ setRange(newRange);
+ onChange(newRange);
+ };
+
+ const handleInputChange = (event: ChangeEvent, index: number): void => {
+ const newValue = parseInt(event.target.value, 10);
+ if (!Number.isNaN(newValue)) {
+ const newRange = [...range];
+ newRange[index] = newValue;
+ setRange(newRange);
+ onChange(newRange);
+ }
+ };
+
+ return (
+
+ );
+}
+
+export default PriceRangeSlider;
diff --git a/src/components/PriceRange/__tests__/PriceRange.test.tsx b/src/components/PriceRange/__tests__/PriceRange.test.tsx
new file mode 100644
index 0000000..95ff874
--- /dev/null
+++ b/src/components/PriceRange/__tests__/PriceRange.test.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import PriceRange from '../PriceRange';
+import '@testing-library/jest-dom';
+import 'resize-observer-polyfill';
+
+class ResizeObserver {
+ public observe(): void {}
+ public unobserve(): void {}
+ public disconnect(): void {}
+}
+
+global.ResizeObserver = ResizeObserver;
+
+test('PriceRangeSlider renders correctly', () => {
+ const min = 0;
+ const max = 100;
+ const onChange = jest.fn();
+
+ const { getByText, getByDisplayValue } = render( );
+
+ const title = getByText('Price range');
+ expect(title).toBeInTheDocument();
+
+ const currency = getByText('EUR');
+ expect(currency).toBeInTheDocument();
+
+ const minInput = getByDisplayValue('0');
+ expect(minInput).toBeInTheDocument();
+
+ const maxInput = getByDisplayValue('100');
+ expect(maxInput).toBeInTheDocument();
+
+ const slider = getByDisplayValue('0');
+ expect(slider).toBeInTheDocument();
+
+ fireEvent.change(minInput, { target: { value: '25' } });
+ fireEvent.change(maxInput, { target: { value: '75' } });
+
+ expect(onChange).toHaveBeenCalledWith([25, 75]);
+});
diff --git a/src/components/ShippingAddress/ShippingAddress.tsx b/src/components/ShippingAddress/ShippingAddress.tsx
new file mode 100644
index 0000000..7a76fd3
--- /dev/null
+++ b/src/components/ShippingAddress/ShippingAddress.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import styles from './ShippingAdress.module.scss';
+
+function ShippingAddress({
+ city,
+ country,
+ postalCode,
+ name,
+ streetName,
+}: {
+ city: string;
+ country: string;
+ postalCode: string;
+ name: string;
+ streetName: string;
+}): JSX.Element {
+ return (
+
+
{name}
+
{`${streetName}`}
+
{`${city}`}
+
{`${postalCode}`}
+
{`${country}`}
+
+ );
+}
+
+export default ShippingAddress;
diff --git a/src/components/ShippingAddress/ShippingAdress.module.scss b/src/components/ShippingAddress/ShippingAdress.module.scss
new file mode 100644
index 0000000..54bfc67
--- /dev/null
+++ b/src/components/ShippingAddress/ShippingAdress.module.scss
@@ -0,0 +1,7 @@
+div {
+ font-size: inherit;
+ color: inherit;
+ font-family: inherit;
+ font-style: inherit;
+ font-weight: inherit;
+}
diff --git a/src/components/SortingSelect/SortingSelect.scss b/src/components/SortingSelect/SortingSelect.scss
new file mode 100644
index 0000000..70d3459
--- /dev/null
+++ b/src/components/SortingSelect/SortingSelect.scss
@@ -0,0 +1,21 @@
+.sorting-select-container {
+ display: flex;
+ align-items: center;
+
+ label {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 6px;
+ font-family: 'Oswald', sans-serif;
+ font-size: 14px;
+ }
+
+ .sorting-select {
+ padding: 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 14px;
+ cursor: pointer;
+ }
+}
diff --git a/src/components/SortingSelect/SortingSelect.tsx b/src/components/SortingSelect/SortingSelect.tsx
new file mode 100644
index 0000000..6da39e3
--- /dev/null
+++ b/src/components/SortingSelect/SortingSelect.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import './SortingSelect.scss';
+
+function SortingSelect({
+ selectedOption,
+ options,
+ onSelect,
+}: {
+ selectedOption: string;
+ options: { value: string; label: string }[];
+ onSelect: (newOption: string) => void;
+}): JSX.Element {
+ return (
+
+
+ Sort by:
+ onSelect(e.target.value)}
+ >
+ {options.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+ );
+}
+
+export default SortingSelect;
diff --git a/src/components/SortingSelect/__tests__/SortingSelect.test.tsx b/src/components/SortingSelect/__tests__/SortingSelect.test.tsx
new file mode 100644
index 0000000..0230cc7
--- /dev/null
+++ b/src/components/SortingSelect/__tests__/SortingSelect.test.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import SortingSelect from '../SortingSelect';
+import '@testing-library/jest-dom';
+
+describe('SortingSelect Component', () => {
+ const options = [
+ { value: 'name.asc', label: 'Name (Ascending)' },
+ { value: 'name.desc', label: 'Name (Descending)' },
+ { value: 'price.asc', label: 'Price (Ascending)' },
+ { value: 'price.desc', label: 'Price (Descending)' },
+ ];
+
+ it('renders correctly with selected option', () => {
+ const selectedOption = 'price.asc';
+ const onSelect = jest.fn();
+
+ const { getByLabelText, getByDisplayValue } = render(
+ ,
+ );
+
+ const sortingSelect = getByLabelText('Sort by:');
+ const selectedValue = getByDisplayValue('Price (Ascending)');
+
+ expect(sortingSelect).toBeInTheDocument();
+ expect(selectedValue).toBeInTheDocument();
+ });
+
+ it('calls onSelect when an option is selected', () => {
+ const selectedOption = 'name.asc';
+ const onSelect = jest.fn();
+
+ const { getByLabelText, getByDisplayValue } = render(
+ ,
+ );
+
+ const sortingSelect = getByLabelText('Sort by:');
+ const selectedValue = getByDisplayValue('Name (Ascending)');
+
+ expect(sortingSelect).toBeInTheDocument();
+ expect(selectedValue).toBeInTheDocument();
+
+ fireEvent.change(sortingSelect, { target: { value: 'price.desc' } });
+
+ expect(onSelect).toHaveBeenCalledWith('price.desc');
+ });
+});
diff --git a/src/hooks/.gitkeep b/src/hooks/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/index.scss b/src/index.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/interfaces/Cart.ts b/src/interfaces/Cart.ts
new file mode 100644
index 0000000..bc45466
--- /dev/null
+++ b/src/interfaces/Cart.ts
@@ -0,0 +1,97 @@
+import { BaseAddress, CreatedBy, LastModifiedBy } from '@interfaces/Customer';
+import { AuthorBy } from './Product';
+
+export interface Cart {
+ type: string;
+ id: string;
+ version: number;
+ versionModifiedAt: string;
+ lastMessageSequenceNumber: number;
+ createdAt: string;
+ lastModifiedAt: string;
+ lastModifiedBy: LastModifiedBy;
+ createdBy: CreatedBy;
+ anonymousId: string;
+ lineItems: ProductInCart[];
+ cartState: string;
+ totalPrice: LinePrice;
+ shippingMode: string;
+ shipping: string[];
+ customerId: string;
+ customLineItems: string[];
+ discountCodes: {
+ discountCode: {
+ typeId: string;
+ id: string;
+ };
+ state: string;
+ }[];
+ directDiscounts: string[];
+ inventoryMode: string;
+ taxMode: string;
+ taxRoundingMode: string;
+ taxCalculationMode: string;
+ deleteDaysAfterLastModification: number;
+ refusedGifts: string[];
+ origin: string;
+ itemShippingAddresses: BaseAddress[];
+}
+
+export interface ProductInCart {
+ id: string;
+ productId: string;
+ productKey: string;
+ quantity: number;
+ price: {
+ value: LinePrice;
+ discounted?: {
+ value: LinePrice;
+ };
+ };
+ discountedPrice?: {
+ value: LinePrice;
+ };
+ totalPrice: LinePrice;
+ name: {
+ en: string;
+ };
+ variant: {
+ images: { url: string }[];
+ };
+}
+
+export interface LinePrice {
+ currencyCode: string;
+ centAmount: number;
+ fractionDigits: number;
+}
+
+export interface CartDiscount {
+ id: string;
+ version: number;
+ key: string;
+ name: {
+ en: string;
+ };
+ description: {
+ en: string;
+ };
+ value: string;
+ CartDiscountValue: string;
+ cartPredicate: string;
+ target: {
+ type: string;
+ predicate: string;
+ };
+ sortOrder: string;
+ stores: { key: string; typeId: string };
+ isActive: boolean;
+ validFrom: string;
+ validUntil: string;
+ requiresDiscountCode: boolean;
+ stackingMode: string;
+ createdAt: string;
+ createdBy: AuthorBy;
+ lastModifiedAt: string;
+ lastModifiedBy: AuthorBy;
+}
diff --git a/src/interfaces/Category.ts b/src/interfaces/Category.ts
new file mode 100644
index 0000000..af138c4
--- /dev/null
+++ b/src/interfaces/Category.ts
@@ -0,0 +1,31 @@
+import { ProductFormattedData } from './Product';
+
+export interface Category {
+ id: string;
+ name: {
+ en: string;
+ };
+ slug: {
+ en: string;
+ };
+ ancestors: [
+ {
+ typeId: string;
+ id: string;
+ },
+ ];
+ parent?: {
+ id: string;
+ };
+ used?: boolean;
+}
+
+export interface CategoryFormattedData {
+ name: string;
+ id: string;
+ ancestors: CategoryFormattedData[];
+}
+
+export interface CategoryProps {
+ category: ProductFormattedData;
+}
diff --git a/src/interfaces/Customer.ts b/src/interfaces/Customer.ts
new file mode 100644
index 0000000..e566879
--- /dev/null
+++ b/src/interfaces/Customer.ts
@@ -0,0 +1,83 @@
+export interface CustomerDraft {
+ email: string;
+ password: string;
+ firstName: string;
+ lastName: string;
+ anonymousId?: string;
+ dateOfBirth: string;
+ addresses?: BaseAddress[];
+ defaultShippingAddress?: number;
+ defaultBillingAddress?: number;
+ billingAddresses: number[];
+ shippingAddresses: number[];
+}
+
+export interface BaseAddress {
+ country: string;
+ streetName: string;
+ city: string;
+ postalCode: string;
+}
+
+export interface CustomerData {
+ id: string;
+ version: number;
+ versionModifiedAt: string;
+ lastMessageSequenceNumber: number;
+ createdAt: string;
+ lastModifiedAt: string;
+ lastModifiedBy: LastModifiedBy;
+ createdBy: CreatedBy;
+ email: string;
+ firstName: string;
+ lastName: string;
+ dateOfBirth: string;
+ password: string;
+ addresses: BaseAddress[];
+ shippingAddressIds: string[];
+ billingAddressIds: string[];
+ isEmailVerified: boolean;
+ stores: [];
+ authenticationMode: string;
+}
+
+export interface CreatedBy {
+ clientId: string;
+ isPlatformClient: boolean;
+ anonymousId: string;
+}
+
+export interface LastModifiedBy {
+ clientId: string;
+ isPlatformClient: boolean;
+ anonymousId: string;
+}
+
+export interface Address {
+ city: string;
+ country: string;
+ id: string;
+ postalCode: string;
+ streetName: string;
+}
+
+export interface SendAddress {
+ city: string;
+ postalCode: string;
+ country: string;
+ streetName: string;
+ addressId: string;
+}
+
+export interface CustomersId {
+ email: string;
+ firstName: string;
+ lastName: string;
+ billingAddressIds: string[];
+ shippingAddressIds: string[];
+ defaultShippingAddressId: string;
+ defaultBillingAddressId: string;
+ addresses: Address[];
+ dateOfBirth: string;
+ id: string;
+}
diff --git a/src/interfaces/Discount.ts b/src/interfaces/Discount.ts
new file mode 100644
index 0000000..5c76a23
--- /dev/null
+++ b/src/interfaces/Discount.ts
@@ -0,0 +1,33 @@
+import { AuthorBy } from './Product';
+
+export interface DiscountCode {
+ id: string;
+ version: number;
+ name: {
+ en: string;
+ };
+ description: {
+ en: string;
+ };
+ code: string;
+ cartDiscounts: CartDiscountReference[];
+ cartPredicate: string;
+ isActive: boolean;
+ references: { id: string; typeId: string };
+ maxApplications: number;
+ maxApplicationsPerCustomer: number;
+ groups: string[];
+ validFrom: string;
+ validUntil: string;
+ applicationVersion: number;
+ createdAt: string;
+ createdBy: AuthorBy;
+ lastModifiedAt: string;
+ lastModifiedBy: AuthorBy;
+}
+
+export interface CartDiscountReference {
+ id: string;
+ typeId: string;
+ obj: DiscountCode;
+}
diff --git a/src/interfaces/Errors.ts b/src/interfaces/Errors.ts
new file mode 100644
index 0000000..864fef1
--- /dev/null
+++ b/src/interfaces/Errors.ts
@@ -0,0 +1,9 @@
+export interface ResponseErrorItem {
+ code: string;
+ detailedErrorMessage: string;
+ message: string;
+}
+
+export interface FormErrorsInterface {
+ formErrors: { [email: string]: string; password: string };
+}
diff --git a/src/interfaces/Product.ts b/src/interfaces/Product.ts
new file mode 100644
index 0000000..2db8029
--- /dev/null
+++ b/src/interfaces/Product.ts
@@ -0,0 +1,146 @@
+export interface ProductCatalog {
+ id: string;
+ description: {
+ en: string;
+ };
+ name: {
+ en: string;
+ };
+ key: string;
+ masterVariant: {
+ id: number;
+ sku: string;
+ images: [
+ {
+ url: string;
+ },
+ ];
+ prices: [
+ {
+ id: string;
+ value: {
+ currencyCode: string;
+ centAmount: number;
+ fractionDigits: number;
+ };
+ discounted?: {
+ value: {
+ centAmount: number;
+ };
+ };
+ },
+ ];
+ attributes: [
+ {
+ name: string;
+ value: string;
+ },
+ ];
+ };
+ categories: [
+ {
+ typeId: string;
+ id: string;
+ },
+ ];
+ inCart?: boolean;
+}
+
+export interface ProductDetailedPage {
+ id: string;
+ version: number;
+ versionModifiedAt: string;
+ lastMessageSequenceNumber: number;
+ createdAt: string;
+ lastModifiedAt: string;
+ lastModifiedBy: AuthorBy;
+ createdBy: AuthorBy;
+ productType: {
+ typeId: string;
+ id: string;
+ };
+ masterData: {
+ current: {
+ name: {
+ en: string;
+ };
+ description: {
+ en: string;
+ };
+ categories: [
+ {
+ typeId: string;
+ id: string;
+ },
+ ];
+ slug: {
+ en: string;
+ };
+ masterVariant: {
+ id: number;
+ sku: string;
+ prices: [
+ {
+ id: string;
+ value: {
+ type: string;
+ currencyCode: string;
+ centAmount: number;
+ fractionDigits: number;
+ };
+ discounted: {
+ value: {
+ type: string;
+ currencyCode: string;
+ centAmount: number;
+ fractionDigits: number;
+ };
+ discount: {
+ typeId: string;
+ id: string;
+ };
+ };
+ },
+ ];
+ images: [
+ {
+ url: string;
+ label: string;
+ dimensions: {
+ w: number;
+ h: number;
+ };
+ },
+ ];
+ attributes: [
+ {
+ name: string;
+ value: string;
+ },
+ ];
+ assets: [];
+ };
+ variants: [];
+ };
+ published: boolean;
+ hasStagedChanges: boolean;
+ };
+ key: string;
+ lastVariantId: number;
+}
+
+export interface ProductFormattedData {
+ name: string;
+ id: string;
+ slug: string;
+ ancestors: ProductFormattedData[];
+ parent?: string;
+}
+
+export interface AuthorBy {
+ isPlatformClient: true;
+ user: {
+ typeId: string;
+ id: string;
+ };
+}
diff --git a/src/interfaces/Register.ts b/src/interfaces/Register.ts
new file mode 100644
index 0000000..8e0c73d
--- /dev/null
+++ b/src/interfaces/Register.ts
@@ -0,0 +1,18 @@
+import { BaseAddress } from '@interfaces/Customer';
+
+export interface RegistrationFormData {
+ email: string;
+ password: string;
+ firstName: string;
+ lastName: string;
+ dateOfBirth: string;
+ defaultShippingAddress: boolean;
+ defaultBillingAddress: boolean;
+ sameBillingShipping: boolean;
+ billingAddress: BaseAddress;
+ shippingAddress: BaseAddress;
+}
+
+export interface PostalCodePattern {
+ [key: string]: string;
+}
diff --git a/src/layouts/.gitkeep b/src/layouts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..4e4a1e8
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import App from './App';
+import './normalize.scss';
+import './index.scss';
+
+const root = document.getElementById('root');
+
+if (root) {
+ ReactDOM.createRoot(root).render(
+
+
+ ,
+ );
+}
diff --git a/src/normalize.scss b/src/normalize.scss
new file mode 100644
index 0000000..8d14d83
--- /dev/null
+++ b/src/normalize.scss
@@ -0,0 +1,343 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+ ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+ line-height: 1.15; /* 1 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+ ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+ margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+ display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+ box-sizing: content-box; /* 1 */
+ height: 0; /* 1 */
+ overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+ border-bottom: none; /* 1 */
+ text-decoration: underline; /* 2 */
+ text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+ border-style: none;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input {
+ /* 1 */
+ overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select {
+ /* 1 */
+ text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type='button']::-moz-focus-inner,
+[type='reset']::-moz-focus-inner,
+[type='submit']::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type='button']:-moz-focusring,
+[type='reset']:-moz-focusring,
+[type='submit']:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+ padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ * `fieldset` elements in all browsers.
+ */
+
+legend {
+ box-sizing: border-box; /* 1 */
+ color: inherit; /* 2 */
+ display: table; /* 1 */
+ max-width: 100%; /* 1 */
+ padding: 0; /* 3 */
+ white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+ vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type='checkbox'],
+[type='radio'] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type='number']::-webkit-inner-spin-button,
+[type='number']::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type='search'] {
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type='search']::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/* Interactive
+ ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+ display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+ display: list-item;
+}
+
+/* Misc
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+ display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+ display: none;
+}
diff --git a/src/pages/About/AboutPage.scss b/src/pages/About/AboutPage.scss
new file mode 100644
index 0000000..9e54706
--- /dev/null
+++ b/src/pages/About/AboutPage.scss
@@ -0,0 +1,86 @@
+.about__main-heading {
+ color: #000;
+ text-align: center;
+ font-family: Oswald;
+ font-size: 48px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 68px;
+ margin-bottom: 80px;
+}
+
+.about__container {
+ margin: 0 auto 50px auto;
+ display: grid;
+ max-width: 830px;
+ gap: 60px;
+ grid-template-columns: 470px 300px;
+ grid-template-rows: 300px 300px;
+
+ .about-card:last-of-type {
+ grid-column: 2;
+ grid-row: 1 / 2;
+ flex-wrap: wrap;
+ gap: 20px;
+ margin-top: 20px;
+ }
+}
+
+@media (max-width: 890px) {
+ .about__container {
+ display: flex;
+ flex-direction: column;
+ .about-card,
+ .about-card-inverted {
+ justify-content: center;
+ }
+ .about-card:last-of-type {
+ flex-wrap: nowrap;
+ margin-top: 0px;
+ gap: 40px;
+ }
+ .about-card__content {
+ max-width: 260px;
+ }
+ }
+}
+
+@media (max-width: 540px) {
+ .about__container {
+ .about-card,
+ .about-card-inverted,
+ .about-card:last-of-type {
+ flex-wrap: wrap;
+ gap: 20px;
+ }
+ }
+}
+
+.about__rs-school {
+ display: flex;
+ align-items: center;
+
+ .rs-school_logo {
+ width: 160px;
+ }
+
+ .rs-school_link {
+ margin: 10px auto;
+ }
+}
+.about__collaboration-wrapper {
+ margin: 0 auto;
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ .item {
+ padding: 0 20px;
+ max-width: 700px;
+ color: #000;
+ font-family: Oswald;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+ }
+}
diff --git a/src/pages/About/AboutPage.tsx b/src/pages/About/AboutPage.tsx
new file mode 100644
index 0000000..283a0e6
--- /dev/null
+++ b/src/pages/About/AboutPage.tsx
@@ -0,0 +1,69 @@
+import AboutCard from '@src/components/AboutCard/AboutCard';
+import React from 'react';
+import './AboutPage.scss';
+
+import howlPng from '@assets/howl.png';
+import academegPng from '@assets/academeg.png';
+import capapaJpg from '@assets/capapa.jpg';
+import { Link } from 'react-router-dom';
+
+export default function AboutPage(): JSX.Element {
+ const info = [
+ {
+ image: howlPng,
+ name: 'Arthur',
+ role: 'Team Lead',
+ github: 'howl404',
+ bio: "I'm a JavaScript sorcerer, a TypeScript tamer, and a React wrangler. An algorithm wizard and a master of procrastination. If I had an algorithm to combat procrastination, I'd probably start using it... later, maybe",
+ contributions: ['Catalog', 'About Us'],
+ inverted: false,
+ },
+ {
+ image: capapaJpg,
+ name: 'Rashit',
+ role: 'Developer',
+ github: 'capapa',
+ bio: "I love JavaScript, I love new technologies and VSCode. I'm developer. Developers are creators of beauty things!",
+ contributions: ['Product', 'Promocodes'],
+ inverted: false,
+ },
+ {
+ image: academegPng,
+ name: 'Mikhail',
+ role: 'designer & developer',
+ github: 'academeg1',
+ bio: 'I am a junior web developer. I have experience in web development and I want to develop in this area. I am passionate about new technologies and am ready to learn anything that can make my experience more valuable and increase my productivity.',
+ contributions: ['Account', 'Cart'],
+ inverted: false,
+ },
+ ];
+ return (
+ <>
+ Who are we?
+
+ {info.map((member) => (
+
+ ))}
+
+
+
+ We actively conducted code reviews among team members using a combination of tools like Jira and GitHub. This
+ ensured that each line of code met our high standards for quality and efficiency. Our teamwork extended beyond
+ just coding, we held regular discussions to brainstorm ideas, assist each other with challenges, and refine
+ our project's direction.
+
+
+ This collaborative process not only improved the overall codebase but also fostered a culture of knowledge
+ sharing and continuous improvement. We maintained a disciplined approach with regular commits and a clear pull
+ request system, which contributed to the project's success and our ability to stay organized and on track.
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/pages/AccountAddress/AccountAddress.module.scss b/src/pages/AccountAddress/AccountAddress.module.scss
new file mode 100644
index 0000000..0358278
--- /dev/null
+++ b/src/pages/AccountAddress/AccountAddress.module.scss
@@ -0,0 +1,90 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Oswald&display=swap');
+
+:root {
+ --gray-1: #828282;
+ --divider: #c4c4c4;
+ --light-blue-hover: #f0f2f2;
+ --black-2: #3f3f3f;
+ --light-blue-active: #c2c2c2;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.page__title {
+ color: #000;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 48px;
+ font-style: normal;
+ font-weight: 400;
+ text-align: center;
+ margin: 0;
+}
+
+h3 {
+ color: #000;
+ font-family: 'Oswald', sans-serif;
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 400;
+}
+
+.dashboard__information {
+ display: flex;
+ justify-content: space-evenly;
+ margin-top: 17px;
+ padding: 73px 0 73px 0;
+ border: 1px solid var(--divider);
+}
+
+.link {
+ text-decoration: none;
+}
+
+.dashboard__description {
+ display: block;
+ max-width: 400px;
+ flex: 1;
+}
+
+.block__address {
+ margin: 25px 0;
+}
+
+.account__information_blockTitle {
+ color: var(--black-2);
+ font-family: 'Oswald', sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ text-transform: uppercase;
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+
+.block__address {
+ color: var(--gray-1);
+ font-family: 'Roboto', sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 400;
+ display: flex;
+ gap: 10px;
+}
+
+.btn__edit {
+ color: black;
+ cursor: pointer;
+ width: 25px;
+ height: 25px;
+}
+
+.form_default_address {
+ display: flex;
+ gap: 10px;
+}
diff --git a/src/pages/AccountAddress/AccountAddress.tsx b/src/pages/AccountAddress/AccountAddress.tsx
new file mode 100644
index 0000000..64ab7e4
--- /dev/null
+++ b/src/pages/AccountAddress/AccountAddress.tsx
@@ -0,0 +1,337 @@
+import React, { useEffect, useState } from 'react';
+import ShippingAddress from '@src/components/ShippingAddress/ShippingAddress';
+import BillingAddress from '@src/components/BillingAddress/BillingAddress';
+import { Address, CustomersId } from '@src/interfaces/Customer';
+import { FaCheck, FaEdit, FaAddressBook, FaRegSave } from 'react-icons/fa';
+import { AiOutlineDelete } from 'react-icons/ai';
+import Modal from '@src/components/Modal/Modal';
+import {
+ getCustomerId,
+ requestAddBillingAddress,
+ requestAddShippingAddress,
+ requestDefaultBillingAddress,
+ requestDefaultShippingAddress,
+ requestIdBillingAddress,
+ requestIdShippingAddress,
+ requestRemoveAddress,
+} from '@src/services/AuthService/AuthService';
+import ModalAccountInformation from '@src/components/ModalAccountInformation/ModalAccountInformation';
+import FormInput from '@src/components/FormInput/FormInput';
+import { PostalCodePattern } from '@src/interfaces/Register';
+import styles from './AccountAddress.module.scss';
+
+const postalCodePattern: PostalCodePattern = {
+ US: '\\d{5}-\\d{4}|\\d{5}',
+ RU: '\\d{6}',
+ GB: '[A-Za-z]{1,2}\\d{1,2}[A-Za-z]?\\s?\\d[A-Za-z]{2}',
+ DE: '\\d{5}',
+ FR: '\\d{5}',
+};
+
+function AccountAddress(): JSX.Element {
+ const [modalActive, setModalActive] = useState(false);
+ const [modalActiveNewAddress, setModalActiveNewAddress] = useState(false);
+ const [modalActiveNewAddressBilling, setModalActiveNewAddressBilling] = useState(false);
+ const [userAccount, setUserAccount] = useState({
+ email: '',
+ firstName: '',
+ lastName: '',
+ billingAddressIds: [],
+ shippingAddressIds: [],
+ dateOfBirth: '',
+ id: '',
+ defaultShippingAddressId: '',
+ defaultBillingAddressId: '',
+ addresses: [
+ {
+ city: '',
+ country: '',
+ id: '',
+ postalCode: '',
+ streetName: '',
+ },
+ ],
+ });
+
+ const [selectedData, setSelectedData] = useState({
+ city: '',
+ postalCode: '',
+ country: '',
+ streetName: '',
+ addressId: '',
+ });
+ const [newAddress, setNewAddress] = useState({
+ city: '',
+ postalCode: '',
+ country: '',
+ streetName: '',
+ id: '',
+ });
+ const [checkBoxBilling, setCheckBoxBilling] = useState(false);
+
+ const [isFormComplete, setIsFormComplete] = useState(false);
+
+ useEffect(() => {
+ if (
+ [newAddress.streetName, newAddress.postalCode, newAddress.city, newAddress.country].every(
+ (value) => value !== '',
+ ) === true
+ ) {
+ setIsFormComplete(true);
+ } else setIsFormComplete(false);
+ }, [newAddress.streetName, newAddress.postalCode, newAddress.city, newAddress.country]);
+
+ function modalWindow(
+ modalActiveM: boolean,
+ setModalActiveM: React.Dispatch>,
+ requestAddress: (streetName: string, postalCode: string, city: string, country: string) => Promise,
+ requestIdAddress: (addressId: string) => Promise,
+ requestDefaultAddress: (addressId: string) => Promise,
+ idModal: string,
+ ): JSX.Element {
+ return (
+ {
+ e.preventDefault();
+ requestAddress(newAddress.streetName, newAddress.postalCode, newAddress.city, newAddress.country).then(
+ (item) => {
+ const addId = item.addresses[item.addresses.length - 1].id;
+ requestIdAddress(addId).then((items) => {
+ if (checkBoxBilling) {
+ requestDefaultAddress(addId).then((itema) => setUserAccount({ ...itema }));
+ setCheckBoxBilling(false);
+ } else {
+ setUserAccount({ ...items });
+ }
+ setModalActiveM(false);
+ });
+ },
+ );
+ }}
+ >
+ <>
+
+
Set default address
+
{
+ setCheckBoxBilling(e.target.checked);
+ }}
+ />
+
+
+
+ Country
+ setNewAddress({ ...newAddress, country: e.target.value })}
+ >
+
+ Select a country ...
+
+
+ United States
+
+
+ Russia
+
+
+ United Kingdom
+
+
+ Germany
+
+
+ France
+
+
+
+
+ setNewAddress({ ...newAddress, city: e.target.value })}
+ id={idModal}
+ type="text"
+ pattern="[A-Za-z]+"
+ title="Must contain only letters"
+ value={newAddress.city}
+ />
+ setNewAddress({ ...newAddress, postalCode: e.target.value })}
+ id={idModal}
+ type="text"
+ pattern={postalCodePattern[newAddress.country] || '.*'}
+ title="Must be a valid postal code of a selected country"
+ value={newAddress.postalCode}
+ />
+ setNewAddress({ ...newAddress, streetName: e.target.value })}
+ id={idModal}
+ type="text"
+ pattern=".*"
+ title="Must contain more than 1 character"
+ value={newAddress.streetName}
+ />
+
+ add new address
+ {' '}
+ >
+
+ );
+ }
+
+ useEffect(() => {
+ getCustomerId().then((item) => setUserAccount(item));
+ }, []);
+
+ const [billingAddress, setBillingAddress] = useState([]);
+ const [shippingAddress, setShippingAddress] = useState([]);
+ // const { billingAddressIds, shippingAddressIds } = userAccount;
+
+ useEffect(() => {
+ const bill = userAccount.addresses.filter((item) => userAccount.billingAddressIds.includes(item.id));
+ setBillingAddress(bill);
+ const ship = userAccount.addresses.filter((item) => userAccount.shippingAddressIds.includes(item.id));
+ setShippingAddress(ship);
+ }, [userAccount, setUserAccount]);
+
+ const billingAddressArr = billingAddress.map(({ id, city, postalCode, country, streetName }) => {
+ const isDefault = id === userAccount.defaultBillingAddressId;
+ return (
+
+ {isDefault &&
}
+
{
+ setModalActive(true);
+ setSelectedData({ city, postalCode, country, streetName, addressId: id });
+ }}
+ />
+ {
+ requestRemoveAddress(id).then((item) => {
+ setUserAccount({ ...item });
+ });
+ }}
+ />
+
+
+ );
+ });
+
+ const shippingAddressArr = shippingAddress.map(({ id, city, postalCode, country, streetName }) => {
+ const isDefault = id === userAccount.defaultShippingAddressId;
+ return (
+
+ {isDefault &&
}
+
+
{
+ setSelectedData({ city, postalCode, country, streetName, addressId: id });
+ setModalActive(true);
+ }}
+ />
+ {
+ requestRemoveAddress(id).then((item) => {
+ setUserAccount({ ...item });
+ });
+ }}
+ />
+
+
+ );
+ });
+
+ return (
+
+
Edit Address Information
+
+
+
+ Billing Adresses{' '}
+ {
+ setModalActiveNewAddressBilling(true);
+ }}
+ >
+ +
+
+
+ {modalActiveNewAddressBilling &&
+ modalWindow(
+ modalActiveNewAddressBilling,
+ setModalActiveNewAddressBilling,
+ requestAddBillingAddress,
+ requestIdBillingAddress,
+ requestDefaultBillingAddress,
+ 'billing',
+ )}
+
+ {billingAddress ? billingAddressArr :
You have not set a default billing address. }
+
+
+ Shipping Adresses{' '}
+ setModalActiveNewAddress(true)}>
+ +
+
+ {modalActiveNewAddress &&
+ modalWindow(
+ modalActiveNewAddress,
+ setModalActiveNewAddress,
+ requestAddShippingAddress,
+ requestIdShippingAddress,
+ requestDefaultShippingAddress,
+ 'shipping',
+ )}
+
+ {shippingAddress ? shippingAddressArr :
You have not set a default shipping address. }
+
+
+
+ );
+}
+
+export default AccountAddress;
diff --git a/src/pages/AccountDashboard/AccountDashboard.scss b/src/pages/AccountDashboard/AccountDashboard.scss
new file mode 100644
index 0000000..9fd5e62
--- /dev/null
+++ b/src/pages/AccountDashboard/AccountDashboard.scss
@@ -0,0 +1,55 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Oswald&display=swap');
+
+:root {
+ --gray-1: #828282;
+ --divider: #c4c4c4;
+ --light-blue-hover: #f0f2f2;
+ --black-2: #3f3f3f;
+ --light-blue-active: #c2c2c2;
+}
+
+.dashboard__container {
+ max-width: 1200px;
+ margin: 0 auto;
+
+ .dashboard__page__title {
+ color: #000;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 48px;
+ font-style: normal;
+ font-weight: 400;
+ text-align: center;
+ margin: 0;
+ }
+
+ .dashboard__information {
+ display: flex;
+ justify-content: space-evenly;
+ margin-top: 17px;
+ padding: 73px 0 73px 0;
+ border: 1px solid var(--divider);
+ }
+
+ .link {
+ text-decoration: none;
+ }
+}
+
+@media (max-width: 760px) {
+ .dashboard__information {
+ flex-direction: column;
+ div {
+ margin: 0 auto;
+ }
+ }
+}
+
+@media (max-width: 401px) {
+ .dashboard__information {
+ .form-input {
+ max-width: 320px;
+ }
+ }
+}
diff --git a/src/pages/AccountDashboard/AccountDashboard.tsx b/src/pages/AccountDashboard/AccountDashboard.tsx
new file mode 100644
index 0000000..dedeeb8
--- /dev/null
+++ b/src/pages/AccountDashboard/AccountDashboard.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import Breadcrumbs from '@src/components/Breadcrumbs/Breadcrumbs';
+import AccountMenu from '@src/components/AccountMenu/AccountMenu';
+import { Route, Routes } from 'react-router-dom';
+import Profile from '../Profile/Profile';
+import './AccountDashboard.scss';
+import AccountInformation from '../AccountInformation/AccountInformation';
+import AccountAddress from '../AccountAddress/AccountAddress';
+import AccountOrder from '../AccountOrder/AccountOrder';
+
+function AccountDashboard({ onLogOut }: { onLogOut: () => void }): JSX.Element {
+ return (
+
+
+
+
My Dashboard
+
+
+
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ );
+}
+
+export default AccountDashboard;
diff --git a/src/pages/AccountInformation/AccountInformation.module.scss b/src/pages/AccountInformation/AccountInformation.module.scss
new file mode 100644
index 0000000..7e22150
--- /dev/null
+++ b/src/pages/AccountInformation/AccountInformation.module.scss
@@ -0,0 +1,100 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Oswald&display=swap');
+
+:root {
+ --gray-1: #828282;
+ --divider: #c4c4c4;
+ --light-blue-hover: #f0f2f2;
+ --black-2: #3f3f3f;
+ --light-blue-active: #c2c2c2;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.page__title {
+ color: #000;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 48px;
+ font-style: normal;
+ font-weight: 400;
+ text-align: center;
+ margin: 0;
+}
+
+h3 {
+ color: #000;
+ font-family: 'Oswald', sans-serif;
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 400;
+}
+
+.dashboard__information {
+ display: flex;
+ justify-content: space-evenly;
+ margin-top: 17px;
+ padding: 73px 0 73px 0;
+ border: 1px solid var(--divider);
+}
+
+.link {
+ text-decoration: none;
+}
+
+.btn {
+ color: #fff;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ width: 200px;
+ height: 35px;
+ background: black;
+ &:hover {
+ background: rgba(0, 0, 0, 0.685);
+ }
+}
+
+.btn__save {
+ color: #fff;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ width: 150px;
+ height: 50px;
+ background: black;
+ text-transform: uppercase;
+ &:hover {
+ background: rgba(0, 0, 0, 0.685);
+ }
+}
+
+.account__information_block {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.align {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+}
+
+.gapPass {
+ margin-top: 10px;
+}
+
+.btn__save:disabled {
+ background-color: #4e4e4eca;
+}
diff --git a/src/pages/AccountInformation/AccountInformation.tsx b/src/pages/AccountInformation/AccountInformation.tsx
new file mode 100644
index 0000000..6c40ac2
--- /dev/null
+++ b/src/pages/AccountInformation/AccountInformation.tsx
@@ -0,0 +1,303 @@
+import React, { useEffect, useState } from 'react';
+import FormInput from '@src/components/FormInput/FormInput';
+import {
+ changeDateofBirthRequest,
+ changeEmailRequest,
+ changeFirstNameRequest,
+ changeLastNameRequest,
+ changePasswordRequest,
+ getCustomerId,
+ // getCustomerId,
+} from '@src/services/AuthService/AuthService';
+import { CustomersId } from '@src/interfaces/Customer';
+import { FaEdit, FaRegSave, FaExchangeAlt } from 'react-icons/fa';
+import ModalAccountInformation from '@src/components/ModalAccountInformation/ModalAccountInformation';
+import Toastify from 'toastify-js';
+import styles from './AccountInformation.module.scss';
+
+function AccountInformation({ onLogOut }: { onLogOut: () => void }): JSX.Element {
+ const [user, setUser] = useState({
+ email: '',
+ firstName: '',
+ lastName: '',
+ billingAddressIds: [],
+ shippingAddressIds: [],
+ defaultShippingAddressId: '',
+ defaultBillingAddressId: '',
+ addresses: [],
+ dateOfBirth: '',
+ id: '',
+ });
+
+ const [emailInformation, setEmailInformation] = useState(user.email);
+ const [editUserInformation, setEditUserInformation] = useState({
+ firstName: user.firstName,
+ lastName: user.lastName,
+ dateOfBirth: user.dateOfBirth,
+ });
+ useEffect(() => {
+ setUser({ ...user, email: emailInformation });
+ getCustomerId().then((item) => {
+ setUser(item);
+ setEditUserInformation({ firstName: item.firstName, lastName: item.lastName, dateOfBirth: item.dateOfBirth });
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [emailInformation]);
+
+ const [modalActive, setModalActive] = useState(false);
+ const [modalActiveEmail, setModalActiveEmail] = useState(false);
+ const [passwordInformation, setPasswordInformation] = useState({ oldPasswowrd: '', newPassword: '' });
+
+ const [editInformation, setEditInformation] = useState(false);
+
+ function getFormattedDate(): string {
+ const currentDate = new Date(Date.now());
+ const currentYear = currentDate.getFullYear();
+ currentDate.setFullYear(currentYear - 13);
+
+ const year = currentDate.getFullYear();
+ const month = String(currentDate.getMonth() + 1).padStart(2, '0');
+ const day = String(currentDate.getDate()).padStart(2, '0');
+
+ const formattedDate = `${year}-${month}-${day}`;
+ return formattedDate;
+ }
+
+ const [isEmailInputExist, setEmailInputExist] = useState(false);
+
+ const [isFormComplete, setIsFormComplete] = useState(false);
+
+ const [isPasswordInputsExist, setIsPasswordInputsExist] = useState(false);
+
+ useEffect(() => {
+ if (passwordInformation.oldPasswowrd !== '' && passwordInformation.newPassword !== '') {
+ setIsPasswordInputsExist(true);
+ } else {
+ setIsPasswordInputsExist(false);
+ }
+ }, [passwordInformation.oldPasswowrd, passwordInformation.newPassword]);
+
+ useEffect(() => {
+ if (emailInformation !== '') {
+ setEmailInputExist(true);
+ } else {
+ setEmailInputExist(false);
+ }
+ }, [emailInformation]);
+
+ useEffect(() => {
+ if (
+ [editUserInformation.firstName, editUserInformation.dateOfBirth, editUserInformation.lastName].every(
+ (value) => value !== '',
+ ) === true
+ ) {
+ setIsFormComplete(true);
+ } else setIsFormComplete(false);
+ }, [editUserInformation.firstName, editUserInformation.dateOfBirth, editUserInformation.lastName]);
+
+ return (
+
+
Edit Account Information
+
+ setEditInformation(!editInformation)}
+ >
+ {!editInformation ? 'Edit' : 'Close edit'} Information
+
+
+
+ {
+ e.preventDefault();
+ changeEmailRequest(emailInformation).then((item) => {
+ if (item !== undefined) {
+ Toastify({
+ text: 'Email is successfully update!',
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(315deg, #7ee8fa 0%, #80ff72 74%)',
+ },
+ }).showToast();
+ setModalActiveEmail(false);
+ }
+ });
+ }}
+ >
+ <>
+ Edit email
+ {
+ setEmailInformation(e.target.value);
+ }}
+ id="email"
+ type="text"
+ pattern="[A-Za-z0-9._+\-']+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}"
+ title="Must contain a valid email"
+ value={emailInformation}
+ />
+
+ save email
+
+ >
+
+ {
+ e.preventDefault();
+ changePasswordRequest(passwordInformation.newPassword, passwordInformation.oldPasswowrd).then((item) => {
+ if (item !== undefined) {
+ Toastify({
+ text: 'Password is successfully update!',
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(315deg, #7ee8fa 0%, #80ff72 74%)',
+ },
+ }).showToast();
+ onLogOut();
+ }
+ });
+ }}
+ >
+ <>
+ Edit password
+ setPasswordInformation({ ...passwordInformation, oldPasswowrd: e.target.value })}
+ id="old_password"
+ type="password"
+ pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
+ title="Minimum 8 characters, at least 1 uppercase letter, 1 lowercase letter, and 1 number"
+ value={passwordInformation.oldPasswowrd}
+ />
+ setPasswordInformation({ ...passwordInformation, newPassword: e.target.value })}
+ id="new_password"
+ type="password"
+ pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
+ title="Minimum 8 characters, at least 1 uppercase letter, 1 lowercase letter, and 1 number"
+ value={passwordInformation.newPassword}
+ />
+
+ save password
+
+ >
+
+
+
+ );
+}
+
+export default AccountInformation;
diff --git a/src/pages/AccountOrder/AccountOrder.module.scss b/src/pages/AccountOrder/AccountOrder.module.scss
new file mode 100644
index 0000000..88963e1
--- /dev/null
+++ b/src/pages/AccountOrder/AccountOrder.module.scss
@@ -0,0 +1,38 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Oswald&display=swap');
+
+:root {
+ --gray-1: #828282;
+ --divider: #c4c4c4;
+ --light-blue-hover: #f0f2f2;
+ --black-2: #3f3f3f;
+ --light-blue-active: #c2c2c2;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.page__title {
+ color: #000;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 48px;
+ font-style: normal;
+ font-weight: 400;
+ text-align: center;
+ margin: 0;
+}
+
+.dashboard__information {
+ display: flex;
+ justify-content: space-evenly;
+ margin-top: 17px;
+ padding: 73px 0 73px 0;
+ border: 1px solid var(--divider);
+}
+
+.link {
+ text-decoration: none;
+}
diff --git a/src/pages/AccountOrder/AccountOrder.tsx b/src/pages/AccountOrder/AccountOrder.tsx
new file mode 100644
index 0000000..b0436fb
--- /dev/null
+++ b/src/pages/AccountOrder/AccountOrder.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+// import Breadcrumbs from '@src/components/Breadcrumbs/Breadcrumbs';
+// import AccountMenu from '@src/components/AccountMenu/AccountMenu';
+// import styles from './AccountOrder.module.scss';
+
+function AccountOrder(): JSX.Element {
+ return Account order
;
+}
+
+export default AccountOrder;
diff --git a/src/pages/Basket/Basket.scss b/src/pages/Basket/Basket.scss
new file mode 100644
index 0000000..511df01
--- /dev/null
+++ b/src/pages/Basket/Basket.scss
@@ -0,0 +1,284 @@
+@import url('https://fonts.googleapis.com/css2?family=Oswald&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
+
+.crumbs {
+ text-align: center;
+}
+
+h2 {
+ font-family: 'Oswald', sans-serif;
+ font-size: 48px;
+ font-style: normal;
+ color: black;
+ text-align: center;
+ margin-top: 15px;
+ margin-bottom: 55px;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.cart {
+ display: flex;
+ justify-content: center;
+ gap: 70px;
+ flex-wrap: wrap;
+}
+
+// .cart-main {
+// }
+
+.main-list-cart {
+ max-width: 900px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.table-text {
+ &:first-child {
+ text-align: left;
+ }
+ text-transform: uppercase;
+ color: #828282;
+ font-family: 'Oswald', sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 500;
+ border-bottom: 2px solid #c4c4c4;
+ padding-bottom: 18px;
+}
+
+thead {
+ margin-bottom: 25px;
+}
+
+.sub-information-list-cart {
+ display: flex;
+ flex-direction: column;
+ max-width: 500px;
+ width: 100%;
+ button {
+ font-family: 'Oswald', sans-serif;
+ width: 100.8%;
+ height: 55px;
+ background: black;
+ color: #f0f2f2;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ text-transform: uppercase;
+ }
+}
+
+.discount-code {
+ width: 100%;
+ border: 2px solid #c4c4c4;
+ background: #f0f2f2;
+ margin-bottom: 55px;
+ h3 {
+ margin: 36px 0 0 32px;
+ }
+
+ .div-input,
+ input {
+ margin: 22px 0 43px 32px;
+ font-family: 'Roboto', sans-serif;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ // font-family: 'Roboto', sans-serif;
+ border: 1px solid #c4c4c4;
+ height: 44px;
+ max-width: 410px;
+ width: 75%;
+ background: #fff;
+ padding-left: 20px;
+ display: flex;
+ align-items: center;
+ }
+}
+
+.subtotal-sum,
+.subtotal-discount,
+.oreder-total {
+ display: flex;
+ justify-content: space-between;
+ font-family: 'Oswald', sans-serif;
+ margin: 0 20px;
+}
+
+.subtotal-sum {
+ font-family: 'Oswald', sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 500;
+}
+
+.subtotal-discount {
+ font-family: 'Oswald', sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 500;
+ color: #828282;
+}
+
+.oreder-total {
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 400;
+}
+
+.total-sum-block {
+ width: 100%;
+ border: 2px solid #c4c4c4;
+ background: #f0f2f2;
+ display: flex;
+ flex-direction: column;
+ gap: 25px;
+ border-bottom: none;
+ padding: 45px 0 30px 0;
+}
+
+.cart-information {
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 500;
+ padding-top: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 35px;
+}
+
+.button__to-catalog {
+ border: none;
+ margin-top: 15px;
+ background: #000;
+ color: #fff;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ width: 150px;
+ height: 50px;
+ transition: all 0.3s;
+}
+
+.button__to-catalog:hover {
+ background: #434343;
+ border-radius: 5px;
+}
+
+.clear-cart {
+ margin-top: 20px;
+ margin-right: 20px;
+ align-items: flex-end;
+ border: none;
+ background: #000;
+ color: #fff;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ width: 150px;
+ height: 50px;
+ transition: all 0.3s;
+ align-self: flex-end;
+}
+
+.clear-cart:hover {
+ background: #434343;
+ border-radius: 5px;
+}
+
+.container-button-clear {
+ text-align: end;
+}
+
+.title-for-empty {
+ text-align: center;
+ padding-top: 25px;
+ font-family: 'Oswald', sans-serif;
+ font-size: 18px;
+}
+
+.title-for-empty > a > button {
+ margin: 0;
+}
+.loader {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ text-align: center;
+}
+@media (max-width: 1500px) {
+ .cart-main {
+ flex-direction: column;
+ align-items: center;
+ padding: 0 20px;
+ overflow: auto;
+ }
+}
+
+@media (max-width: 890px) {
+ .cart-item-container .cart-image {
+ max-width: 100px;
+ max-height: 100px;
+ text-align: left;
+ }
+}
+
+@media (max-width: 600px) {
+ .cart {
+ gap: 30px;
+ }
+
+ .sub-information-list-cart {
+ padding: 0 10px;
+ }
+
+ .discount-code {
+ margin-bottom: 30px;
+ }
+}
+
+.clear-modal {
+ font-family: 'Oswald', sans-serif;
+ width: 100vw;
+ height: 100vh;
+ color: white;
+ background-color: rgba(0, 0, 0, 0.663);
+ position: fixed;
+ top: 0;
+ left: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: all 0.5s;
+ flex-direction: column;
+ z-index: 999;
+ p {
+ font-size: 18px;
+ }
+ div {
+ display: flex;
+ gap: 10px;
+ }
+ button {
+ text-transform: uppercase;
+ padding: 1rem;
+ background-color: black;
+ color: white;
+ }
+}
diff --git a/src/pages/Basket/Basket.tsx b/src/pages/Basket/Basket.tsx
new file mode 100644
index 0000000..a1c2624
--- /dev/null
+++ b/src/pages/Basket/Basket.tsx
@@ -0,0 +1,348 @@
+import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
+import './Basket.scss';
+import Breadcrumbs from '@src/components/Breadcrumbs/Breadcrumbs';
+import CartItem from '@src/components/CartItem/CartItem';
+import Cookies from 'js-cookie';
+import { Cart } from '@src/interfaces/Cart';
+import {
+ addDiscountCode,
+ getCartById,
+ removeDiscountCode,
+ removeFromCart,
+} from '@src/services/CartService/CartService';
+import { Link } from 'react-router-dom';
+import { getDiscountCodeById } from '@src/services/DiscountService/DiscountService';
+import { ClipLoader } from 'react-spinners';
+import returnCartPrice from '@src/utilities/returnCartPrice';
+import getCookieToken from '@src/utilities/getCookieToken';
+
+function Basket({ setTotalSumInCart }: { setTotalSumInCart: Dispatch> }): JSX.Element {
+ const [cart, setCart] = useState({
+ type: '',
+ id: '',
+ version: 0,
+ versionModifiedAt: '',
+ lastMessageSequenceNumber: 0,
+ createdAt: '',
+ lastModifiedAt: '',
+ anonymousId: '',
+ lineItems: [],
+ lastModifiedBy: { clientId: '', isPlatformClient: false, anonymousId: '' },
+ createdBy: { clientId: '', isPlatformClient: false, anonymousId: '' },
+ cartState: '',
+ customerId: '',
+ totalPrice: { centAmount: 0, currencyCode: '', fractionDigits: 2 },
+ shippingMode: '',
+ shipping: [],
+ customLineItems: [],
+ discountCodes: [],
+ directDiscounts: [],
+ inventoryMode: '',
+ taxMode: '',
+ taxRoundingMode: '',
+ taxCalculationMode: '',
+ deleteDaysAfterLastModification: 0,
+ refusedGifts: [],
+ origin: '',
+ itemShippingAddresses: [],
+ });
+
+ const [loading, setLoading] = useState(true);
+
+ const onLoaded = (): void => {
+ setLoading(false);
+ };
+
+ const [currentDiscountCode, setCurrentDiscountCode] = useState('');
+
+ const applyPromoCode = (event: React.MouseEvent): void => {
+ async function fetchData(): Promise {
+ const token = await getCookieToken();
+
+ if (!token) return;
+
+ const cartId = Cookies.get('cart-id') as string;
+ const btnNode = event.target as HTMLElement;
+ const inputNode = btnNode.previousElementSibling as HTMLInputElement;
+ const code = inputNode.value.trim();
+ const cartDiscount = await addDiscountCode(token, cartId, cart.version, code);
+ if (cartDiscount) {
+ setCart(cartDiscount);
+ }
+ }
+ fetchData();
+ };
+
+ const deletePromoCode = (): void => {
+ async function fetchData(): Promise {
+ const token = await getCookieToken();
+
+ if (!token) return;
+
+ const cartId = Cookies.get('cart-id') as string;
+ const cartDiscount = await removeDiscountCode(token, cartId, cart.version, cart.discountCodes[0].discountCode.id);
+ if (cartDiscount) {
+ setCart(cartDiscount);
+ }
+ }
+ fetchData();
+ };
+
+ const [isModalOpen, setModalOpen] = useState(false);
+
+ const handleClearCart = (): void => {
+ let i = cart.lineItems.length;
+ const anonFunc = (version: number): void => {
+ i -= 1;
+ if (i === -1) return;
+
+ getCookieToken().then((token) => {
+ if (token) {
+ removeFromCart(token, cart.id, cart.lineItems[i].id, version).then((item) => {
+ if (i === 0) setCart(item);
+ anonFunc(item.version);
+ });
+ }
+ });
+ };
+ anonFunc(cart.version);
+ };
+
+ const openModal = (): void => {
+ setModalOpen(true);
+ };
+
+ const closeModal = (): void => {
+ setModalOpen(false);
+ };
+
+ const confirmAndClearCart = (): void => {
+ handleClearCart();
+ closeModal();
+ };
+
+ const [cartItems, setCartItems] = useState([]);
+ const [totalCart, setTotalCart] = useState<{
+ centAmount: number;
+ centAmountDiscount: number;
+ centAmountDiscountPromo: number;
+ centSubtotal: number;
+ currencyCode: string;
+ fractionDigits: number;
+ }>({
+ centAmount: 0,
+ centAmountDiscount: 0,
+ centAmountDiscountPromo: 0,
+ centSubtotal: 0,
+ currencyCode: 'EUR',
+ fractionDigits: 2,
+ });
+
+ useEffect(() => {
+ const cartId = Cookies.get('cart-id');
+ if (cartId) {
+ getCookieToken().then((token) => {
+ if (token) {
+ getCartById(token, cartId).then((carta) => {
+ setCart(carta);
+ onLoaded();
+ });
+ }
+ });
+ } else {
+ onLoaded();
+ }
+ }, []);
+
+ useEffect(() => {
+ const carts = cart.lineItems.map(({ variant, name, id, quantity, price, discountedPrice }) => (
+
+ ));
+
+ const objDiscount = cart.lineItems.reduce(
+ (acc, val) => {
+ const price = val.price.value.centAmount;
+ const discountPrice = val.price.discounted?.value.centAmount || 0;
+ const discountedPrice = price - (val.price.discounted?.value.centAmount || price);
+ const lastPrice = discountPrice || price;
+ const discountedPricePromo = lastPrice - (val.discountedPrice?.value.centAmount || lastPrice);
+ return {
+ centSubtotal: price * val.quantity + acc.centSubtotal,
+ discountedPrice: discountedPrice * val.quantity + acc.discountedPrice,
+ discountedPricePromo: discountedPricePromo * val.quantity + acc.discountedPricePromo,
+ };
+ },
+ { centSubtotal: 0, discountedPrice: 0, discountedPricePromo: 0 },
+ );
+
+ setTotalCart({
+ ...cart.totalPrice,
+ centSubtotal: objDiscount.centSubtotal,
+ centAmountDiscount: objDiscount.discountedPrice,
+ centAmountDiscountPromo: objDiscount.centSubtotal - objDiscount.discountedPrice - cart.totalPrice.centAmount,
+ });
+ setCartItems(carts);
+
+ returnCartPrice().then((cartPrice) => {
+ if (cartPrice !== false) {
+ setTotalSumInCart(cartPrice);
+ }
+ });
+
+ async function fetchData(): Promise {
+ if (cart.discountCodes.length) {
+ const discountInfo = await getDiscountCodeById(cart.discountCodes[0].discountCode.id);
+ if (discountInfo) setCurrentDiscountCode(discountInfo.code);
+ }
+ }
+ fetchData();
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [cart]);
+
+ const getFormattedSum = (sum: number): string =>
+ new Intl.NumberFormat('en-IN', {
+ style: 'currency',
+ currency: 'EUR',
+ }).format(sum / 100);
+
+ let content = (
+
+ loading
+
+ );
+ if (loading) {
+ content = (
+
+
+
+
+
+ );
+ } else {
+ content =
+ cart.lineItems.length !== 0 ? (
+ <>
+ {cartItems}
+ {/*
+
+
+
+ */}
+ >
+ ) : (
+ <>
+
+
+ Your cart is empty... try to find and add new products :)
+
+
+
+
+
+
+ Catalog
+
+
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+ Shopping Cart
+ {isModalOpen && (
+
+
Are you sure you want to clear your cart?
+
+
+ Yes
+
+
+ No
+
+
+
+ )}
+
+
+
+
+
+
+ Product
+
+ Price
+ Quantity
+ Total
+
+
+
+ {content}
+
+
+ {cart.lineItems.length !== 0 && (
+
+ clear cart
+
+ )}
+
+
+
+ {cart.discountCodes.length ? (
+ <>
+
Delete Discount Code
+ {/*
*/}
+
{currentDiscountCode}
+
deletePromoCode()}>
+ delete discount
+
+ >
+ ) : (
+ <>
+
Apply Discount Code
+
+
+ Apply
+
+ >
+ )}
+
+
+
+
Subtotal
+
{getFormattedSum(totalCart.centSubtotal)}
+
+
+
Discount
+
{getFormattedSum(totalCart.centAmountDiscount)}
+
+
+
Promocode
+
{getFormattedSum(totalCart.centAmountDiscountPromo)}
+
+
+
ORDER TOTAL
+
{getFormattedSum(totalCart.centAmount)}
+
+
+
proceed to checkout
+
+
+ >
+ );
+}
+
+export default Basket;
diff --git a/src/pages/Catalog/CatalogPage.scss b/src/pages/Catalog/CatalogPage.scss
new file mode 100644
index 0000000..e2897cd
--- /dev/null
+++ b/src/pages/Catalog/CatalogPage.scss
@@ -0,0 +1,143 @@
+.catalog-content {
+ flex-direction: column;
+
+ .filter-list {
+ width: 250px;
+ }
+
+ .main-content {
+ margin-bottom: 50px;
+ }
+
+ &,
+ .main-content,
+ .categories,
+ .catalog-header,
+ .sort-container,
+ .info-header,
+ .options-header {
+ display: flex;
+ justify-content: center;
+ }
+
+ .info-header {
+ margin-bottom: 10px;
+ }
+
+ .catalog-header {
+ flex-direction: column;
+ justify-content: space-around;
+ margin-top: 6px;
+ a {
+ text-decoration: none;
+ color: #333;
+ transition: 200ms;
+ border-bottom: 1px solid transparent;
+ &:hover {
+ border-bottom: 1px solid #333;
+ }
+ }
+ }
+
+ .sort-container,
+ .info-header,
+ .options-header {
+ flex-wrap: wrap;
+ position: relative;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ }
+}
+
+@media (max-width: 980px) {
+ .categories {
+ position: absolute;
+ z-index: -1;
+ opacity: 0;
+ top: 40px;
+ flex-direction: column;
+ }
+
+ .categories-open {
+ opacity: 1;
+ z-index: 3;
+ .category {
+ margin: 0;
+ }
+ }
+
+ .open-categories {
+ display: block;
+ }
+}
+
+@media (max-width: 620px) {
+ .main-content {
+ flex-wrap: wrap;
+ }
+}
+
+.search-products {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+ font-family: 'Oswald', sans-serif;
+ img {
+ width: 16px;
+ height: 16px;
+ pointer-events: none;
+ filter: invert(100%);
+ }
+}
+
+.catalog-pagination {
+ padding: 10px 0;
+ margin: 0;
+ display: flex;
+ width: 100%;
+ font-family: 'Oswald', sans-serif;
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ z-index: 11;
+ justify-content: center;
+ background-color: #fff;
+ gap: 4px;
+ li {
+ display: block;
+ a {
+ user-select: none;
+ transition: 150ms;
+ padding: 5px 15px;
+ display: block;
+ cursor: pointer;
+ &:hover {
+ background-color: grey;
+ color: #fff;
+ }
+ }
+ }
+
+ .pagination-active {
+ a {
+ background: #000;
+ color: #fff;
+ &:hover {
+ background-color: #000;
+ cursor: auto;
+ }
+ }
+ }
+
+ .disabled {
+ a {
+ background-color: rgba(128, 128, 128, 0.418);
+ &:hover {
+ cursor: auto;
+ color: black;
+ }
+ }
+ }
+}
diff --git a/src/pages/Catalog/CatalogPage.tsx b/src/pages/Catalog/CatalogPage.tsx
new file mode 100644
index 0000000..3ae0ea1
--- /dev/null
+++ b/src/pages/Catalog/CatalogPage.tsx
@@ -0,0 +1,364 @@
+import { getCategories, getProductsByCategory } from '@src/services/ProductsService/ProductsService';
+import CatalogProductCard from '@src/components/CatalogProductCard/CatalogProductCard';
+import React, { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
+import { ProductCatalog, ProductFormattedData } from '@src/interfaces/Product';
+import CategoryCard from '@src/components/CategoryCard/CategoryCard';
+import './CatalogPage.scss';
+import PriceRangeSlider from '@src/components/PriceRange/PriceRange';
+import formattedCategoryList from '@src/utilities/formattedCategoryList';
+import SortingSelect from '@src/components/SortingSelect/SortingSelect';
+import BrandFilter from '@src/components/BrandFilter/BrandFilter';
+import { Link, useNavigate, useParams } from 'react-router-dom';
+import Breadcrumb from '@src/components/Breadcrumb/Breadcrumb';
+import sortingOptions from '@src/utilities/sortingOptions';
+import searchIcon from '@assets/search.svg';
+import removeItemCart from '@src/utilities/removeItemCart';
+import addItemCart from '@src/utilities/addItemCart';
+import getFormattedCart from '@src/utilities/getFormattedCart';
+import ReactPaginate from 'react-paginate';
+import { ClipLoader } from 'react-spinners';
+import returnCartPrice from '@src/utilities/returnCartPrice';
+
+export default function Catalog({
+ setTotalSumInCart,
+}: {
+ setTotalSumInCart: Dispatch>;
+}): JSX.Element {
+ const navigate = useNavigate();
+
+ const { categoryslug, subcategoryslug, subcategoryslug2 } = useParams<{
+ categoryslug: string;
+ subcategoryslug: string;
+ subcategoryslug2: string;
+ }>();
+
+ const minPrice = 0;
+ const maxPrice = 5000;
+
+ const [priceRange, setPriceRange] = useState([minPrice, maxPrice]);
+ const [search, setSearch] = useState('');
+ const [products, setProducts] = useState([]);
+ const [amountOfProducts, setAmountOfProducts] = useState(0);
+ const [categories, setCategories] = useState([]);
+ const [sort, setSort] = useState('name.en asc');
+ const [brand, setBrand] = useState('');
+ const [savedBrands, setSavedBrands] = useState([]);
+ const [currentCategory, setCurrentCategory] = useState<{ name: string; key?: string }[]>([]);
+ const [breadcrumb, setBreadcrumb] = useState<{ name: string; slug: string }[]>([]);
+
+ const productsPerPage = 6;
+ const [currentOffset, setCurrentOffset] = useState(0);
+
+ const [gettingNewProducts, setGettingNewProducts] = useState(false);
+
+ const [cartList, setCartList] = useState<{ id: string; productId: string }[]>([]);
+
+ const [displayCategories, setDisplayCategories] = useState(false);
+
+ const handleAddToCart = async (productSku: string): Promise => {
+ const result = await addItemCart(productSku);
+ if (result) {
+ setCartList(result);
+ }
+
+ const cartPrice = await returnCartPrice();
+ if (cartPrice !== false) {
+ setTotalSumInCart(cartPrice);
+ }
+ return Promise.resolve();
+ };
+
+ const handleRemoveFromCart = async (productSku: string): Promise => {
+ const result = await removeItemCart(productSku);
+ if (result) {
+ setCartList(result);
+ }
+
+ const cartPrice = await returnCartPrice();
+ if (cartPrice !== false) {
+ setTotalSumInCart(cartPrice);
+ }
+ return Promise.resolve();
+ };
+
+ const clearBrand = useCallback(() => {
+ setBrand('');
+ }, []);
+
+ const getNewProducts = useCallback(() => {
+ setGettingNewProducts(true);
+ let formatPriceRange;
+ if (priceRange[0] === 0) {
+ formatPriceRange = `variants.price.centAmount:range (0 to ${priceRange[1]}00)`;
+ } else {
+ formatPriceRange = `variants.price.centAmount:range (${priceRange[0]}00 to ${priceRange[1]}00)`;
+ }
+
+ if (currentCategory.length > 0) {
+ getProductsByCategory(
+ `categories.id: subtree("${currentCategory[currentCategory.length - 1].key}")&filter=${formatPriceRange}`,
+ sort,
+ search,
+ brand,
+ productsPerPage,
+ currentOffset,
+ ).then((data) => {
+ setProducts(data.results);
+ setAmountOfProducts(data.total);
+ setGettingNewProducts(false);
+ });
+ } else {
+ getProductsByCategory(`${formatPriceRange}`, sort, search, brand, productsPerPage, currentOffset).then((data) => {
+ setProducts(data.results);
+ setAmountOfProducts(data.total);
+ setGettingNewProducts(false);
+ });
+ }
+ }, [priceRange, sort, brand, currentCategory, search, currentOffset]);
+
+ const handlePageChange = (selectedPage: { selected: number }): void => {
+ setCurrentOffset(selectedPage.selected * productsPerPage);
+ };
+
+ const handleSortingChange = (newOption: string): void => {
+ setSort(newOption);
+ setCurrentOffset(0);
+ };
+
+ const handlePriceChange = (newRange: number[]): void => {
+ setPriceRange(newRange);
+ setCurrentOffset(0);
+ };
+
+ const handleBrandChange = (newBrand: string): void => {
+ setBrand(newBrand);
+ setCurrentOffset(0);
+ };
+
+ useEffect(() => {
+ const fetchCategory = async (
+ name: string,
+ ): Promise<{
+ name: string;
+ slug: string;
+ }> => {
+ const data = await getCategories(`slug(en = "${name}")`);
+ return {
+ name: data.results[0].name.en,
+ slug: data.results[0].slug.en,
+ };
+ };
+
+ async function fetchCategoriesInOrder(
+ currentCategories: {
+ name: string;
+ key?: string | undefined;
+ }[],
+ ): Promise<
+ {
+ name: string;
+ slug: string;
+ }[]
+ > {
+ const breadcrumbArray = [];
+
+ const fetchPromises = currentCategories.map((category) => fetchCategory(category.name));
+
+ const results = await Promise.all(fetchPromises);
+
+ breadcrumbArray.push(...results);
+
+ return breadcrumbArray;
+ }
+
+ if (currentCategory.length > 0) {
+ fetchCategoriesInOrder(currentCategory).then((array) => {
+ setBreadcrumb(array);
+ });
+ }
+ }, [currentCategory]);
+
+ useEffect(() => {
+ formattedCategoryList().then((data) => {
+ setCategories(data.mainCategories);
+ });
+ }, []);
+
+ useEffect(() => {
+ async function fetchData(): Promise {
+ const cart = await getFormattedCart();
+ if (cart) {
+ setCartList(cart);
+ }
+ }
+ fetchData();
+ }, []);
+
+ useEffect(() => {
+ getNewProducts();
+ }, [sort, priceRange, getNewProducts]);
+
+ useEffect(() => {
+ setGettingNewProducts(true);
+ if (categories.length > 1 && categoryslug) {
+ const mainCategory = categories.filter((category) => category.slug === categoryslug);
+ if (mainCategory.length > 0) {
+ if (subcategoryslug) {
+ const subcategory = mainCategory[0].ancestors.filter((category) => category.slug === subcategoryslug);
+ if (subcategory.length === 0) {
+ navigate('/NotFound');
+ }
+ }
+ } else {
+ navigate('/NotFound');
+ }
+ }
+
+ if (subcategoryslug && categoryslug) {
+ getCategories(`slug(en = "${subcategoryslug}")`).then((data) => {
+ setCurrentCategory([{ name: categoryslug }, { name: subcategoryslug, key: data.results[0].id }]);
+ getProductsByCategory(`categories.id: subtree("${data.results[0].id}")`, sort, undefined, undefined)
+ .then((result) => {
+ setSavedBrands(result.results);
+ })
+ .then(() => {
+ getProductsByCategory(
+ `categories.id: subtree("${data.results[0].id}")`,
+ sort,
+ undefined,
+ undefined,
+ productsPerPage,
+ ).then((result) => {
+ setProducts(result.results);
+ setAmountOfProducts(data.total);
+ setGettingNewProducts(false);
+ });
+ });
+ });
+ } else if (categoryslug) {
+ getCategories(`slug(en = "${categoryslug}")`).then((data) => {
+ setCurrentCategory([{ name: categoryslug, key: data.results[0].id }]);
+ getProductsByCategory(`categories.id: subtree("${data.results[0].id}")`, sort, undefined, undefined)
+ .then((result) => {
+ setSavedBrands(result.results);
+ })
+ .then(() => {
+ getProductsByCategory(
+ `categories.id: subtree("${data.results[0].id}")`,
+ sort,
+ undefined,
+ undefined,
+ productsPerPage,
+ ).then((result) => {
+ setProducts(result.results);
+ setAmountOfProducts(data.total);
+ setGettingNewProducts(false);
+ });
+ });
+ });
+ } else {
+ setCurrentCategory([]);
+ getProductsByCategory(`variants.prices:exists`, sort, undefined, undefined)
+ .then((data) => {
+ setSavedBrands(data.results);
+ })
+ .then(() => {
+ getProductsByCategory(`variants.prices:exists`, sort, undefined, undefined, productsPerPage).then((data) => {
+ setProducts(data.results);
+ setAmountOfProducts(data.total);
+ setGettingNewProducts(false);
+ });
+ });
+ }
+ }, [categories, sort, categoryslug, subcategoryslug, subcategoryslug2, navigate]);
+
+ return (
+
+
+
+
{
+ setDisplayCategories(!displayCategories);
+ }}
+ >
+ Categories
+
+
+ {categories.map((category) => (
+
+ ))}
+
+
+
+
+ /
+ {
+ setBreadcrumb([]);
+ }}
+ >
+ Catalog
+
+
+
+
+
+
+
+
+
+
{
+ const { value } = event.target;
+ setSearch(value);
+ }}
+ />
+
+
+
+
+
+
+
+
+ {gettingNewProducts ? (
+
+
+
+ ) : (
+
+ {products.map((product) => (
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/pages/Home/Home.scss b/src/pages/Home/Home.scss
new file mode 100644
index 0000000..6c6e513
--- /dev/null
+++ b/src/pages/Home/Home.scss
@@ -0,0 +1,9 @@
+.promo-codes {
+ text-align: center;
+
+ .promo-codes_list {
+ display: flex;
+ justify-content: center;
+ text-align: left;
+ }
+}
diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx
new file mode 100644
index 0000000..9127ec9
--- /dev/null
+++ b/src/pages/Home/Home.tsx
@@ -0,0 +1,49 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { DiscountCode } from '@src/interfaces/Discount';
+import { getDiscountCodes } from '@src/services/DiscountService/DiscountService';
+import './Home.scss';
+
+export default function Home(): JSX.Element {
+ const [discountCodes, setdiscountCodes] = useState();
+
+ useEffect(() => {
+ async function fetchData(): Promise {
+ const dataDiscountCodes = await getDiscountCodes();
+ if (dataDiscountCodes) {
+ setdiscountCodes(dataDiscountCodes);
+ }
+ }
+ fetchData();
+ }, []);
+
+ return (
+
+
Home
+
+
PROMO CODES
+
+
+ {discountCodes?.map((discountCode: DiscountCode) => (
+
+ {discountCode.code} - {discountCode.description.en}
+
+ ))}
+
+
+
+
+
+
+ Login
+
+
+
+
+ Register
+
+
+
+
+ );
+}
diff --git a/src/pages/Home/__tests__/Home.test.tsx b/src/pages/Home/__tests__/Home.test.tsx
new file mode 100644
index 0000000..b371f84
--- /dev/null
+++ b/src/pages/Home/__tests__/Home.test.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import { BrowserRouter } from 'react-router-dom';
+import Home from '../Home';
+import '@testing-library/jest-dom';
+
+describe('Home Component', () => {
+ it('renders properly', () => {
+ const { getByText, container } = render(
+
+
+ ,
+ );
+ expect(getByText('Home')).toBeInTheDocument();
+ expect(container.querySelector('.main-heading')).toBeInTheDocument();
+ expect(container.querySelector('.wrapper-btn')).toBeInTheDocument();
+ });
+
+ it('navigates to /login and /register on button clicks', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ const loginButton = getByText('Login');
+ expect(loginButton).toBeInTheDocument();
+ fireEvent.click(loginButton);
+ expect(document.location.href).toContain('/login');
+
+ const registerButton = getByText('Register');
+ expect(registerButton).toBeInTheDocument();
+ fireEvent.click(registerButton);
+ expect(document.location.href).toContain('/register');
+ });
+});
diff --git a/src/pages/Login/LoginPage.tsx b/src/pages/Login/LoginPage.tsx
new file mode 100644
index 0000000..5303d2b
--- /dev/null
+++ b/src/pages/Login/LoginPage.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import LoginForm from '@components/LoginForm/LoginForm';
+
+function LoginPage({ checkLogIn }: { checkLogIn: () => void }): JSX.Element {
+ return ;
+}
+
+export default LoginPage;
diff --git a/src/pages/Login/__tests__/LoginPage.test.tsx b/src/pages/Login/__tests__/LoginPage.test.tsx
new file mode 100644
index 0000000..54d7404
--- /dev/null
+++ b/src/pages/Login/__tests__/LoginPage.test.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import LoginPage from '../LoginPage';
+import '@testing-library/jest-dom';
+
+describe('LoginPage', () => {
+ test('renders LoginForm component', () => {
+ const mockCheckLogIn = jest.fn();
+
+ render(
+
+
+ ,
+ );
+
+ const loginForm = screen.getByTestId('login-form');
+ expect(loginForm).toBeInTheDocument();
+ });
+});
diff --git a/src/pages/NotFound/NotFound.scss b/src/pages/NotFound/NotFound.scss
new file mode 100644
index 0000000..c93f4b1
--- /dev/null
+++ b/src/pages/NotFound/NotFound.scss
@@ -0,0 +1,7 @@
+.not-found {
+ text-align: center;
+
+ button {
+ margin: 0 auto;
+ }
+}
diff --git a/src/pages/NotFound/NotFound.tsx b/src/pages/NotFound/NotFound.tsx
new file mode 100644
index 0000000..db66109
--- /dev/null
+++ b/src/pages/NotFound/NotFound.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import './NotFound.scss';
+import '@components/Button/Button.scss';
+import '@components/Heading/Heading.scss';
+
+export default function NotFound(): JSX.Element {
+ return (
+
+
Page not found
+ 404
+
+
+ Home
+
+
+
+ );
+}
diff --git a/src/pages/NotFound/__tests__/NotFound.test.tsx b/src/pages/NotFound/__tests__/NotFound.test.tsx
new file mode 100644
index 0000000..3f2dcf1
--- /dev/null
+++ b/src/pages/NotFound/__tests__/NotFound.test.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { render, screen } from '@testing-library/react';
+import NotFound from '../NotFound';
+import '@testing-library/jest-dom';
+
+describe('NotFound', () => {
+ test('renders NotFound component without crashing', () => {
+ render(
+
+
+ ,
+ );
+ });
+ test('displays "404" text', () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText('404')).toBeInTheDocument();
+ });
+ test('displays "Page not found" text', () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText('Page not found')).toBeInTheDocument();
+ });
+ test('displays "Home" button', () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByRole('button', { name: 'Home' })).toBeInTheDocument();
+ });
+});
diff --git a/src/pages/Product/ProductPage.scss b/src/pages/Product/ProductPage.scss
new file mode 100644
index 0000000..efeceb8
--- /dev/null
+++ b/src/pages/Product/ProductPage.scss
@@ -0,0 +1,335 @@
+.product {
+ font-family: 'Oswald', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+
+ margin: auto;
+
+ .product__line {
+ max-width: 1200px;
+ margin-top: 30px;
+ }
+
+ .product__details {
+ margin-bottom: 30px;
+ padding: 20px;
+ background-color: #f8f9fb;
+
+ &-header {
+ display: flex;
+ justify-content: space-between;
+ font-size: 24px;
+ cursor: pointer;
+ }
+
+ &-title {
+ border-top: 1px solid #c4c4c4;
+ padding-top: 20px;
+ margin-top: 20px;
+
+ text-transform: uppercase;
+ margin-bottom: 15px;
+ }
+
+ &-descriptions {
+ font-family: 'Roboto';
+ }
+ }
+
+ .path__category {
+ color: #828282;
+ text-decoration: none;
+ }
+
+ .first-line {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ .product__img-list {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ margin-right: 5px;
+
+ .product__img-item {
+ width: 76px;
+ height: 96px;
+ cursor: pointer;
+
+ img {
+ max-width: 100%;
+ max-height: 100%;
+ }
+ }
+ }
+
+ .product__brand {
+ background-color: #f0f2f2;
+ width: fit-content;
+ padding: 5px 8px;
+ }
+
+ .product__name:first-letter {
+ text-transform: uppercase;
+ }
+
+ .product__attr-title {
+ margin-top: 15px;
+ margin-bottom: 5px;
+ font-size: 14px;
+ text-transform: uppercase;
+ }
+
+ .product__attributes {
+ font-size: 14px;
+ max-width: 300px;
+ }
+
+ .product__color-items {
+ display: flex;
+ gap: 10px;
+
+ &-item {
+ width: 20px;
+ height: 20px;
+ }
+ }
+
+ .product__select-size_items {
+ display: flex;
+ }
+
+ .product__select-size-items_item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 70px;
+ height: 40px;
+ border: 1px solid #c4c4c4;
+ margin-right: 3px;
+ cursor: pointer;
+ }
+
+ .color-black {
+ background-color: #000;
+ }
+
+ .color-yellow {
+ background-color: #e0bb52;
+ }
+
+ .color-green {
+ background-color: #03aa19;
+ }
+
+ .active-color {
+ border: 2px solid #000;
+ }
+
+ .active-size {
+ border: 1px solid #000;
+ }
+
+ .product__quantity_price {
+ display: flex;
+ gap: 20px;
+ }
+
+ button[disabled] + .product__quantity-display {
+ color: #c4c4c4;
+ }
+
+ .product__quantity-input {
+ border: 1px solid #c4c4c4;
+ width: fit-content;
+
+ .product__quantity-btn {
+ font-size: 16px;
+ color: #c4c4c4;
+ width: 40px;
+ height: 30px;
+ &:disabled {
+ cursor: auto;
+ }
+ }
+ }
+
+ @media (max-width: 700px) {
+ .buttons-container {
+ flex-direction: row;
+ }
+ }
+
+ .buttons-container {
+ width: 285px;
+ margin-bottom: 0px;
+ margin-top: 15px;
+
+ gap: 10px;
+ .add-to-cart,
+ .remove-from-cart {
+ margin: 0;
+ width: 100%;
+ font-size: 12px;
+ border-radius: 8px;
+ &:hover {
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.485);
+ border-radius: 16px;
+ }
+
+ &:disabled {
+ background-color: rgba(128, 128, 128, 0.124);
+ cursor: auto;
+ border-radius: 8px;
+ box-shadow: none;
+
+ .cart-add-img {
+ filter: invert(6%) sepia(4%) saturate(1067%) hue-rotate(314deg) brightness(93%) contrast(83%);
+ }
+
+ .cart-remove-img {
+ filter: invert(6%) sepia(4%) saturate(1067%) hue-rotate(314deg) brightness(93%) contrast(83%);
+ }
+ }
+
+ .cart-remove-img,
+ .cart-add-img {
+ height: 50%;
+ }
+
+ .cart-add-img {
+ filter: invert(23%) sepia(99%) saturate(2030%) hue-rotate(96deg) brightness(97%) contrast(106%);
+ }
+
+ .cart-remove-img {
+ filter: invert(10%) sepia(57%) saturate(7500%) hue-rotate(357deg) brightness(104%) contrast(96%);
+ }
+ }
+ }
+
+ .product__price {
+ font-size: 26px;
+
+ .product__price-discounted {
+ text-decoration: line-through;
+ font-size: 16px;
+ }
+
+ .discounted {
+ color: red;
+ }
+ }
+
+ .product__controls {
+ margin-top: 15px;
+
+ .product__btn {
+ border: none;
+ text-align: center;
+ font-family: 'Oswald', sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ width: 140px;
+ height: 50px;
+ transition: all 0.3s;
+
+ &:hover {
+ transition: all 0.3s;
+ border-radius: 10px;
+ }
+ }
+
+ .btn-bag {
+ margin-right: 5px;
+ }
+
+ .btn-black {
+ background: #000;
+ color: #fff;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.291);
+ }
+ }
+
+ .btn-white {
+ border: 1px solid #c4c4c4;
+ }
+ }
+ }
+}
+
+.swiper {
+ width: 100%;
+ height: 300px;
+ max-width: 500px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.swiper-slide {
+ background-size: cover;
+ background-position: center;
+}
+
+.swiper {
+ width: 100%;
+ height: 100%;
+}
+
+.swiper-slide {
+ text-align: center;
+ font-size: 18px;
+ background: #fff;
+}
+
+.swiper-slide img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.modal-view {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ background-color: #000000bd;
+ z-index: 2;
+}
+
+body.modal-open {
+ overflow: hidden;
+}
+
+.swiper-modal {
+ top: 10%;
+ left: 10%;
+ width: 60%;
+ height: 80%;
+ max-width: 800px;
+}
+
+@media screen and (max-width: 510px) {
+ .swiper {
+ max-width: 300px;
+ }
+ .swiper-modal {
+ top: 1%;
+ left: 1%;
+ width: 80%;
+ height: 40%;
+ max-width: 300px;
+ }
+}
diff --git a/src/pages/Product/ProductPage.tsx b/src/pages/Product/ProductPage.tsx
new file mode 100644
index 0000000..1da9112
--- /dev/null
+++ b/src/pages/Product/ProductPage.tsx
@@ -0,0 +1,351 @@
+import React, { Dispatch, SetStateAction, useEffect, useLayoutEffect, useRef, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import './ProductPage.scss';
+
+import { ProductDetailedPage } from '@src/interfaces/Product';
+import { getProductByKey } from '@src/services/ProductsService/ProductsService';
+
+// Import Swiper and styles
+import { Swiper, SwiperSlide, SwiperClass } from 'swiper/react';
+import 'swiper/css';
+import 'swiper/css/free-mode';
+import 'swiper/css/navigation';
+import 'swiper/css/thumbs';
+import { Navigation, Controller } from 'swiper/modules';
+import ClipLoader from 'react-spinners/ClipLoader';
+
+import cartAdd from '@assets/cart-plus-solid.svg';
+import cartRemove from '@assets/cart-shopping-solid.svg';
+import addItemCart from '@src/utilities/addItemCart';
+import removeItemCart from '@src/utilities/removeItemCart';
+import getFormattedCart from '@src/utilities/getFormattedCart';
+import returnCartPrice from '@src/utilities/returnCartPrice';
+
+function ProductPage({ setTotalSumInCart }: { setTotalSumInCart: Dispatch> }): JSX.Element {
+ const { key = '' } = useParams<{
+ key: string;
+ }>();
+
+ const [formData, setFormData] = useState({ key, count: 1, inBag: false, inFavorites: false });
+ const [product, setProducts] = useState();
+ const [cartList, setCartList] = useState<{ id: string; productId: string }[]>([]);
+
+ let CartProduct: {
+ productId: string;
+ id: string;
+ } = {
+ productId: '0',
+ id: '0',
+ };
+ if (cartList.length > 0) {
+ const foundProduct = cartList.find((item) => item.productId === product?.id);
+ if (foundProduct) {
+ CartProduct = foundProduct;
+ }
+ }
+
+ const [addItemLoading, setAddItemLoading] = useState(false);
+ const [removeItemLoading, setRemoveItemLoading] = useState(false);
+
+ useEffect(() => {
+ getProductByKey(formData.key).then((data) => {
+ setProducts(data);
+ });
+ }, [formData.key]);
+
+ const handleAddToCart = async (productSku: string): Promise => {
+ const result = await addItemCart(productSku, formData.count);
+ if (result) {
+ setCartList(result);
+
+ const cartPrice = await returnCartPrice();
+ if (cartPrice !== false) {
+ setTotalSumInCart(cartPrice);
+ }
+ }
+ return Promise.resolve();
+ };
+
+ const handleRemoveFromCart = async (productSku: string): Promise => {
+ const result = await removeItemCart(productSku);
+ if (result) {
+ setCartList(result);
+
+ const cartPrice = await returnCartPrice();
+ if (cartPrice !== false) {
+ setTotalSumInCart(cartPrice);
+ }
+ }
+ return Promise.resolve();
+ };
+
+ useEffect(() => {
+ async function fetchData(): Promise {
+ const cart = await getFormattedCart();
+ if (cart) {
+ setCartList(cart);
+ }
+ }
+ fetchData();
+ }, []);
+
+ const current = product?.masterData.current;
+
+ const currency = current?.masterVariant.prices[0].value.currencyCode || '';
+ const totalPrice = ((current?.masterVariant.prices[0].value.centAmount || 0) * formData.count) / 100;
+ const totalPriceFormated = new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'EUR' }).format(totalPrice);
+ const discountedPrice = ((current?.masterVariant.prices[0].discounted?.value.centAmount || 0) * formData.count) / 100;
+ const discountedPriceFormated = new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'EUR' }).format(
+ discountedPrice,
+ );
+
+ const description = current?.description.en;
+
+ let brand = 'brand not found';
+ const brandModel = current?.name.en;
+ current?.masterVariant.attributes.forEach((attribute) => {
+ if (attribute.name === 'brand') brand = attribute.value;
+ });
+
+ const clickUpCount = (): void => {
+ let count = formData.count + 1;
+ if (count > 100) count = 100;
+ setFormData({ ...formData, count });
+ };
+
+ const clickDownCount = (): void => {
+ let count = formData.count - 1;
+ if (count <= 0) count = 1;
+ setFormData({ ...formData, count });
+ };
+
+ const clickShowHideDetails = (event: React.MouseEvent): void => {
+ const node = event.currentTarget as HTMLElement;
+ const plusMinus = node?.children[1] as HTMLImageElement;
+ const show = plusMinus.innerText === '+';
+ plusMinus.innerHTML = show ? '-' : '+';
+
+ const detailsNode = event.currentTarget.nextElementSibling as HTMLElement;
+ detailsNode.style.display = show ? 'block' : 'none';
+ };
+
+ const showModalImg = (event: React.MouseEvent): void => {
+ if (event.currentTarget !== event.target) return;
+ const node = event.currentTarget as HTMLElement;
+ node.style.display = 'none';
+ document.body.classList.remove('modal-open');
+ };
+
+ const showModal = (): void => {
+ const node = document.querySelector('.modal-view') as HTMLElement;
+ node.style.display = 'block';
+ document.body.classList.add('modal-open');
+ };
+
+ const swiper1Ref = useRef();
+ const swiper2Ref = useRef();
+
+ useLayoutEffect(() => {
+ if (swiper1Ref.current && swiper2Ref.current) {
+ swiper1Ref.current.controller.control = swiper2Ref.current;
+ swiper2Ref.current.controller.control = swiper1Ref.current;
+ }
+ }, []);
+
+ return (
+ <>
+
+
+
+
{
+ swiper1Ref.current = swiper;
+ }}
+ onClick={(): void => showModal()}
+ >
+ {current?.masterVariant.images.map((img: { url: string }, index: number) => (
+
+
+
+ ))}
+
+
+
+ {/*
*/}
+
+
{brand}
+
+
{brandModel}
+
+ {/*
+
+
+
select size(inches)
+
+
*/}
+
+
+
+
quantity
+
+ clickDownCount()}
+ disabled={CartProduct.id !== '0'}
+ >
+ -
+
+ {formData.count}
+ clickUpCount()}
+ disabled={CartProduct.id !== '0'}
+ >
+ +
+
+
+
+
+
+
price total
+ {discountedPrice > 0 ? (
+ <>
+
{discountedPriceFormated}
+
{currency}
+
+
{totalPriceFormated}
+
{currency}
+ >
+ ) : (
+ <>
+
{totalPriceFormated}
+
{currency}
+ >
+ )}
+
+
+
+
+
{
+ setAddItemLoading(true);
+ handleAddToCart(formData.key).then(() => {
+ setAddItemLoading(false);
+ });
+ }}
+ disabled={CartProduct.id !== '0'}
+ draggable="false"
+ >
+ {addItemLoading ? (
+
+ ) : (
+
+ )}
+
+
{
+ setRemoveItemLoading(true);
+ handleRemoveFromCart(CartProduct.id).then(() => {
+ setRemoveItemLoading(false);
+ });
+ }}
+ disabled={CartProduct.id === '0'}
+ draggable="false"
+ >
+ {removeItemLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/*
+ setFormData({ ...formData, inBag: !formData.inBag })}
+ >
+ {formData.inBag ? 'delete from bag' : 'add to bag'}
+
+ setFormData({ ...formData, inFavorites: !formData.inFavorites })}
+ >
+ {formData.inFavorites ? 'delete from save' : 'save'}
+
+
*/}
+
+
+
+
+
+
+
+
about product
+
{description}
+
+
+
+
+
+
+
+
{
+ swiper2Ref.current = swiper;
+ }}
+ >
+ {current?.masterVariant.images.map((img: { url: string }, index: number) => (
+
+
+
+ ))}
+
+
+ >
+ );
+}
+
+export default ProductPage;
diff --git a/src/pages/Profile/Profile.module.scss b/src/pages/Profile/Profile.module.scss
new file mode 100644
index 0000000..db3306a
--- /dev/null
+++ b/src/pages/Profile/Profile.module.scss
@@ -0,0 +1,59 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Oswald&display=swap');
+
+:root {
+ --gray-1: #828282;
+ --divider: #c4c4c4;
+ --light-blue-hover: #f0f2f2;
+ --black-2: #3f3f3f;
+ --light-blue-active: #c2c2c2;
+}
+
+.link {
+ text-decoration: none;
+}
+
+.dashboard__description {
+ display: block;
+ max-width: 400px;
+ flex: 1;
+}
+
+.account__information_title {
+ color: #000;
+ font-family: 'Oswald', sans-serif;
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 400;
+ margin: 0;
+}
+
+.account__information_block {
+ margin-bottom: 30px;
+}
+
+.account__information_blockTitle,
+.account__information_billing {
+ color: var(--black-2);
+ font-family: 'Oswald', sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ text-transform: uppercase;
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+
+.account__information_name,
+.account__information_email,
+.account__information_adress {
+ color: var(--gray-1);
+ font-family: 'Roboto', sans-serif;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+}
+
+.account__information_blockAdress {
+}
diff --git a/src/pages/Profile/Profile.tsx b/src/pages/Profile/Profile.tsx
new file mode 100644
index 0000000..129e811
--- /dev/null
+++ b/src/pages/Profile/Profile.tsx
@@ -0,0 +1,84 @@
+import React, { useEffect, useState } from 'react';
+import { CustomersId } from '@src/interfaces/Customer';
+import BillingAddress from '@src/components/BillingAddress/BillingAddress';
+import ShippingAddress from '@src/components/ShippingAddress/ShippingAddress';
+import { getCustomerId } from '@src/services/AuthService/AuthService';
+import styles from './Profile.module.scss';
+
+function Profile(): JSX.Element {
+ // { user }: { user: CustomersId }
+ const [user, setUser] = useState({
+ email: '',
+ firstName: '',
+ lastName: '',
+ billingAddressIds: [],
+ shippingAddressIds: [],
+ dateOfBirth: '',
+ id: '',
+ defaultShippingAddressId: '',
+ defaultBillingAddressId: '',
+ addresses: [
+ {
+ city: '',
+ country: '',
+ id: '',
+ postalCode: '',
+ streetName: '',
+ },
+ ],
+ });
+ useEffect(() => {
+ getCustomerId().then((item) => setUser(item));
+ }, []);
+
+ const defaultBillingAddress = user.addresses.find((item) => item.id === user.defaultBillingAddressId);
+ const defaultShippingAddress = user.addresses.find((item) => item.id === user.defaultShippingAddressId);
+ const dateBirth = user.dateOfBirth.split('-');
+ const resultDateBirth = `${dateBirth[2]}.${dateBirth[1]}.${dateBirth[0]}`;
+
+ return (
+
+
Account Information
+
+
Contact Information
+
{`${user.firstName} ${user.lastName}`}
+
{resultDateBirth}
+
{user.email}
+
+
Address Book
+
+
Default Billing Address
+
+ {defaultBillingAddress ? (
+
+ ) : (
+ You have not set a default billing address.
+ )}
+
+
+
+
Default Shipping Address
+
+ {defaultShippingAddress ? (
+
+ ) : (
+ You have not set a default shipping address.
+ )}
+
+
+
+ );
+}
+export default Profile;
diff --git a/src/pages/Register/RegistrationPage.scss b/src/pages/Register/RegistrationPage.scss
new file mode 100644
index 0000000..f322a8b
--- /dev/null
+++ b/src/pages/Register/RegistrationPage.scss
@@ -0,0 +1,54 @@
+.form-address {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 20px;
+ gap: 15px;
+
+ label {
+ display: flex;
+ gap: 5px;
+ }
+
+ .form-billing-address,
+ .form-shipping-address {
+ padding: 10px;
+ border: 1px solid #7a7a7a;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: column;
+ }
+}
+
+@media (max-width: 900px) {
+ .form-address {
+ .form-input label {
+ width: clamp(200px, 45vw, 400px);
+ }
+ }
+}
+
+@media (max-width: 700px) {
+ .buttons-container {
+ flex-direction: column;
+ margin-top: 20px;
+ .btn {
+ margin: 0;
+ }
+ }
+}
+
+@media (max-width: 600px) {
+ .form-address {
+ flex-direction: column;
+ margin: 0 2rem;
+ .form-input label {
+ width: 25rem;
+ }
+ }
+}
+
+@media (max-width: 410px) {
+ .form-input label {
+ flex-direction: column;
+ }
+}
diff --git a/src/pages/Register/RegistrationPage.tsx b/src/pages/Register/RegistrationPage.tsx
new file mode 100644
index 0000000..7891301
--- /dev/null
+++ b/src/pages/Register/RegistrationPage.tsx
@@ -0,0 +1,379 @@
+import React, { useEffect, useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import Cookies from 'js-cookie';
+import FormInput from '@components/FormInput/FormInput';
+import { RegistrationFormData } from '@interfaces/Register';
+import { getNewToken, logInUser, registerUser } from '@services/AuthService/AuthService';
+import FormAddress from '@components/FormAddress/FormAddress';
+import './RegistrationPage.scss';
+import '@components/Heading/Heading.scss';
+import '@components/Button/Button.scss';
+import { BaseAddress, CustomerDraft } from '@interfaces/Customer';
+import Toastify from 'toastify-js';
+
+function RegistrationPage({ checkLogIn }: { checkLogIn: () => void }): JSX.Element {
+ function getFormattedDate(): string {
+ const currentDate = new Date(Date.now());
+ const currentYear = currentDate.getFullYear();
+ currentDate.setFullYear(currentYear - 13);
+
+ const year = currentDate.getFullYear();
+ const month = String(currentDate.getMonth() + 1).padStart(2, '0');
+ const day = String(currentDate.getDate()).padStart(2, '0');
+
+ const formattedDate = `${year}-${month}-${day}`;
+ return formattedDate;
+ }
+
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ firstName: '',
+ lastName: '',
+ dateOfBirth: '',
+ defaultShippingAddress: false,
+ defaultBillingAddress: false,
+ sameBillingShipping: false,
+ billingAddress: {
+ city: '',
+ postalCode: '',
+ streetName: '',
+ country: '',
+ },
+ shippingAddress: { city: '', postalCode: '', streetName: '', country: '' },
+ });
+
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const authType = Cookies.get('auth-type');
+ if (authType === 'password') {
+ navigate('/');
+ Toastify({
+ text: 'Log out to access this page',
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ }
+ }, [navigate]);
+
+ const [isFormComplete, setIsFormComplete] = useState(false);
+
+ const handleInputChange = (event: React.ChangeEvent): void => {
+ const { id, value } = event.target;
+ setFormData({ ...formData, [id]: value });
+ };
+
+ const handleCheckboxChange = (event: React.ChangeEvent): void => {
+ const { id, checked } = event.target;
+ if ((id !== 'sameBillingShipping' && formData.sameBillingShipping) || (id === 'sameBillingShipping' && checked)) {
+ setFormData({ ...formData, [id]: checked, billingAddress: formData.shippingAddress });
+
+ return;
+ }
+ setFormData({ ...formData, [id]: checked });
+ };
+
+ const handleBillingAddressChange = (address: Partial): void => {
+ const newAddress = { ...formData.billingAddress, ...address };
+ setFormData({ ...formData, billingAddress: newAddress });
+ };
+
+ const handleShippingAddressChange = (address: Partial): void => {
+ if (formData.sameBillingShipping) {
+ const newAddress = { ...formData.shippingAddress, ...address };
+ setFormData({ ...formData, shippingAddress: newAddress, billingAddress: newAddress });
+ } else {
+ const newAddress = { ...formData.shippingAddress, ...address };
+ setFormData({ ...formData, shippingAddress: newAddress });
+ }
+ };
+
+ useEffect(() => {
+ if (
+ Object.values(formData).every((value) => {
+ if (typeof value !== 'object') {
+ return value !== '';
+ }
+ return Object.values(value).every((objValue) => objValue !== '');
+ })
+ ) {
+ setIsFormComplete(true);
+ } else {
+ setIsFormComplete(false);
+ }
+ }, [formData]);
+
+ const handleSubmit = async (e: React.FormEvent): Promise => {
+ e.preventDefault();
+
+ const {
+ email,
+ password,
+ firstName,
+ lastName,
+ dateOfBirth,
+ shippingAddress,
+ billingAddress,
+ defaultShippingAddress,
+ defaultBillingAddress,
+ } = formData;
+
+ const registerData: CustomerDraft = {
+ email,
+ password,
+ firstName,
+ lastName,
+ dateOfBirth,
+ addresses: [shippingAddress, billingAddress],
+ shippingAddresses: [0],
+ billingAddresses: [1],
+ };
+
+ if (defaultShippingAddress) {
+ registerData.defaultShippingAddress = 0;
+ }
+ if (defaultBillingAddress) {
+ registerData.defaultBillingAddress = 1;
+ }
+
+ const accessToken = Cookies.get('access-token');
+ let anonToken = Cookies.get('anon-token');
+ const anonRefreshToken = Cookies.get('anon-refresh-token');
+ const cartId = Cookies.get('cart-id');
+
+ const threeHours = 180 / (24 * 60);
+ const currentDate = new Date();
+ const currentPlusFiveMinutes = currentDate.getTime() + 250000;
+
+ const anonTokenExpires = Cookies.get('anon-token-expires');
+
+ if (anonTokenExpires) {
+ const anonExpiryDate = new Date(anonTokenExpires);
+
+ if (currentPlusFiveMinutes >= anonExpiryDate.getTime()) {
+ anonToken = '';
+ Cookies.remove('anon-token');
+ Cookies.remove('anon-token-expires');
+ }
+ }
+
+ if (accessToken) {
+ if (cartId) {
+ if (anonToken) {
+ registerUser(registerData, anonToken).then((result) => {
+ if (result !== false) {
+ logInUser(email, password).then((results) => {
+ if (results) {
+ Cookies.set('access-token', results.accessToken, { expires: 2 });
+ Cookies.set('refresh-token', results.refreshToken, { expires: 200 });
+ Cookies.set('auth-type', 'password', { expires: 2 });
+ checkLogIn();
+ navigate('/');
+ Toastify({
+ text: 'Account is successfully created!',
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(315deg, #7ee8fa 0%, #80ff72 74%)',
+ },
+ }).showToast();
+ }
+ });
+ }
+ });
+ } else if (anonRefreshToken) {
+ const token = await getNewToken(anonRefreshToken);
+ Cookies.set('anon-token', token.accessToken, { expires: threeHours });
+ currentDate.setHours(currentDate.getHours() + 3);
+ Cookies.set('anon-token-expires', currentDate.toISOString(), { expires: threeHours });
+ Cookies.set('anon-refresh-token', anonRefreshToken, { expires: 200 });
+
+ registerUser(registerData, token.accessToken).then((result) => {
+ if (result !== false) {
+ logInUser(email, password).then((results) => {
+ if (results) {
+ Cookies.set('access-token', results.accessToken, { expires: 2 });
+ Cookies.set('refresh-token', results.refreshToken, { expires: 200 });
+ Cookies.set('auth-type', 'password', { expires: 2 });
+ checkLogIn();
+ navigate('/');
+ Toastify({
+ text: 'Account is successfully created!',
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(315deg, #7ee8fa 0%, #80ff72 74%)',
+ },
+ }).showToast();
+ }
+ });
+ }
+ });
+ }
+ } else {
+ registerUser(registerData, accessToken).then((result) => {
+ if (result !== false) {
+ logInUser(email, password).then((results) => {
+ if (results) {
+ Cookies.set('access-token', results.accessToken, { expires: 2 });
+ Cookies.set('refresh-token', results.refreshToken, { expires: 200 });
+ Cookies.set('auth-type', 'password', { expires: 2 });
+ checkLogIn();
+ navigate('/');
+ Toastify({
+ text: 'Account is successfully created!',
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(315deg, #7ee8fa 0%, #80ff72 74%)',
+ },
+ }).showToast();
+ }
+ });
+ }
+ });
+ }
+ }
+ };
+
+ return (
+
+ );
+}
+
+export default RegistrationPage;
diff --git a/src/pages/Register/__tests__/RegistrationPage.test.tsx b/src/pages/Register/__tests__/RegistrationPage.test.tsx
new file mode 100644
index 0000000..54f8cc1
--- /dev/null
+++ b/src/pages/Register/__tests__/RegistrationPage.test.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { MemoryRouter } from 'react-router-dom';
+import RegistrationPage from '../RegistrationPage';
+
+describe('RegistrationPage component', () => {
+ test('Renders personal info inputs', () => {
+ render(
+
+ {}} />
+ ,
+ );
+
+ const headingElement = screen.getByText('Registration Page');
+ const emailLabel = screen.getByText('Email *');
+ const passwordLabel = screen.getByText('Password *');
+ const firstNameLabel = screen.getByText('First name *');
+ const lastNameLabel = screen.getByText('Last name *');
+ const dateOfBirthLabel = screen.getByText('Date of birth *');
+
+ expect(headingElement).toBeInTheDocument();
+ expect(emailLabel).toBeInTheDocument();
+ expect(passwordLabel).toBeInTheDocument();
+ expect(firstNameLabel).toBeInTheDocument();
+ expect(lastNameLabel).toBeInTheDocument();
+ expect(dateOfBirthLabel).toBeInTheDocument();
+ });
+});
diff --git a/src/services/AuthService/AuthService.ts b/src/services/AuthService/AuthService.ts
new file mode 100644
index 0000000..be1cfa4
--- /dev/null
+++ b/src/services/AuthService/AuthService.ts
@@ -0,0 +1,816 @@
+import axios, { AxiosError } from 'axios';
+import Toastify from 'toastify-js';
+import { ResponseErrorItem } from '@interfaces/Errors';
+import { CustomerData, CustomerDraft, CustomersId, SendAddress } from '@interfaces/Customer';
+import 'toastify-js/src/toastify.css';
+import Cookies from 'js-cookie';
+import { Cart } from '@src/interfaces/Cart';
+
+const authHost = 'https://auth.europe-west1.gcp.commercetools.com';
+const apiUrl = 'https://api.europe-west1.gcp.commercetools.com';
+
+const apiId = 'Cshtoo22G2afdntJDUBkDtc0';
+const apiSecret = '0xZeWTiFELWzFjDBT_vfD48YDRdlfALK';
+const apiScope =
+ 'manage_my_shopping_lists:rs-alchemists-ecommerce manage_my_payments:rs-alchemists-ecommerce view_standalone_prices:rs-alchemists-ecommerce view_cart_discounts:rs-alchemists-ecommerce view_discount_codes:rs-alchemists-ecommerce view_orders:rs-alchemists-ecommerce view_messages:rs-alchemists-ecommerce view_shopping_lists:rs-alchemists-ecommerce create_anonymous_token:rs-alchemists-ecommerce view_shipping_methods:rs-alchemists-ecommerce manage_my_profile:rs-alchemists-ecommerce view_types:rs-alchemists-ecommerce view_categories:rs-alchemists-ecommerce view_order_edits:rs-alchemists-ecommerce manage_my_business_units:rs-alchemists-ecommerce view_products:rs-alchemists-ecommerce manage_my_orders:rs-alchemists-ecommerce';
+const projectKey = 'rs-alchemists-ecommerce';
+
+const registerUser = async (userData: CustomerDraft, token: string): Promise => {
+ let errorText = '';
+
+ try {
+ const response = await axios.post(`${apiUrl}/${projectKey}/me/signup`, userData, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (response.status === 201) {
+ const result = response.data;
+ return result;
+ }
+ errorText = response.data.message;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return false;
+};
+
+const getClientAccessToken = async (): Promise<{
+ accessToken: string;
+}> => {
+ const authHeader = `Basic ${btoa(`${apiId}:${apiSecret}`)}`;
+ const response = await axios.post(`${authHost}/oauth/token`, `grant_type=client_credentials&scope=${apiScope}`, {
+ headers: {
+ Authorization: authHeader,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+
+ const accessToken = response.data.access_token;
+ return { accessToken };
+};
+
+const getAnonymousToken = async (): Promise<{
+ accessToken: string;
+ refreshToken: string;
+}> => {
+ const scope = `create_anonymous_token:${projectKey} manage_my_orders:${projectKey} manage_my_profile:${projectKey}`;
+ const authHeader = `Basic ${btoa(`${apiId}:${apiSecret}`)}`;
+ const response = await axios.post(
+ `${authHost}/oauth/${projectKey}/anonymous/token`,
+ `grant_type=client_credentials&scope=${scope}`,
+ {
+ headers: {
+ Authorization: authHeader,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ },
+ );
+
+ const accessToken = response.data.access_token;
+ const refreshToken = response.data.refresh_token;
+ return { accessToken, refreshToken };
+};
+
+const logInUserWithCart = async (
+ email: string,
+ password: string,
+): Promise<{ cart: Cart | undefined; customer: CustomerData } | undefined> => {
+ let errorText;
+ try {
+ const token = Cookies.get('anon-token');
+ const authHeader = `Bearer ${token}`;
+ const requestBody = {
+ email,
+ password,
+ };
+
+ const response = await axios.post(`${apiUrl}/${projectKey}/me/login`, requestBody, {
+ headers: {
+ Authorization: authHeader,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const { customer, cart } = response.data;
+
+ return { customer, cart };
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const logInUser = async (
+ email: string,
+ password: string,
+): Promise<
+ | {
+ accessToken: string;
+ refreshToken: string;
+ }
+ | undefined
+> => {
+ let errorText;
+ try {
+ const authHeader = `Basic ${btoa(`${apiId}:${apiSecret}`)}`;
+ const requestBody = `grant_type=password&username=${email}&password=${password}&scope=${apiScope}`;
+
+ const response = await axios.post(`${authHost}/oauth/${projectKey}/customers/token`, requestBody, {
+ headers: {
+ Authorization: authHeader,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+
+ const accessToken = response.data.access_token;
+ const refreshToken = response.data.refresh_token;
+ return { accessToken, refreshToken };
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const getCustomerId = async (): Promise => {
+ const response = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ },
+ });
+ return response.data;
+};
+
+const getNewToken = async (
+ refreshToken: string,
+): Promise<{
+ accessToken: string;
+}> => {
+ const authHeader = `Basic ${btoa(`${apiId}:${apiSecret}`)}`;
+ const response = await axios.post(
+ `${authHost}/oauth/token`,
+ `grant_type=refresh_token&refresh_token=${refreshToken}`,
+ {
+ headers: {
+ Authorization: authHeader,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ },
+ );
+
+ const accessToken = response.data.access_token;
+ return { accessToken };
+};
+
+const sendData = async (data: SendAddress, id: string, addressId: string): Promise => {
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'changeAddress',
+ addressId: `${addressId}`,
+ address: data,
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+};
+
+const changePasswordRequest = async (
+ currentPassword: string,
+ newPassword: string,
+): Promise => {
+ let errorText;
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ try {
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me/password`,
+ {
+ version: currentVersion,
+ currentPassword: newPassword,
+ newPassword: currentPassword,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const changeEmailRequest = async (newEmail: string): Promise => {
+ let errorText;
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ try {
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'changeEmail',
+ email: newEmail,
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const changeFirstNameRequest = async (firstName: string): Promise => {
+ let errorText;
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ try {
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'setFirstName',
+ firstName,
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const changeDateofBirthRequest = async (dateofBirth: string): Promise => {
+ let errorText;
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ try {
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'setDateOfBirth',
+ dateOfBirth: String(dateofBirth),
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const changeLastNameRequest = async (lastName: string): Promise => {
+ let errorText;
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ try {
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'setLastName',
+ lastName,
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ return response.data;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const requestRemoveAddress = async (addressId: string): Promise => {
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'removeAddress',
+ addressId,
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+};
+
+const requestAddShippingAddress = async (
+ streetName: string,
+ postalCode: string,
+ city: string,
+ country: string,
+): Promise => {
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'addAddress',
+ address: {
+ streetName,
+ postalCode,
+ city,
+ country,
+ },
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+};
+
+const requestIdShippingAddress = async (idAddress: string): Promise => {
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'addShippingAddressId',
+ addressId: idAddress,
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+};
+
+const requestIdBillingAddress = async (idAddress: string): Promise => {
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'addBillingAddressId',
+ addressId: idAddress,
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+};
+
+const requestAddBillingAddress = async (
+ streetName: string,
+ postalCode: string,
+ city: string,
+ country: string,
+): Promise => {
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'addAddress',
+ address: {
+ streetName,
+ postalCode,
+ city,
+ country,
+ },
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+};
+
+const requestDefaultBillingAddress = async (addressId: string): Promise => {
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'setDefaultBillingAddress',
+ addressId,
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+};
+
+const requestDefaultShippingAddress = async (addressId: string): Promise => {
+ const profileResponse = await axios.get(`${apiUrl}/${projectKey}/me`, {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const currentVersion = profileResponse.data.version;
+ const response = await axios.post(
+ `${apiUrl}/${projectKey}/me`,
+ {
+ version: currentVersion,
+ actions: [
+ {
+ action: 'setDefaultShippingAddress',
+ addressId,
+ },
+ ],
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${Cookies.get('access-token')}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data;
+};
+
+export {
+ registerUser,
+ logInUser,
+ logInUserWithCart,
+ getAnonymousToken,
+ getClientAccessToken,
+ getCustomerId,
+ getNewToken,
+ sendData,
+ changePasswordRequest,
+ changeEmailRequest,
+ changeLastNameRequest,
+ changeFirstNameRequest,
+ changeDateofBirthRequest,
+ requestRemoveAddress,
+ requestAddShippingAddress,
+ requestIdShippingAddress,
+ requestIdBillingAddress,
+ requestAddBillingAddress,
+ requestDefaultBillingAddress,
+ requestDefaultShippingAddress,
+};
diff --git a/src/services/AuthService/__tests__/AuthService.test.tsx b/src/services/AuthService/__tests__/AuthService.test.tsx
new file mode 100644
index 0000000..12f614e
--- /dev/null
+++ b/src/services/AuthService/__tests__/AuthService.test.tsx
@@ -0,0 +1,72 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { getClientAccessToken, logInUser } from '../AuthService';
+
+describe('Get client access token', () => {
+ const email = 'qwertyyu@gmail.com';
+ const password = '123456qQ';
+ const projectKey = 'rs-alchemists-ecommerce';
+ const authHost = 'https://auth.europe-west1.gcp.commercetools.com';
+
+ test('should return client access token', async () => {
+ const response = await getClientAccessToken();
+ expect(() => response).not.toThrow();
+ });
+ test('should return access token', async () => {
+ const response = await getClientAccessToken();
+ expect(Object.keys(response)).toStrictEqual(['accessToken']);
+ });
+
+ test('return accessToken at successful auth', async () => {
+ const result = await logInUser(email, password);
+ if (result !== undefined) expect(Object.keys(result)).toStrictEqual(['accessToken', 'refreshToken']);
+ });
+
+ // test('createCart post', async () => {
+ // const response = await getAnonymousAccessToken();
+ // const result = await createCart(response.accessToken);
+ // expect(Object.keys(result)).toStrictEqual([
+ // 'type',
+ // 'id',
+ // 'version',
+ // 'versionModifiedAt',
+ // 'lastMessageSequenceNumber',
+ // 'createdAt',
+ // 'lastModifiedAt',
+ // 'lastModifiedBy',
+ // 'createdBy',
+ // 'anonymousId',
+ // 'lineItems',
+ // 'cartState',
+ // 'totalPrice',
+ // 'shippingMode',
+ // 'shipping',
+ // 'customLineItems',
+ // 'discountCodes',
+ // 'directDiscounts',
+ // 'inventoryMode',
+ // 'taxMode',
+ // 'taxRoundingMode',
+ // 'taxCalculationMode',
+ // 'deleteDaysAfterLastModification',
+ // 'refusedGifts',
+ // 'origin',
+ // 'itemShippingAddresses',
+ // ]);
+ // });
+
+ test('Error with errorMessage from object response.data.errors', async () => {
+ const mock = new MockAdapter(axios);
+ const detailedErrorMessage = 'Детальное сообщение об ошибке';
+ const errorMessage = 'Сообщение об ошибке';
+
+ mock.onPost(`${authHost}/oauth/${projectKey}/customers/token`).reply(400, {
+ errors: [{ message: errorMessage, detailedErrorMessage }],
+ });
+
+ const result = await logInUser(email, password);
+
+ expect(result).toBeUndefined();
+ mock.reset();
+ });
+});
diff --git a/src/services/CartService/CartService.ts b/src/services/CartService/CartService.ts
new file mode 100644
index 0000000..af11254
--- /dev/null
+++ b/src/services/CartService/CartService.ts
@@ -0,0 +1,266 @@
+import axios, { AxiosError } from 'axios';
+import Toastify from 'toastify-js';
+import { Cart } from '@interfaces/Cart';
+import { ResponseErrorItem } from '@src/interfaces/Errors';
+
+const projectKey = 'rs-alchemists-ecommerce';
+const apiUrl = 'https://api.europe-west1.gcp.commercetools.com';
+
+const createCart = async (token: string): Promise => {
+ const cartEndpoint = `${apiUrl}/${projectKey}/me/carts`;
+
+ const requestBody = {
+ currency: 'EUR',
+ };
+
+ const response = await axios.post(cartEndpoint, JSON.stringify(requestBody), {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const cart: Cart = response.data;
+ return cart;
+};
+
+const addToCart = async (
+ token: string,
+ cartId: string,
+ productSku: string,
+ version: number,
+ quantity: number,
+): Promise => {
+ const cartEndpoint = `${apiUrl}/${projectKey}/me/carts/${cartId}`;
+
+ const requestBody = {
+ version,
+ actions: [
+ {
+ action: 'addLineItem',
+ quantity,
+ sku: productSku,
+ },
+ ],
+ };
+
+ const response = await axios.post(cartEndpoint, JSON.stringify(requestBody), {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const cart: Cart = response.data;
+ return cart;
+};
+
+const getCartByCustomerId = async (token: string, customerId: string): Promise => {
+ let url = `${apiUrl}/${projectKey}/me/carts`;
+
+ url += `?where=customerId="${customerId}"`;
+
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const cart: Cart = response.data.results[0];
+ return cart;
+};
+
+const getCartByAnonId = async (token: string, anonymousId: string): Promise => {
+ let url = `${apiUrl}/${projectKey}/me/carts`;
+
+ url += `?where=anonymousId="${anonymousId}"`;
+
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const cart: Cart = response.data.results[0];
+ return cart;
+};
+
+const getCartById = async (token: string, id: string): Promise => {
+ const url = `${apiUrl}/${projectKey}/me/carts/${id}`;
+
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const cart: Cart = response.data;
+ return cart;
+};
+
+const removeFromCart = async (
+ token: string,
+ cartId: string,
+ itemId: string,
+ version: number,
+ quantity?: number,
+): Promise => {
+ const cartEndpoint = `${apiUrl}/${projectKey}/me/carts/${cartId}`;
+
+ const requestBody = {
+ version,
+ actions: [
+ {
+ action: 'removeLineItem',
+ lineItemId: itemId,
+ quantity,
+ },
+ ],
+ };
+
+ const response = await axios.post(cartEndpoint, JSON.stringify(requestBody), {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const cart: Cart = response.data;
+ return cart;
+};
+
+const addDiscountCode = async (
+ token: string,
+ cartId: string,
+ version: number,
+ code: string,
+): Promise => {
+ const cartEndpoint = `${apiUrl}/${projectKey}/me/carts/${cartId}`;
+
+ const requestBody = {
+ version,
+ actions: [
+ {
+ action: 'addDiscountCode',
+ code,
+ },
+ ],
+ };
+
+ let errorText;
+ try {
+ const response = await axios.post(cartEndpoint, JSON.stringify(requestBody), {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const cart: Cart = response.data;
+ return cart;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const removeDiscountCode = async (
+ token: string,
+ cartId: string,
+ version: number,
+ discountId: string,
+): Promise => {
+ const cartEndpoint = `${apiUrl}/${projectKey}/me/carts/${cartId}`;
+
+ const requestBody = {
+ version,
+ actions: [
+ {
+ action: 'removeDiscountCode',
+ discountCode: {
+ typeId: 'discount-code',
+ id: discountId,
+ },
+ },
+ ],
+ };
+ let errorText;
+ try {
+ const response = await axios.post(cartEndpoint, JSON.stringify(requestBody), {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const cart: Cart = response.data;
+ return cart;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+export {
+ createCart,
+ addToCart,
+ removeFromCart,
+ addDiscountCode,
+ removeDiscountCode,
+ getCartByCustomerId,
+ getCartByAnonId,
+ getCartById,
+};
diff --git a/src/services/DiscountService/DiscountService.ts b/src/services/DiscountService/DiscountService.ts
new file mode 100644
index 0000000..f46ce47
--- /dev/null
+++ b/src/services/DiscountService/DiscountService.ts
@@ -0,0 +1,138 @@
+import { ResponseErrorItem } from '@src/interfaces/Errors';
+import axios, { AxiosError } from 'axios';
+import Toastify from 'toastify-js';
+import Cookies from 'js-cookie';
+import 'toastify-js/src/toastify.css';
+import { DiscountCode } from '@src/interfaces/Discount';
+import { CartDiscount } from '@src/interfaces/Cart';
+
+const apiUrl = 'https://api.europe-west1.gcp.commercetools.com';
+const projectKey = 'rs-alchemists-ecommerce';
+
+const getDiscountCodes = async (): Promise => {
+ const token = Cookies.get('access-token');
+ const url = `${apiUrl}/${projectKey}/discount-codes`;
+ let errorText;
+ try {
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return response.data.results;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const getDiscountCodeById = async (id: string): Promise => {
+ const token = Cookies.get('access-token');
+ const url = `${apiUrl}/${projectKey}/discount-codes/${id}`;
+ let errorText;
+ try {
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return response.data;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const getCartDiscountCodeById = async (id: string): Promise => {
+ const token = Cookies.get('access-token');
+ const url = `${apiUrl}/${projectKey}/cart-discounts/${id}`;
+ let errorText;
+ try {
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return response.data;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+export { getDiscountCodes, getDiscountCodeById, getCartDiscountCodeById };
diff --git a/src/services/ProductsService/ProductsService.ts b/src/services/ProductsService/ProductsService.ts
new file mode 100644
index 0000000..4fe4231
--- /dev/null
+++ b/src/services/ProductsService/ProductsService.ts
@@ -0,0 +1,161 @@
+import { ResponseErrorItem } from '@src/interfaces/Errors';
+import { ProductDetailedPage, ProductCatalog } from '@src/interfaces/Product';
+import { Category } from '@src/interfaces/Category';
+import axios, { AxiosError } from 'axios';
+import Toastify from 'toastify-js';
+import Cookies from 'js-cookie';
+import 'toastify-js/src/toastify.css';
+
+const apiUrl = 'https://api.europe-west1.gcp.commercetools.com';
+const projectKey = 'rs-alchemists-ecommerce';
+
+const getProductById = async (id: string): Promise => {
+ const token = Cookies.get('access-token');
+ const url = `${apiUrl}/${projectKey}/products/${id}`;
+ let errorText;
+ try {
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return response.data;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response?.data) {
+ if (e.response.data?.errors.length) {
+ errorText = e.response.data.errors
+ .map((errItem: ResponseErrorItem) => errItem.detailedErrorMessage || errItem.message)
+ .join('\r\n');
+ } else {
+ errorText = e.response.data?.message;
+ }
+ } else if (e instanceof Error) {
+ errorText = e.message;
+ } else if (typeof e === 'string') {
+ errorText = e;
+ }
+ }
+
+ Toastify({
+ text: errorText,
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(to right, #ff0000, #fdacac)',
+ },
+ }).showToast();
+ return undefined;
+};
+
+const getProductByKey = async (key: string): Promise => {
+ const token = Cookies.get('access-token');
+ const url = `${apiUrl}/${projectKey}/products/key=${key}`;
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return response.data;
+};
+
+const getProducts = async (): Promise<{
+ limit: number;
+ offset: number;
+ count: number;
+ total: number;
+ results: ProductCatalog[];
+}> => {
+ const token = Cookies.get('access-token');
+ const url = `${apiUrl}/${projectKey}/products`;
+
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return response.data;
+};
+
+const getCategories = async (
+ query?: string,
+): Promise<{
+ limit: number;
+ offset: number;
+ count: number;
+ total: number;
+ results: Category[];
+}> => {
+ const token = Cookies.get('access-token');
+ let url = `${apiUrl}/${projectKey}/categories`;
+
+ if (query) {
+ const encodedQuery = encodeURIComponent(query);
+ url += `?where=${encodedQuery}`;
+ }
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return response.data;
+};
+
+const getCategory = async (key: string): Promise => {
+ const token = Cookies.get('access-token');
+ const url = `${apiUrl}/${projectKey}/categories/key=${key}`;
+
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data;
+};
+
+const getProductsByCategory = async (
+ filter: string,
+ sort: string,
+ text?: string,
+ brand?: string,
+ limit?: number,
+ offset?: number,
+): Promise<{
+ limit: number;
+ offset: number;
+ count: number;
+ total: number;
+ results: ProductCatalog[];
+}> => {
+ const token = Cookies.get('access-token');
+ let url = `${apiUrl}/${projectKey}/product-projections/search?filter=${filter}&sort=${sort}`;
+
+ if (text) {
+ url += `&text.en=${text}`;
+ }
+
+ if (brand) {
+ url += `&filter=variants.attributes.brand:"${brand}"`;
+ }
+
+ if (limit) {
+ url += `&limit=${limit}`;
+ }
+
+ if (offset) {
+ url += `&offset=${offset}`;
+ }
+
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return response.data;
+};
+
+export { getProducts, getCategories, getCategory, getProductsByCategory, getProductByKey, getProductById };
diff --git a/src/services/ProductsService/__tests__/ProductsService.test.ts b/src/services/ProductsService/__tests__/ProductsService.test.ts
new file mode 100644
index 0000000..20648eb
--- /dev/null
+++ b/src/services/ProductsService/__tests__/ProductsService.test.ts
@@ -0,0 +1,132 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { getProducts, getCategories, getCategory, getProductsByCategory } from '../ProductsService';
+
+const mock = new MockAdapter(axios);
+
+describe('API Functions', () => {
+ afterEach(() => {
+ mock.reset();
+ });
+
+ it('should get products', async () => {
+ const responseData = {
+ limit: 10,
+ offset: 0,
+ count: 10,
+ total: 100,
+ results: [{}],
+ };
+
+ mock
+ .onGet('https://api.europe-west1.gcp.commercetools.com/rs-alchemists-ecommerce/products')
+ .reply(200, responseData);
+
+ const products = await getProducts();
+
+ expect(products).toEqual(responseData);
+ });
+
+ it('should get categories', async () => {
+ const responseData = {
+ limit: 10,
+ offset: 0,
+ count: 10,
+ total: 100,
+ results: [{}],
+ };
+
+ mock
+ .onGet('https://api.europe-west1.gcp.commercetools.com/rs-alchemists-ecommerce/categories')
+ .reply(200, responseData);
+
+ const categories = await getCategories();
+
+ expect(categories).toEqual(responseData);
+ });
+
+ it('should get categories with query', async () => {
+ const responseData = {
+ limit: 10,
+ offset: 0,
+ count: 10,
+ total: 100,
+ results: [{}],
+ };
+
+ const query = 'test-query';
+
+ mock
+ .onGet('https://api.europe-west1.gcp.commercetools.com/rs-alchemists-ecommerce/categories?where=test-query')
+ .reply(200, responseData);
+
+ const categories = await getCategories(query);
+
+ expect(categories).toEqual(responseData);
+ });
+
+ it('should get a category with where filter', async () => {
+ const categoryKey = 'test-category-key';
+ const responseData = {};
+
+ mock
+ .onGet(`https://api.europe-west1.gcp.commercetools.com/rs-alchemists-ecommerce/categories/key=${categoryKey}`)
+ .reply(200, responseData);
+
+ const category = await getCategory(categoryKey);
+
+ expect(category).toEqual(responseData);
+ });
+
+ it('should get products by category', async () => {
+ const filter = 'test-filter';
+ const sort = 'test-sort';
+ const responseData = {
+ limit: 10,
+ offset: 0,
+ count: 10,
+ total: 100,
+ results: [{}],
+ };
+
+ mock.onGet().reply((config) => {
+ expect(config.url).toContain('product-projections/search');
+ if (config.headers) {
+ expect(config.headers.Authorization).toContain('Bearer');
+ }
+
+ return [200, responseData];
+ });
+
+ const products = await getProductsByCategory(filter, sort);
+
+ expect(products).toEqual(responseData);
+ });
+
+ it('should get products by category with brand and text filter', async () => {
+ const filter = 'test-filter';
+ const sort = 'test-sort';
+ const brand = 'test-brand';
+ const text = 'test-text';
+ const responseData = {
+ limit: 10,
+ offset: 0,
+ count: 10,
+ total: 100,
+ results: [{}],
+ };
+
+ mock.onGet().reply((config) => {
+ expect(config.url).toContain('product-projections/search');
+ if (config.headers) {
+ expect(config.headers.Authorization).toContain('Bearer');
+ }
+
+ return [200, responseData];
+ });
+
+ const products = await getProductsByCategory(filter, sort, text, brand);
+
+ expect(products).toEqual(responseData);
+ });
+});
diff --git a/src/styleMock.ts b/src/styleMock.ts
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/src/styleMock.ts
@@ -0,0 +1 @@
+export {};
diff --git a/src/utilities/.gitkeep b/src/utilities/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/utilities/addItemCart.ts b/src/utilities/addItemCart.ts
new file mode 100644
index 0000000..bc605bd
--- /dev/null
+++ b/src/utilities/addItemCart.ts
@@ -0,0 +1,56 @@
+import { getAnonymousToken } from '@src/services/AuthService/AuthService';
+import { getCartById, addToCart, createCart } from '@src/services/CartService/CartService';
+import Cookies from 'js-cookie';
+import Toastify from 'toastify-js';
+import getCookieToken from './getCookieToken';
+
+const addItemCart = async (product: string, quantity = 1): Promise<{ productId: string; id: string }[] | false> => {
+ const cartId = Cookies.get('cart-id');
+ let resultCart;
+
+ if (cartId) {
+ const token = await getCookieToken();
+ if (token) {
+ const cart = await getCartById(token, cartId);
+ resultCart = await addToCart(token, cart.id, product, cart.version, quantity);
+ }
+ } else {
+ const response = await getAnonymousToken();
+ const threeHours = 180 / (24 * 60);
+
+ Cookies.set('anon-token', response.accessToken, { expires: threeHours });
+ const currentDate = new Date();
+ currentDate.setHours(currentDate.getHours() + 3);
+ Cookies.set('anon-token-expires', currentDate.toISOString(), { expires: threeHours });
+ Cookies.set('anon-refresh-token', response.refreshToken, { expires: 200 });
+
+ const cart = await createCart(response.accessToken);
+ Cookies.set('cart-id', cart.id, { expires: 200 });
+
+ resultCart = await addToCart(response.accessToken, cart.id, product, cart.version, quantity);
+ }
+
+ if (resultCart) {
+ const formattedCart = resultCart.lineItems.map((lineItem) => ({
+ productId: lineItem.productId,
+ id: lineItem.id,
+ }));
+ Toastify({
+ text: 'Product is added to the cart',
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(315deg, #7ee8fa 0%, #80ff72 74%)',
+ },
+ }).showToast();
+ return formattedCart;
+ }
+
+ return false;
+};
+
+export default addItemCart;
diff --git a/src/utilities/formattedCategoryList.ts b/src/utilities/formattedCategoryList.ts
new file mode 100644
index 0000000..f4ac2b0
--- /dev/null
+++ b/src/utilities/formattedCategoryList.ts
@@ -0,0 +1,78 @@
+import { ProductFormattedData } from '@src/interfaces/Product';
+import { getCategories } from '@src/services/ProductsService/ProductsService';
+
+async function formattedCategoryList(): Promise<{
+ mainCategories: ProductFormattedData[];
+ subCategories: ProductFormattedData[];
+ subCategories2: ProductFormattedData[];
+}> {
+ const mainCategories: ProductFormattedData[] = [];
+ const subCategories: ProductFormattedData[] = [];
+ const subCategories2: ProductFormattedData[] = [];
+
+ const data1 = await getCategories('parent is not defined');
+ data1.results.forEach((item) => {
+ const category: ProductFormattedData = {
+ name: item.name.en,
+ id: item.id,
+ ancestors: [],
+ slug: item.slug.en,
+ };
+ mainCategories.push(category);
+ });
+
+ const data2 = await getCategories('parent is defined');
+ data2.results.forEach((item) => {
+ const category: ProductFormattedData = {
+ name: item.name.en,
+ id: item.id,
+ ancestors: [],
+ slug: item.slug.en,
+ };
+
+ if (item.parent?.id) {
+ mainCategories
+ .filter((c) => c.id === item.parent?.id)
+ .forEach((c) => {
+ if (!c.ancestors.find((ancestor) => ancestor.id === item.id)) {
+ subCategories.push(category);
+ c.ancestors.push(category);
+ const itemCopy = item;
+ itemCopy.used = true;
+ }
+ });
+ }
+ });
+
+ data2.results
+ .filter((item) => !item.used)
+ .forEach((item) => {
+ const category: ProductFormattedData = {
+ name: item.name.en,
+ id: item.id,
+ ancestors: [],
+ slug: item.slug.en,
+ };
+
+ if (item.parent?.id) {
+ subCategories
+ .filter((c) => c.id === item.parent?.id)
+ .forEach((c) => {
+ if (!c.ancestors.find((ancestor) => ancestor.id === item.id)) {
+ subCategories2.push(category);
+ c.ancestors.push(category);
+ const itemCopy = item;
+ itemCopy.used = true;
+ }
+ });
+ }
+ });
+
+ return {
+ mainCategories,
+ subCategories,
+ subCategories2,
+ };
+}
+
+export default formattedCategoryList;
diff --git a/src/utilities/getCookieToken.ts b/src/utilities/getCookieToken.ts
new file mode 100644
index 0000000..d8df083
--- /dev/null
+++ b/src/utilities/getCookieToken.ts
@@ -0,0 +1,41 @@
+import { getNewToken } from '@src/services/AuthService/AuthService';
+import Cookies from 'js-cookie';
+
+async function getCookieToken(): Promise {
+ const threeHours = 180 / (24 * 60);
+ const currentDate = new Date();
+ const currentPlusFiveMinutes = currentDate.getTime() + 250000;
+
+ const authType = Cookies.get('auth-type');
+ const accessToken = Cookies.get('access-token');
+ let anonToken = Cookies.get('anon-token');
+ const anonRefreshToken = Cookies.get('anon-refresh-token');
+ const anonTokenExpires = Cookies.get('anon-token-expires');
+
+ if (anonTokenExpires) {
+ const anonExpiryDate = new Date(anonTokenExpires);
+
+ if (currentPlusFiveMinutes >= anonExpiryDate.getTime()) {
+ anonToken = '';
+ Cookies.remove('anon-token');
+ Cookies.remove('anon-token-expires');
+ }
+ }
+
+ if (authType === 'password' && accessToken) return accessToken;
+
+ if (anonToken) return anonToken;
+
+ if (anonRefreshToken) {
+ const token = await getNewToken(anonRefreshToken);
+ Cookies.set('anon-token', token.accessToken, { expires: threeHours });
+ Cookies.set('anon-refresh-token', anonRefreshToken, { expires: 200 });
+ currentDate.setHours(currentDate.getHours() + 3);
+ Cookies.set('anon-token-expires', currentDate.toISOString(), { expires: threeHours });
+ return token.accessToken;
+ }
+
+ return '';
+}
+
+export default getCookieToken;
diff --git a/src/utilities/getFormattedCart.ts b/src/utilities/getFormattedCart.ts
new file mode 100644
index 0000000..d9bf8cf
--- /dev/null
+++ b/src/utilities/getFormattedCart.ts
@@ -0,0 +1,22 @@
+import { getCartById } from '@src/services/CartService/CartService';
+import Cookies from 'js-cookie';
+import getCookieToken from './getCookieToken';
+
+const getFormattedCart = async (): Promise<{ productId: string; id: string }[] | false> => {
+ const cartId = Cookies.get('cart-id');
+ if (cartId) {
+ const token = await getCookieToken();
+ if (token) {
+ const cart = await getCartById(token, cartId);
+ const formattedCart = cart.lineItems.map((lineItem) => ({
+ productId: lineItem.productId,
+ id: lineItem.id,
+ }));
+ return formattedCart;
+ }
+ }
+ Cookies.remove('cart-id');
+ return false;
+};
+
+export default getFormattedCart;
diff --git a/src/utilities/removeItemCart.ts b/src/utilities/removeItemCart.ts
new file mode 100644
index 0000000..60cf780
--- /dev/null
+++ b/src/utilities/removeItemCart.ts
@@ -0,0 +1,40 @@
+import { getCartById, removeFromCart } from '@src/services/CartService/CartService';
+import Cookies from 'js-cookie';
+import Toastify from 'toastify-js';
+import getCookieToken from './getCookieToken';
+
+const removeItemCart = async (product: string): Promise<{ productId: string; id: string }[] | false> => {
+ const cartId = Cookies.get('cart-id');
+ let resultCart;
+
+ if (cartId) {
+ const token = await getCookieToken();
+ if (token) {
+ const cart = await getCartById(token, cartId);
+ resultCart = await removeFromCart(token, cart.id, product, cart.version);
+ }
+ }
+
+ if (resultCart) {
+ const formattedCart = resultCart.lineItems.map((lineItem) => ({
+ productId: lineItem.productId,
+ id: lineItem.id,
+ }));
+ Toastify({
+ text: 'Product is removed from the cart',
+ duration: 3000,
+ newWindow: true,
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ stopOnFocus: true,
+ style: {
+ background: 'linear-gradient(315deg, #7ee8fa 0%, #80ff72 74%)',
+ },
+ }).showToast();
+ return formattedCart;
+ }
+ return false;
+};
+
+export default removeItemCart;
diff --git a/src/utilities/returnCartPrice.ts b/src/utilities/returnCartPrice.ts
new file mode 100644
index 0000000..aa0d0d1
--- /dev/null
+++ b/src/utilities/returnCartPrice.ts
@@ -0,0 +1,17 @@
+import { getCartById } from '@src/services/CartService/CartService';
+import Cookies from 'js-cookie';
+import getCookieToken from './getCookieToken';
+
+const returnCartPrice = async (): Promise => {
+ const cartId = Cookies.get('cart-id');
+ if (cartId) {
+ const token = await getCookieToken();
+ if (token) {
+ const cart = await getCartById(token, cartId);
+ return cart.totalPrice.centAmount;
+ }
+ }
+ return false;
+};
+
+export default returnCartPrice;
diff --git a/src/utilities/sortingOptions.ts b/src/utilities/sortingOptions.ts
new file mode 100644
index 0000000..98e03c4
--- /dev/null
+++ b/src/utilities/sortingOptions.ts
@@ -0,0 +1,8 @@
+const sortingOptions = [
+ { value: 'name.en asc', label: 'Name (Ascending)' },
+ { value: 'name.en desc', label: 'Name (Descending)' },
+ { value: 'price asc', label: 'Price (Ascending)' },
+ { value: 'price desc', label: 'Price (Descending)' },
+];
+
+export default sortingOptions;
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..fe273f9
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "baseUrl": ".",
+ "paths": {
+ "@src/*": ["./src/*"],
+ "@components/*": ["./src/components/*"],
+ "@pages/*": ["./src/pages/*"],
+ "@services/*": ["./src/services/*"],
+ "@interfaces/*": ["./src/interfaces/*"],
+ "@assets/*": ["./src/assets/*"]
+ }
+ },
+ "include": ["src", "jest.config.ts"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
\ No newline at end of file
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..65dbdb9
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Node"
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..72ff2eb
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import * as path from 'path';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+
+ resolve: {
+ alias: {
+ 'node-fetch': 'isomorphic-fetch',
+ '@src': path.resolve(__dirname, './src'),
+ '@components': path.resolve(__dirname, './src/components'),
+ '@pages': path.resolve(__dirname, './src/pages'),
+ '@services': path.resolve(__dirname, './src/services'),
+ '@interfaces': path.resolve(__dirname, './src/interfaces'),
+ '@assets': path.resolve(__dirname, './src/assets'),
+ },
+ },
+});