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 }) => ( + + + + )); + 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]) => ( +
+ +
+ ))} +
+ ); +} + +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 ( + <> + + + + + + + + ); +} + +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} +

{name.en}

+ {price} +
+ + + + + +
+
+ ); +} + +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 ( + <> +
+ +
+ + + + + ); +} + +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 ( +
+ +
+ ); +} + +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 }) => ( +
  • + + + +
  • + )); + + return ( +
    +
    + +
    + logo +
    + + + + + +
    +
    + ); +} + +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 ( +
    +
    +

    Log in

    + + +
    + + + + +
    + +
    + ); +} + +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 ( +
    setActive(false)}> +
    e.stopPropagation()} + onSubmit={(e): void => { + e.preventDefault(); + sendData(selectedData, userId, selectedData.addressId).then((item) => { + const date = item.addresses.filter((adress) => adress.id === selectedData.addressId)[0]; + setUserAccount({ ...userAccount, addresses: item.addresses }); + setSelectedData({ + addressId: date.id, + city: date.city, + postalCode: date.postalCode, + country: date.country, + streetName: date.streetName, + }); + Toastify({ + text: 'Update address successfully!', + duration: 3000, + newWindow: true, + close: true, + gravity: 'top', + position: 'right', + stopOnFocus: true, + style: { + background: 'linear-gradient(315deg, #7ee8fa 0%, #80ff72 74%)', + }, + }).showToast(); + setActive(false); + }); + }} + > +
    + +
    + + + + + +
    + ); +} + +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)}> +
    e.stopPropagation()} + onSubmit={onSubmit} + > + {children} +
    +
    + ); +} + +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 ( + <> + {/* */} + + + + + + ); +} + +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 ( +
    +
    +

    Price range

    +

    EUR

    +
    +
    + handleInputChange(event, 0)} + /> + handleInputChange(event, 1)} + /> +
    + +
    + ); +} + +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 ( +
    + +
    + ); +} + +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. +

    +
    + +
    + + RS School + +
    + + ); +} 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); + }} + /> +
    +
    + +
    + 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} + /> + {' '} + +
    + ); + } + + 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{' '} + + {modalActiveNewAddressBilling && + modalWindow( + modalActiveNewAddressBilling, + setModalActiveNewAddressBilling, + requestAddBillingAddress, + requestIdBillingAddress, + requestDefaultBillingAddress, + 'billing', + )} +
    + {billingAddress ? billingAddressArr : You have not set a default billing address.} + +
    + Shipping Adresses{' '} + + {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

    +
    + + +
    { + e.preventDefault(); + changeLastNameRequest(editUserInformation.lastName).then((item) => { + if (item !== undefined) { + changeFirstNameRequest(editUserInformation.firstName).then((items) => { + if (items !== undefined) { + changeDateofBirthRequest(editUserInformation.dateOfBirth).then((birth) => { + if (birth !== undefined) { + Toastify({ + text: 'Information 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(); + setEditInformation(false); + } + }); + } + }); + } + }); + }} + > + setEditUserInformation({ ...editUserInformation, firstName: e.target.value })} + id="firstName" + type="text" + pattern="[A-Za-z]+" + title="Must contain at least one character and no special characters or numbers" + value={editUserInformation.firstName} + disabled={!editInformation} + /> + setEditUserInformation({ ...editUserInformation, lastName: e.target.value })} + id="lastName" + type="text" + pattern="[A-Za-z]+" + title="Must contain at least one character and no special characters or numbers" + value={editUserInformation.lastName} + disabled={!editInformation} + /> + { + setEditUserInformation({ ...editUserInformation, dateOfBirth: e.target.value }); + }} + id="dateOfBirth" + type="date" + pattern=".*" + title="You need to be older than 13 years old" + max={getFormattedDate()} + value={editUserInformation.dateOfBirth} + disabled={!editInformation} + /> + + + + + { + 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} + /> + + +
    + { + 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} + /> + + +
    +
    +
    + ); +} + +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 :) + + + + + + + + + + + ); + } + + return ( + <> + +

    Shopping Cart

    + {isModalOpen && ( +
    +

    Are you sure you want to clear your cart?

    +
    + + +
    +
    + )} +
    +
    +
    + + + + + + + + + + + + {content} +
    Product PriceQuantityTotal
    +
    + {cart.lineItems.length !== 0 && ( + + )} +
    +
    +
    + {cart.discountCodes.length ? ( + <> +

    Delete Discount Code

    + {/* */} +
    {currentDiscountCode}
    + + + ) : ( + <> +

    Apply Discount Code

    + + + + )} +
    +
    +
    +
    Subtotal
    +
    {getFormattedSum(totalCart.centSubtotal)}
    +
    +
    +
    Discount
    +
    {getFormattedSum(totalCart.centAmountDiscount)}
    +
    +
    +
    Promocode
    +
    {getFormattedSum(totalCart.centAmountDiscountPromo)}
    +
    +
    +
    ORDER TOTAL
    +
    {getFormattedSum(totalCart.centAmount)}
    +
    +
    + +
    +
    + + ); +} + +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 ( +
    +
    +
    + +
    + {categories.map((category) => ( + + ))} +
    + +
    +
    + / + { + setBreadcrumb([]); + }} + > + Catalog + +
    + + +
    +
    +
    +
    + Search icon + { + 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} +
    • + ))} +
    +
    +
    +
    + + + + + + +
    +
    + ); +} 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

    + + + +
    + ); +} 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) => ( + + {`${key}${index}`} + + ))} + + +
    + {/* */} + +

    {brand}

    + +

    {brandModel}

    + + {/*
    +
    select color
    +
    +
    +
    +
    +
    +
    + +
    +
    select size(inches)
    +
    +
    14
    +
    15
    +
    16
    +
    +
    */} + +
    +
    +
    quantity
    +
    + + {formData.count} + +
    +
    + +
    +
    price total
    + {discountedPrice > 0 ? ( + <> + {discountedPriceFormated} +  {currency} +
    + {totalPriceFormated} +  {currency} + + ) : ( + <> + {totalPriceFormated} +  {currency} + + )} +
    +
    + +
    + + +
    + + {/*
    + + +
    */} +
    +
    +
    +
    +
    +
    Details
    +
    -
    +
    + +
    +
    about product
    +
    {description}
    +
    +
    +
    +
    +
    + +
    + { + swiper2Ref.current = swiper; + }} + > + {current?.masterVariant.images.map((img: { url: string }, index: number) => ( + + {`${key}${index}`} + + ))} + +
    + + ); +} + +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 ( +
    +

    Registration Page

    +
    + + + + + +
    +
    + + + + +
    + +
    + + + {formData.sameBillingShipping ? ( + + ) : ( + + )} +
    +
    + +
    + + + + +
    + +
    + ); +} + +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'), + }, + }, +});