From 5a5cfede7aa13187517c34c842dbd4778c3e962d Mon Sep 17 00:00:00 2001 From: Bogdan <114234698+Quiddlee@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:10:06 +0300 Subject: [PATCH 001/144] Initial commit --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb1c340 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# react-components \ No newline at end of file From fe56cc07b55b5935befa99b3c33a7b9a86774c70 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 24 Oct 2023 15:23:32 +0300 Subject: [PATCH 002/144] =?UTF-8?q?init:=20start=20react-components=20task?= =?UTF-8?q?=20=F0=9F=A5=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 9 + .eslintrc.cjs | 80 + .gitignore | 24 + .husky/pre-commit | 4 + .lintstagedrc.cjs | 4 + README.md | 2 +- index.html | 19 + package-lock.json | 6402 +++++++++++++++++++++++++++++++++++++++++++ package.json | 49 + postcss.config.js | 6 + prettier.config.cjs | 10 + public/.gitkeep | 0 src/App.tsx | 9 + src/assets/.gitkeep | 0 src/index.css | 3 + src/main.tsx | 12 + src/vite-env.d.ts | 1 + tailwind.config.js | 18 + tsconfig.json | 25 + tsconfig.node.json | 10 + vite.config.js | 6 + vite.config.ts | 7 + 22 files changed, 6699 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .eslintrc.cjs create mode 100644 .gitignore create mode 100644 .husky/pre-commit create mode 100644 .lintstagedrc.cjs create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 prettier.config.cjs create mode 100644 public/.gitkeep create mode 100644 src/App.tsx create mode 100644 src/assets/.gitkeep create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/vite-env.d.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.js create mode 100644 vite.config.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ab561f3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..a8fcbdb --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,80 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'airbnb', + 'airbnb-typescript', + 'airbnb/hooks', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/recommended', + 'prettier', + ], + ignorePatterns: ['dist', '.eslintrc.cjs', 'vite.config.ts', 'prettier.config.js', '@typescript-eslint', 'node_modules', 'tailwind.config.js'], + plugins: ['react', 'react-refresh', 'simple-import-sort', 'import', 'prettier'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + project: ['tsconfig.json'], + tsconfigRootDir: __dirname, + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + 'react/prefer-stateless-function': 0, + 'no-void': 0, + 'react/react-in-jsx-scope': 0, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'import/extensions': [ + 'error', + 'ignorePackages', + { + 'ts': 'always', + 'tsx': 'always' + } + ], + 'import/namespace': 0, + 'sort-imports': ['error', {ignoreCase: true, ignoreDeclarationSort: true}], + 'import/order': [ + 'error', + { + groups: [ + ['external', 'builtin'], + 'internal', + ['sibling', 'parent'], + 'index', + ], + pathGroups: [ + { + pattern: '@(react|react-native)', + group: 'external', + position: 'before', + }, + { + pattern: '@src/**', + group: 'internal', + }, + ], + pathGroupsExcludedImportTypes: ['internal', 'react'], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + }, + ], + }, + settings: { + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + react: { + version: 'detect', + }, + }, + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..3e1cef8 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npm run precommit diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs new file mode 100644 index 0000000..19f1f01 --- /dev/null +++ b/.lintstagedrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + "./src/**/*.{tsx,ts}": "pnpm lint", + "./src/**/*.{tsx,ts,html,css}": "pnpm format", +} diff --git a/README.md b/README.md index bb1c340..0007986 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# react-components \ No newline at end of file +# react-components diff --git a/index.html b/index.html new file mode 100644 index 0000000..1af0478 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + react-ts-tw-template + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..53c85b5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6402 @@ +{ + "name": "react-ts-tw-template", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "react-ts-tw-template", + "version": "0.0.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitejs/plugin-react": "^4.0.3", + "autoprefixer": "^10.4.16", + "eslint": "^8.50.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "eslint-plugin-simple-import-sort": "^10.0.0", + "husky": "^8.0.3", + "lint-staged": "^15.0.2", + "postcss": "^8.4.30", + "prettier": "^3.0.3", + "prettier-plugin-tailwindcss": "^0.5.4", + "tailwindcss": "^3.3.3", + "typescript": "^5.2.2", + "vite": "^4.4.9" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "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.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", + "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@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.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.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.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "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.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.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/@eslint/eslintrc/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "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/@types/babel__core": { + "version": "7.20.3", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", + "integrity": "sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==", + "dev": true, + "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.6", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.6.tgz", + "integrity": "sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.3.tgz", + "integrity": "sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.3.tgz", + "integrity": "sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.9", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", + "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", + "integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.14.tgz", + "integrity": "sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz", + "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.0.tgz", + "integrity": "sha512-lgX7F0azQwRPB7t7WAyeHWVfW1YJ9NIgd9mvGhfQpRY56X6AVf8mwM8Wol+0z4liE7XX3QOt8MN1rUKCfSjRIA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/type-utils": "6.9.0", + "@typescript-eslint/utils": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^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.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.0.tgz", + "integrity": "sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", + "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.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.0.tgz", + "integrity": "sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0" + }, + "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.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.0.tgz", + "integrity": "sha512-XXeahmfbpuhVbhSOROIzJ+b13krFmgtc4GlEuu1WBT+RpyGPIA4Y/eGnXzjbDj5gZLzpAXO/sj+IF/x2GtTMjQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/utils": "6.9.0", + "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.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.0.tgz", + "integrity": "sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw==", + "dev": true, + "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.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.0.tgz", + "integrity": "sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", + "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.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.0.tgz", + "integrity": "sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.0", + "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.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.0.tgz", + "integrity": "sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.9.0", + "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/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.1.0.tgz", + "integrity": "sha512-rM0SqazU9iqPUraQ2JlIvReeaxOoRj6n+PzB1C0cBzIbd8qP336nC39/R9yPi3wVcah7E7j/kdU1uCUqMEU4OQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.22.20", + "@babel/plugin-transform-react-jsx-self": "^7.22.5", + "@babel/plugin-transform-react-jsx-source": "^7.22.5", + "@types/babel__core": "^7.20.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "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/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "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.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "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.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "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", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", + "dev": true + }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.2.tgz", + "integrity": "sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001553", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz", + "integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==", + "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" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "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/default-browser/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/default-browser/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.565", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.565.tgz", + "integrity": "sha512-XbMoT6yIvg2xzcbs5hCADi0dXBh4//En3oFXmtPX+jiyyiCTiM9DGFT2SLottjpEs9Z8Mh8SqahbR96MaHfuSg==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "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.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "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.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dev": true, + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "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.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "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", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-airbnb-typescript": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz", + "integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==", + "dev": true, + "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": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", + "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.20.7", + "aria-query": "^5.1.3", + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.6.2", + "axobject-query": "^3.1.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.3", + "language-tags": "=1.0.5", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", + "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", + "dev": true, + "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.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.3.tgz", + "integrity": "sha512-Hh0wv8bUNY877+sI0BlCUlsS0TYYQqvzEwJsJJPM2WF4RnTStSnSR3zdJYa2nPOJgg3UghXi54lVyMSmpCalzA==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.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", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz", + "integrity": "sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==", + "dev": true, + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "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/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "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-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "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-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "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-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", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/jiti": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", + "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "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/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "dev": true, + "dependencies": { + "language-subtag-registry": "~0.3.2" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lint-staged": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.0.2.tgz", + "integrity": "sha512-vnEy7pFTHyVuDmCAIFKR5QDO8XLVlPFQQyujQ/STOxe40ICWqJ6knS2wSJ/ffX/Lw0rz83luRDh+ET7toN+rOw==", + "dev": true, + "dependencies": { + "chalk": "5.3.0", + "commander": "11.1.0", + "debug": "4.3.4", + "execa": "8.0.1", + "lilconfig": "2.1.0", + "listr2": "7.0.2", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.3" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/listr2": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-7.0.2.tgz", + "integrity": "sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^5.0.1", + "rfdc": "^1.3.0", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-update": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", + "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^5.0.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^5.0.0", + "strip-ansi": "^7.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "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.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.hasown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.6.tgz", + "integrity": "sha512-2Xgb+GQlkPAUCFi3sV+NOYcSI5XgduvDBL2Zt/hwJudeKXkyvRS65c38SB0yb9UB40+1rL83I6m0RtlOQ8eHdg==", + "dev": true, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@shufo/prettier-plugin-blade": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@shufo/prettier-plugin-blade": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "prettier-plugin-twig-melody": { + "optional": true + } + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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" + } + ] + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "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/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.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-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "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/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/run-applescript/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-applescript/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-applescript/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/run-applescript/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "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", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "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/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "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/tailwindcss": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", + "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "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.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "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/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "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" + } + ], + "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", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "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-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "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.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "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": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz", + "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6e4662d --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "react-ts-tw-template", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "type-check": "tsc --noEmit", + "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", + "prepare": "husky install", + "precommit": "pnpm lint-staged && pnpm type-check" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitejs/plugin-react": "^4.0.3", + "autoprefixer": "^10.4.16", + "eslint": "^8.50.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "eslint-plugin-simple-import-sort": "^10.0.0", + "husky": "^8.0.3", + "lint-staged": "^15.0.2", + "postcss": "^8.4.30", + "prettier": "^3.0.3", + "prettier-plugin-tailwindcss": "^0.5.4", + "tailwindcss": "^3.3.3", + "typescript": "^5.2.2", + "vite": "^4.4.9" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/prettier.config.cjs b/prettier.config.cjs new file mode 100644 index 0000000..bfc7591 --- /dev/null +++ b/prettier.config.cjs @@ -0,0 +1,10 @@ +module.exports = { + tabWidth: 2, + singleQuote: true, + arrowParens: 'always', + trailingComma: 'all', + bracketSpacing: true, + bracketSameLine: true, + editorconfig: true, + plugins: ['prettier-plugin-tailwindcss'], +}; diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..8cd69c0 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +class App extends React.Component { + render() { + return
Hello React!
; + } +} + +export default App; diff --git a/src/assets/.gitkeep b/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..cc44d49 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import ReactDOM from 'react-dom/client'; + +import App from './App.tsx'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); 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/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..ae7006b --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,18 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + transitionTimingFunction: { + bounce: 'cubic-bezier(.25,1.55,.65,.97)', + }, + height: { + screen: '100dvh', + }, + }, + fontFamily: { + sans: 'Roboto Mono, monospace', + }, + }, + plugins: [], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a7fc6fb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..fabde1a --- /dev/null +++ b/vite.config.js @@ -0,0 +1,6 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5a33944 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From b8703d4d00f72be9854ec4f4e5d427a608de5b3e Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 24 Oct 2023 15:26:34 +0300 Subject: [PATCH 003/144] chore: remove unused config file --- vite.config.js | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 vite.config.js diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index fabde1a..0000000 --- a/vite.config.js +++ /dev/null @@ -1,6 +0,0 @@ -import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [react()], -}); From 923d4ad31ba351c07c5473b0578500a10a3e32c1 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 25 Oct 2023 13:37:32 +0300 Subject: [PATCH 004/144] feat: create folder structure --- src/{ => app}/App.tsx | 4 ++-- src/main.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/{ => app}/App.tsx (63%) diff --git a/src/App.tsx b/src/app/App.tsx similarity index 63% rename from src/App.tsx rename to src/app/App.tsx index 8cd69c0..7b68e7c 100644 --- a/src/App.tsx +++ b/src/app/App.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import { Component } from 'react'; -class App extends React.Component { +class App extends Component { render() { return
Hello React!
; } diff --git a/src/main.tsx b/src/main.tsx index cc44d49..3286290 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,8 +2,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App.tsx'; import './index.css'; +import App from './app/App.tsx'; ReactDOM.createRoot(document.getElementById('root')!).render( From 7f69287ec9fba8d1107f8a9c4c234426fccc9694 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 25 Oct 2023 13:57:23 +0300 Subject: [PATCH 005/144] chore: change eslint rule --- .eslintrc.cjs | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a8fcbdb..0517778 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -24,6 +24,7 @@ module.exports = { sourceType: 'module', }, rules: { + 'react/state-in-constructor': ['error', 'never'], 'react/prefer-stateless-function': 0, 'no-void': 0, 'react/react-in-jsx-scope': 0, From 12ba3623755b574267eb7f4fe66620e9e0221472 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 25 Oct 2023 17:24:44 +0300 Subject: [PATCH 006/144] chore: change default font --- index.html | 7 ++++--- tailwind.config.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 1af0478..ec4c7e5 100644 --- a/index.html +++ b/index.html @@ -7,10 +7,11 @@ href="data:image/svg+xml,⚛️" /> - - + + + - react-ts-tw-template + React components
diff --git a/tailwind.config.js b/tailwind.config.js index ae7006b..ccc3a9a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -11,7 +11,7 @@ export default { }, }, fontFamily: { - sans: 'Roboto Mono, monospace', + sans: 'Poppins, monospace, sans-serif', }, }, plugins: [], From 94aa46259158ba829fda1b3870ed0cb23246a8fb Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 25 Oct 2023 17:25:22 +0300 Subject: [PATCH 007/144] feat: add initial design --- src/app/App.tsx | 4 +++- src/features/Search/Search.tsx | 15 +++++++++++++++ src/features/Search/ui/Input.tsx | 27 +++++++++++++++++++++++++++ src/index.css | 11 +++++++++++ src/pages/MainLayout.tsx | 27 +++++++++++++++++++++++++++ src/widgets/Header/Header.tsx | 15 +++++++++++++++ src/widgets/Main/Main.tsx | 9 +++++++++ 7 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/features/Search/Search.tsx create mode 100644 src/features/Search/ui/Input.tsx create mode 100644 src/pages/MainLayout.tsx create mode 100644 src/widgets/Header/Header.tsx create mode 100644 src/widgets/Main/Main.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index 7b68e7c..b82365d 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,8 +1,10 @@ import { Component } from 'react'; +import MainLayout from '../pages/MainLayout.tsx'; + class App extends Component { render() { - return
Hello React!
; + return ; } } diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx new file mode 100644 index 0000000..7feceed --- /dev/null +++ b/src/features/Search/Search.tsx @@ -0,0 +1,15 @@ +import { Component } from 'react'; + +import Input from './ui/Input.tsx'; + +class Search extends Component { + render() { + return ( +
+ +
+ ); + } +} + +export default Search; diff --git a/src/features/Search/ui/Input.tsx b/src/features/Search/ui/Input.tsx new file mode 100644 index 0000000..7b3b3d9 --- /dev/null +++ b/src/features/Search/ui/Input.tsx @@ -0,0 +1,27 @@ +import { Component } from 'react'; + +interface IInputState { + val: string; +} + +class Input extends Component { + state = { + val: '', + }; + + render() { + const { val } = this.state; + + return ( + this.setState({ val: e.target.value })} + /> + ); + } +} + +export default Input; diff --git a/src/index.css b/src/index.css index b5c61c9..ee2000b 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,14 @@ @tailwind base; @tailwind components; @tailwind utilities; + +* { + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; +} + +body { + background: theme(colors.black); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx new file mode 100644 index 0000000..083f030 --- /dev/null +++ b/src/pages/MainLayout.tsx @@ -0,0 +1,27 @@ +import { Component } from 'react'; + +import Header from '../widgets/Header/Header.tsx'; +import Main from '../widgets/Main/Main.tsx'; + +class MainLayout extends Component { + render() { + return ( + <> +
+ +
+
+
+
+
+
+
+
+
+
+ + ); + } +} + +export default MainLayout; diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx new file mode 100644 index 0000000..12a0e58 --- /dev/null +++ b/src/widgets/Header/Header.tsx @@ -0,0 +1,15 @@ +import { Component } from 'react'; + +import Search from '../../features/Search/Search.tsx'; + +class Header extends Component { + render() { + return ( +
+ +
+ ); + } +} + +export default Header; diff --git a/src/widgets/Main/Main.tsx b/src/widgets/Main/Main.tsx new file mode 100644 index 0000000..5e4e436 --- /dev/null +++ b/src/widgets/Main/Main.tsx @@ -0,0 +1,9 @@ +import { Component } from 'react'; + +class Main extends Component { + render() { + return
Main
; + } +} + +export default Main; From 3f5db179d20c94d5ac813d140e796f544e022735 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 25 Oct 2023 17:38:49 +0300 Subject: [PATCH 008/144] feat: add search input ui --- src/features/Search/Search.tsx | 2 +- src/features/Search/ui/Input.tsx | 2 +- src/pages/MainLayout.tsx | 5 ++++- src/shared/types/interfaces.ts | 5 +++++ src/widgets/Header/Header.tsx | 10 +++++++--- 5 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 src/shared/types/interfaces.ts diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 7feceed..47f2cc5 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -5,7 +5,7 @@ import Input from './ui/Input.tsx'; class Search extends Component { render() { return ( -
+
); diff --git a/src/features/Search/ui/Input.tsx b/src/features/Search/ui/Input.tsx index 7b3b3d9..5d9396f 100644 --- a/src/features/Search/ui/Input.tsx +++ b/src/features/Search/ui/Input.tsx @@ -14,7 +14,7 @@ class Input extends Component { return (
-
+
+ +
diff --git a/src/shared/types/interfaces.ts b/src/shared/types/interfaces.ts new file mode 100644 index 0000000..65bcf34 --- /dev/null +++ b/src/shared/types/interfaces.ts @@ -0,0 +1,5 @@ +import { ReactNode } from 'react'; + +export interface IChildren { + children: ReactNode; +} diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 12a0e58..01f9c6d 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -1,12 +1,16 @@ import { Component } from 'react'; -import Search from '../../features/Search/Search.tsx'; +import { IChildren } from '../../shared/types/interfaces.ts'; -class Header extends Component { +interface IHeaderProps extends IChildren {} + +class Header extends Component { render() { + const { children } = this.props; + return (
- + {children}
); } From 426ed6a901a3010f38cb87c835bf9cf2b9496bb7 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 25 Oct 2023 17:45:04 +0300 Subject: [PATCH 009/144] refactor: encapsulate background into separate component --- src/pages/MainLayout.tsx | 23 +++++++++-------------- src/pages/ui/GradientBackground.tsx | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 src/pages/ui/GradientBackground.tsx diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index f07c1c2..7102dab 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,5 +1,6 @@ import { Component } from 'react'; +import GradientBackground from './ui/GradientBackground.tsx'; import Search from '../features/Search/Search.tsx'; import Header from '../widgets/Header/Header.tsx'; import Main from '../widgets/Main/Main.tsx'; @@ -7,22 +8,16 @@ import Main from '../widgets/Main/Main.tsx'; class MainLayout extends Component { render() { return ( - <> -
+
+ -
-
-
- -
-
-
-
-
-
-
+
+
+ +
+
- +
); } } diff --git a/src/pages/ui/GradientBackground.tsx b/src/pages/ui/GradientBackground.tsx new file mode 100644 index 0000000..3606105 --- /dev/null +++ b/src/pages/ui/GradientBackground.tsx @@ -0,0 +1,18 @@ +import { Component } from 'react'; + +class GradientBackground extends Component { + render() { + return ( + <> +
+ +
+
+
+
+ + ); + } +} + +export default GradientBackground; From 45062df0998ec7d46e911af14ffad70551a28322 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 25 Oct 2023 18:08:05 +0300 Subject: [PATCH 010/144] feat: add search input interactivity --- src/features/Search/ui/Input.tsx | 2 +- src/widgets/Header/Header.tsx | 2 +- tailwind.config.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/Search/ui/Input.tsx b/src/features/Search/ui/Input.tsx index 5d9396f..e2b6650 100644 --- a/src/features/Search/ui/Input.tsx +++ b/src/features/Search/ui/Input.tsx @@ -14,7 +14,7 @@ class Input extends Component { return ( { const { children } = this.props; return ( -
+
{children}
); diff --git a/tailwind.config.js b/tailwind.config.js index ccc3a9a..6d32d8f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,7 +4,7 @@ export default { theme: { extend: { transitionTimingFunction: { - bounce: 'cubic-bezier(.25,1.55,.65,.97)', + bounce: 'cubic-bezier(.25,1.55,.65,1.4)', }, height: { screen: '100dvh', From 90a35cf4fdadba8834f9939389d5982c5971f8da Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 25 Oct 2023 18:47:20 +0300 Subject: [PATCH 011/144] chore: adjust eslint rule --- .eslintrc.cjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0517778..6b9a46b 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -24,6 +24,8 @@ module.exports = { sourceType: 'module', }, rules: { + 'react/destructuring-assignment': 0, + 'react/require-default-props': 0, 'react/state-in-constructor': ['error', 'never'], 'react/prefer-stateless-function': 0, 'no-void': 0, From 590ddba87a5efea02c7d7fd6bf6a5df53decdc4b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 25 Oct 2023 18:49:33 +0300 Subject: [PATCH 012/144] feat: implement search feature --- src/features/ProductList/ProductList.tsx | 9 +++++++++ src/features/Search/Search.tsx | 23 ++++++++++++++++++++--- src/features/Search/ui/Button.tsx | 20 ++++++++++++++++++++ src/features/Search/ui/Input.tsx | 24 +++++++++++++++++------- src/pages/MainLayout.tsx | 10 +++++----- src/widgets/Header/Header.tsx | 10 +++------- src/widgets/Main/Main.tsx | 8 ++++++-- 7 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 src/features/ProductList/ProductList.tsx create mode 100644 src/features/Search/ui/Button.tsx diff --git a/src/features/ProductList/ProductList.tsx b/src/features/ProductList/ProductList.tsx new file mode 100644 index 0000000..3d2ffa6 --- /dev/null +++ b/src/features/ProductList/ProductList.tsx @@ -0,0 +1,9 @@ +import { Component } from 'react'; + +class ProductList extends Component { + render() { + return
ProductList
; + } +} + +export default ProductList; diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 47f2cc5..400d4cd 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -1,12 +1,29 @@ import { Component } from 'react'; +import Button from './ui/Button.tsx'; import Input from './ui/Input.tsx'; -class Search extends Component { +interface ISearchState { + searchQuery: string; +} + +class Search extends Component { + state = { + searchQuery: '', + }; + + // handleSearch = () => { + // console.log(this.state.searchQuery); + // }; + render() { return ( -
- +
+ this.setState({ searchQuery: newVal })} + /> +
); } diff --git a/src/features/Search/ui/Button.tsx b/src/features/Search/ui/Button.tsx new file mode 100644 index 0000000..7c012f7 --- /dev/null +++ b/src/features/Search/ui/Button.tsx @@ -0,0 +1,20 @@ +import { Component } from 'react'; + +interface ISearchBtnProps { + onClick?: () => void; +} + +class Button extends Component { + render() { + return ( + + ); + } +} + +export default Button; diff --git a/src/features/Search/ui/Input.tsx b/src/features/Search/ui/Input.tsx index e2b6650..fe6a127 100644 --- a/src/features/Search/ui/Input.tsx +++ b/src/features/Search/ui/Input.tsx @@ -1,24 +1,34 @@ -import { Component } from 'react'; +import { Component, SyntheticEvent } from 'react'; interface IInputState { val: string; } -class Input extends Component { +interface IInputProps { + value?: string; + onChange?: (newVal: string) => void; +} + +class Input extends Component { state = { - val: '', + val: this.props.value ?? '', }; - render() { - const { val } = this.state; + handleChange = (e: SyntheticEvent) => { + const { value } = e.target as HTMLInputElement; + this.props.onChange?.(value); + this.setState({ val: value }); + }; + + render() { return ( this.setState({ val: e.target.value })} + value={this.state.val} + onChange={this.handleChange} /> ); } diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 7102dab..c185acd 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,7 +1,7 @@ import { Component } from 'react'; import GradientBackground from './ui/GradientBackground.tsx'; -import Search from '../features/Search/Search.tsx'; +import ProductList from '../features/ProductList/ProductList.tsx'; import Header from '../widgets/Header/Header.tsx'; import Main from '../widgets/Main/Main.tsx'; @@ -12,10 +12,10 @@ class MainLayout extends Component {
-
- -
-
+
+
+ +
); diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index faca9f9..62a4699 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -1,16 +1,12 @@ import { Component } from 'react'; -import { IChildren } from '../../shared/types/interfaces.ts'; +import Search from '../../features/Search/Search.tsx'; -interface IHeaderProps extends IChildren {} - -class Header extends Component { +class Header extends Component { render() { - const { children } = this.props; - return (
- {children} +
); } diff --git a/src/widgets/Main/Main.tsx b/src/widgets/Main/Main.tsx index 5e4e436..4e3e15b 100644 --- a/src/widgets/Main/Main.tsx +++ b/src/widgets/Main/Main.tsx @@ -1,8 +1,12 @@ import { Component } from 'react'; -class Main extends Component { +import { IChildren } from '../../shared/types/interfaces.ts'; + +class Main extends Component { render() { - return
Main
; + const { children } = this.props; + + return
{children}
; } } From a0c5dfd61b68af1d85a0994b178fb1342d77ba33 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 25 Oct 2023 23:11:54 +0300 Subject: [PATCH 013/144] refactor: change search interaction ui --- src/features/Search/Search.tsx | 2 +- src/features/Search/ui/Button.tsx | 2 +- src/features/Search/ui/Input.tsx | 2 +- src/index.css | 18 ++++++++++-------- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 400d4cd..88412ca 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -18,7 +18,7 @@ class Search extends Component { render() { return ( -
+
this.setState({ searchQuery: newVal })} diff --git a/src/features/Search/ui/Button.tsx b/src/features/Search/ui/Button.tsx index 7c012f7..0a6c801 100644 --- a/src/features/Search/ui/Button.tsx +++ b/src/features/Search/ui/Button.tsx @@ -9,7 +9,7 @@ class Button extends Component { return ( diff --git a/src/features/Search/ui/Input.tsx b/src/features/Search/ui/Input.tsx index fe6a127..944147a 100644 --- a/src/features/Search/ui/Input.tsx +++ b/src/features/Search/ui/Input.tsx @@ -24,7 +24,7 @@ class Input extends Component { render() { return ( Date: Thu, 26 Oct 2023 15:36:05 +0300 Subject: [PATCH 014/144] refactor: change background --- src/index.css | 2 +- src/pages/ui/GradientBackground.tsx | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/index.css b/src/index.css index 4772618..104f876 100644 --- a/src/index.css +++ b/src/index.css @@ -9,7 +9,7 @@ } body { - background: theme(colors.black); + background: theme(colors.neutral.950); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/src/pages/ui/GradientBackground.tsx b/src/pages/ui/GradientBackground.tsx index 3606105..6f4d5c5 100644 --- a/src/pages/ui/GradientBackground.tsx +++ b/src/pages/ui/GradientBackground.tsx @@ -4,11 +4,12 @@ class GradientBackground extends Component { render() { return ( <> -
+
-
-
-
+
+
+
+
); From f0cc2c0c7cebc4e724a2fadcf0a339829f51cadf Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 26 Oct 2023 17:15:46 +0300 Subject: [PATCH 015/144] feat: add product card ui --- src/entities/product/ui/ProductCard.tsx | 26 ++++++++++++++++++++++++ src/features/ProductList/ProductList.tsx | 15 +++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/entities/product/ui/ProductCard.tsx diff --git a/src/entities/product/ui/ProductCard.tsx b/src/entities/product/ui/ProductCard.tsx new file mode 100644 index 0000000..7b0174d --- /dev/null +++ b/src/entities/product/ui/ProductCard.tsx @@ -0,0 +1,26 @@ +import { Component } from 'react'; + +class ProductCard extends Component { + render() { + return ( +
  • + The poster of Shazam! Fury of the Gods film +
    +

    Shazam! Fury of the Gods

    +

    + American superhero film based on the DC character Shazam. Produced + by New Line Cinema, DC Studios, and the Safran Company, and + distributed by Warner Bros. +

    +

    Action/Adventure

    +
    +
  • + ); + } +} + +export default ProductCard; diff --git a/src/features/ProductList/ProductList.tsx b/src/features/ProductList/ProductList.tsx index 3d2ffa6..0e7aba5 100644 --- a/src/features/ProductList/ProductList.tsx +++ b/src/features/ProductList/ProductList.tsx @@ -1,8 +1,21 @@ import { Component } from 'react'; +import ProductCard from '../../entities/product/ui/ProductCard.tsx'; + class ProductList extends Component { render() { - return
    ProductList
    ; + return ( +
      + + + + + + + + +
    + ); } } From be56fc9bcbe432a3b921c10d81ce2eaf4e131dcf Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 26 Oct 2023 17:16:27 +0300 Subject: [PATCH 016/144] fix: layout --- src/pages/MainLayout.tsx | 4 ++-- src/pages/ui/GradientBackground.tsx | 2 +- src/widgets/Header/Header.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index c185acd..9338d3d 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -8,10 +8,10 @@ import Main from '../widgets/Main/Main.tsx'; class MainLayout extends Component { render() { return ( -
    +
    -
    +
    diff --git a/src/pages/ui/GradientBackground.tsx b/src/pages/ui/GradientBackground.tsx index 6f4d5c5..1c5ef00 100644 --- a/src/pages/ui/GradientBackground.tsx +++ b/src/pages/ui/GradientBackground.tsx @@ -10,7 +10,7 @@ class GradientBackground extends Component {
    -
    +
    ); } diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 62a4699..6cd90f4 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -5,7 +5,7 @@ import Search from '../../features/Search/Search.tsx'; class Header extends Component { render() { return ( -
    +
    ); From d094bb4d41c37726c292ba457e311fc2f9533d59 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 26 Oct 2023 19:02:53 +0300 Subject: [PATCH 017/144] feat: implement getting movie list --- src/app/App.tsx | 7 ++- src/entities/movie/api/apiMovie.ts | 16 ++++++ src/entities/movie/ui/Movie.tsx | 32 ++++++++++++ src/entities/product/ui/ProductCard.tsx | 26 ---------- src/features/ProductList/MovieList.tsx | 34 ++++++++++++ src/features/ProductList/ProductList.tsx | 22 -------- src/features/Search/Search.tsx | 19 +++++-- .../Search/context/SearchProvider.tsx | 52 +++++++++++++++++++ src/pages/MainLayout.tsx | 4 +- src/shared/const/const.ts | 3 ++ src/shared/types/types.ts | 20 +++++++ 11 files changed, 180 insertions(+), 55 deletions(-) create mode 100644 src/entities/movie/api/apiMovie.ts create mode 100644 src/entities/movie/ui/Movie.tsx delete mode 100644 src/entities/product/ui/ProductCard.tsx create mode 100644 src/features/ProductList/MovieList.tsx delete mode 100644 src/features/ProductList/ProductList.tsx create mode 100644 src/features/Search/context/SearchProvider.tsx create mode 100644 src/shared/const/const.ts create mode 100644 src/shared/types/types.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index b82365d..4bf85a4 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,10 +1,15 @@ import { Component } from 'react'; +import SearchProvider from '../features/Search/context/SearchProvider.tsx'; import MainLayout from '../pages/MainLayout.tsx'; class App extends Component { render() { - return ; + return ( + + + + ); } } diff --git a/src/entities/movie/api/apiMovie.ts b/src/entities/movie/api/apiMovie.ts new file mode 100644 index 0000000..fe11da2 --- /dev/null +++ b/src/entities/movie/api/apiMovie.ts @@ -0,0 +1,16 @@ +import { API_URL, QUERY_FALLBACK } from '../../../shared/const/const.ts'; +import { ApiErrorResponse, ApiResponse } from '../../../shared/types/types.ts'; + +async function getMovieList(query: string): Promise { + const response = await fetch(`${API_URL}&s=${query || QUERY_FALLBACK}`); + + if (!response.ok) throw new Error('Something went wrong fetching movies!'); + + const data = (await response.json()) as ApiResponse | ApiErrorResponse; + + if (data.Response === 'False') throw new Error(data.Error); + + return data; +} + +export default getMovieList; diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx new file mode 100644 index 0000000..73ad6a9 --- /dev/null +++ b/src/entities/movie/ui/Movie.tsx @@ -0,0 +1,32 @@ +import { Component } from 'react'; + +import { Movie as MovieData } from '../../../shared/types/types.ts'; + +interface IProductCardProps { + data: MovieData; +} + +class Movie extends Component { + componentDidMount() {} + + render() { + const { Poster, Title, Type, Year } = this.props.data; + + return ( +
  • + {`The +
    +

    {Title}

    +

    {Year}

    +

    {Type}

    +
    +
  • + ); + } +} + +export default Movie; diff --git a/src/entities/product/ui/ProductCard.tsx b/src/entities/product/ui/ProductCard.tsx deleted file mode 100644 index 7b0174d..0000000 --- a/src/entities/product/ui/ProductCard.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Component } from 'react'; - -class ProductCard extends Component { - render() { - return ( -
  • - The poster of Shazam! Fury of the Gods film -
    -

    Shazam! Fury of the Gods

    -

    - American superhero film based on the DC character Shazam. Produced - by New Line Cinema, DC Studios, and the Safran Company, and - distributed by Warner Bros. -

    -

    Action/Adventure

    -
    -
  • - ); - } -} - -export default ProductCard; diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx new file mode 100644 index 0000000..8a2fc36 --- /dev/null +++ b/src/features/ProductList/MovieList.tsx @@ -0,0 +1,34 @@ +import { Component } from 'react'; + +import Movie from '../../entities/movie/ui/Movie.tsx'; +import { MovieList as MovieListData } from '../../shared/types/types.ts'; +import { + ISearchContext, + SearchContext, +} from '../Search/context/SearchProvider.tsx'; + +interface IProductListState { + movies: MovieListData | null; +} + +class MovieList extends Component { + static contextType = SearchContext; + + declare context: ISearchContext; + + render() { + const { movies } = this.context; + + if (!movies) return null; + + return ( +
      + {movies.map((movie) => ( + + ))} +
    + ); + } +} + +export default MovieList; diff --git a/src/features/ProductList/ProductList.tsx b/src/features/ProductList/ProductList.tsx deleted file mode 100644 index 0e7aba5..0000000 --- a/src/features/ProductList/ProductList.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Component } from 'react'; - -import ProductCard from '../../entities/product/ui/ProductCard.tsx'; - -class ProductList extends Component { - render() { - return ( -
      - - - - - - - - -
    - ); - } -} - -export default ProductList; diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 88412ca..5c35f56 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -1,20 +1,31 @@ import { Component } from 'react'; +import { ISearchContext, SearchContext } from './context/SearchProvider.tsx'; import Button from './ui/Button.tsx'; import Input from './ui/Input.tsx'; +import getMovieList from '../../entities/movie/api/apiMovie.ts'; interface ISearchState { searchQuery: string; } class Search extends Component { + static contextType = SearchContext; + + declare context: ISearchContext; + state = { searchQuery: '', }; - // handleSearch = () => { - // console.log(this.state.searchQuery); - // }; + componentDidMount() { + void this.handleSearch(); + } + + handleSearch = async () => { + const res = await getMovieList(this.state.searchQuery); + this.context.updateMovies(res.Search); + }; render() { return ( @@ -23,7 +34,7 @@ class Search extends Component { value={this.state.searchQuery} onChange={(newVal) => this.setState({ searchQuery: newVal })} /> -
    ); } diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx new file mode 100644 index 0000000..423a646 --- /dev/null +++ b/src/features/Search/context/SearchProvider.tsx @@ -0,0 +1,52 @@ +import { Component, createContext } from 'react'; + +import { IChildren } from '../../../shared/types/interfaces.ts'; +import { MovieList } from '../../../shared/types/types.ts'; + +interface ISearchProviderState { + query: string; + movies: MovieList | null; +} + +export interface ISearchContext extends ISearchProviderState { + updateQuery: (newQuery: string) => void; + updateMovies: (newMovies: MovieList) => void; +} + +export const SearchContext = createContext({ + query: '', +} as ISearchContext); + +class SearchProvider extends Component { + state = { + query: '', + movies: null, + }; + + updateQuery = (newQuery: string) => { + this.setState({ query: newQuery }); + }; + + updateMovies = async (newMovies: MovieList) => { + this.setState({ movies: newMovies }); + }; + + render() { + const { query, movies } = this.state; + const { updateQuery, updateMovies } = this; + + return ( + + {this.props.children} + + ); + } +} + +export default SearchProvider; diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 9338d3d..3f19143 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,7 +1,7 @@ import { Component } from 'react'; import GradientBackground from './ui/GradientBackground.tsx'; -import ProductList from '../features/ProductList/ProductList.tsx'; +import MovieList from '../features/ProductList/MovieList.tsx'; import Header from '../widgets/Header/Header.tsx'; import Main from '../widgets/Main/Main.tsx'; @@ -14,7 +14,7 @@ class MainLayout extends Component {
    - +
    diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts new file mode 100644 index 0000000..f224d1a --- /dev/null +++ b/src/shared/const/const.ts @@ -0,0 +1,3 @@ +export const API_KEY = 'dbb72d83'; +export const API_URL = `https://www.omdbapi.com/?apikey=${API_KEY}`; +export const QUERY_FALLBACK = 'avatar'; diff --git a/src/shared/types/types.ts b/src/shared/types/types.ts new file mode 100644 index 0000000..97310d4 --- /dev/null +++ b/src/shared/types/types.ts @@ -0,0 +1,20 @@ +export type Movie = Readonly<{ + Poster: string; + Title: string; + Type: string; + Year: string; + imdbID: string; +}>; + +export type MovieList = Readonly>; + +export type ApiResponse = Readonly<{ + Response: 'True'; + Search: MovieList; + totalResults: string; +}>; + +export type ApiErrorResponse = Readonly<{ + Error: 'Incorrect IMDb ID.'; + Response: 'False'; +}>; From 20d774d9e4b0543c2282a59dd7a89c83299475d0 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 26 Oct 2023 19:03:48 +0300 Subject: [PATCH 018/144] chore: disable eslint rule --- .eslintrc.cjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6b9a46b..6a57cab 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -24,6 +24,9 @@ module.exports = { sourceType: 'module', }, rules: { + // TODO: remove 👇 after 1 module + 'react/jsx-no-constructed-context-values': 0, + 'react/static-property-placement': 0, 'react/destructuring-assignment': 0, 'react/require-default-props': 0, 'react/state-in-constructor': ['error', 'never'], From 49361d95304fd69286dd7cb4fab0ee8711cab7f8 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 26 Oct 2023 19:50:20 +0300 Subject: [PATCH 019/144] feat: add fetching movie details --- src/entities/movie/api/apiMovie.ts | 26 ++++++++++++++++--- src/entities/movie/ui/Movie.tsx | 36 +++++++++++++++++++++----- src/features/ProductList/MovieList.tsx | 2 ++ src/features/Search/Search.tsx | 2 +- src/shared/const/const.ts | 2 +- src/shared/types/types.ts | 35 ++++++++++++++++++++++++- tsconfig.json | 4 +-- 7 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/entities/movie/api/apiMovie.ts b/src/entities/movie/api/apiMovie.ts index fe11da2..8645c90 100644 --- a/src/entities/movie/api/apiMovie.ts +++ b/src/entities/movie/api/apiMovie.ts @@ -1,16 +1,34 @@ import { API_URL, QUERY_FALLBACK } from '../../../shared/const/const.ts'; -import { ApiErrorResponse, ApiResponse } from '../../../shared/types/types.ts'; +import { + ApiErrorResponse, + ApiMovieListResponse, + ApiMovieResponse, +} from '../../../shared/types/types.ts'; -async function getMovieList(query: string): Promise { +export async function getMovieList( + query: string, +): Promise { const response = await fetch(`${API_URL}&s=${query || QUERY_FALLBACK}`); if (!response.ok) throw new Error('Something went wrong fetching movies!'); - const data = (await response.json()) as ApiResponse | ApiErrorResponse; + const data = (await response.json()) as + | ApiMovieListResponse + | ApiErrorResponse; if (data.Response === 'False') throw new Error(data.Error); return data; } -export default getMovieList; +export async function getMovie(id: string) { + const response = await fetch(`${API_URL}&i=${id}`); + + if (!response.ok) throw new Error('Something went wrong fetching movies!'); + + const data = (await response.json()) as ApiMovieResponse | ApiErrorResponse; + + if (data.Response === 'False') throw new Error(data.Error); + + return data; +} diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 73ad6a9..000fff3 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,19 +1,40 @@ import { Component } from 'react'; import { Movie as MovieData } from '../../../shared/types/types.ts'; +import { getMovie } from '../api/apiMovie.ts'; interface IProductCardProps { data: MovieData; } -class Movie extends Component { - componentDidMount() {} +interface IProductCardState { + description: string; + genre: string; +} + +class Movie extends Component { + state = { + description: '', + genre: '', + }; + + async componentDidMount() { + const movieData = await getMovie(this.props.data.imdbID); + const genre = movieData.Genre.split(', ').slice(0, 2).join('/'); + const description = `${movieData.Plot.slice(0, 70)}...`; + + this.setState({ + description, + genre, + }); + } render() { - const { Poster, Title, Type, Year } = this.props.data; + const { Poster, Title, Year } = this.props.data; + const { description, genre } = this.state; return ( -
  • +
  • { />

    {Title}

    -

    {Year}

    -

    {Type}

    +

    {description}

    +
    + {genre} + {Year} +
  • ); diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index 8a2fc36..9832c08 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -21,6 +21,8 @@ class MovieList extends Component { if (!movies) return null; + // TODO: add loading spinner + return (
      {movies.map((movie) => ( diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 5c35f56..98d6601 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -3,7 +3,7 @@ import { Component } from 'react'; import { ISearchContext, SearchContext } from './context/SearchProvider.tsx'; import Button from './ui/Button.tsx'; import Input from './ui/Input.tsx'; -import getMovieList from '../../entities/movie/api/apiMovie.ts'; +import { getMovieList } from '../../entities/movie/api/apiMovie.ts'; interface ISearchState { searchQuery: string; diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts index f224d1a..06da1d6 100644 --- a/src/shared/const/const.ts +++ b/src/shared/const/const.ts @@ -1,3 +1,3 @@ export const API_KEY = 'dbb72d83'; export const API_URL = `https://www.omdbapi.com/?apikey=${API_KEY}`; -export const QUERY_FALLBACK = 'avatar'; +export const QUERY_FALLBACK = 'shazam'; diff --git a/src/shared/types/types.ts b/src/shared/types/types.ts index 97310d4..837e8b2 100644 --- a/src/shared/types/types.ts +++ b/src/shared/types/types.ts @@ -8,7 +8,7 @@ export type Movie = Readonly<{ export type MovieList = Readonly>; -export type ApiResponse = Readonly<{ +export type ApiMovieListResponse = Readonly<{ Response: 'True'; Search: MovieList; totalResults: string; @@ -18,3 +18,36 @@ export type ApiErrorResponse = Readonly<{ Error: 'Incorrect IMDb ID.'; Response: 'False'; }>; + +export type ApiRating = Readonly<{ + Source: string; + Value: string; +}>; + +export type ApiMovieResponse = Readonly<{ + Actors: string; + Awards: string; + BoxOffice: string; + Country: string; + DVD: string; + Director: string; + Genre: string; + Language: string; + Metascore: string; + Plot: string; + Poster: string; + Production: string; + Rated: string; + Ratings: ApiRating[]; + Released: string; + Response: 'True'; + Runtime: string; + Title: string; + Type: string; + Website: string; + Writer: string; + Year: string; + imdbID: string; + imdbRating: string; + imdbVotes: string; +}>; diff --git a/tsconfig.json b/tsconfig.json index a7fc6fb..b4e1e07 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ESNext", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, From 84679c9d4ec18e944987c74ea21dae9d66bd7191 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 26 Oct 2023 19:51:25 +0300 Subject: [PATCH 020/144] chore: change dev script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e4662d..8b84163 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", From 8b4ff55af903005341b517cb0a85ff0029903b44 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 26 Oct 2023 22:10:53 +0300 Subject: [PATCH 021/144] fix: add movie fallback image --- src/assets/.gitkeep | 0 src/assets/reactJS-logo.png | Bin 0 -> 104634 bytes src/entities/movie/ui/Movie.tsx | 13 +++++++++---- src/features/ProductList/MovieList.tsx | 2 +- src/shared/const/const.ts | 1 + 5 files changed, 11 insertions(+), 5 deletions(-) delete mode 100644 src/assets/.gitkeep create mode 100644 src/assets/reactJS-logo.png diff --git a/src/assets/.gitkeep b/src/assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/assets/reactJS-logo.png b/src/assets/reactJS-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..32b0d3403af22f8d23abb68bf21174798fdcfaf8 GIT binary patch literal 104634 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%0D}2ghv6M}=T4;#Z#LjmY+5}l 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 { const { Poster, Title, Year } = this.props.data; const { description, genre } = this.state; + const isPosterExist = Poster !== NOT_EXIST; + const poster = isPosterExist ? Poster : ReactLogo; + return (
    • {`The -
      +

      {Title}

      -

      {description}

      +

      {description}

      {genre} {Year} diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index 9832c08..0a1c994 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -24,7 +24,7 @@ class MovieList extends Component { // TODO: add loading spinner return ( -
        +
          {movies.map((movie) => ( ))} diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts index 06da1d6..e964bb9 100644 --- a/src/shared/const/const.ts +++ b/src/shared/const/const.ts @@ -1,3 +1,4 @@ export const API_KEY = 'dbb72d83'; export const API_URL = `https://www.omdbapi.com/?apikey=${API_KEY}`; export const QUERY_FALLBACK = 'shazam'; +export const NOT_EXIST = 'N/A'; From ff93543295c0d5f84310b620e809fbe6f9cbf2c3 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 26 Oct 2023 23:30:10 +0300 Subject: [PATCH 022/144] fix: layout issues --- src/entities/movie/ui/Movie.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index f36bd3e..8326bd2 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -23,7 +23,7 @@ class Movie extends Component { async componentDidMount() { const movieData = await getMovie(this.props.data.imdbID); const genre = movieData.Genre.split(', ').slice(0, 2).join('/'); - const description = `${movieData.Plot.slice(0, 70)}...`; + const description = `${movieData.Plot.slice(0, 60)}...`; this.setState({ description, @@ -39,18 +39,20 @@ class Movie extends Component { const poster = isPosterExist ? Poster : ReactLogo; return ( -
        • +
        • {`The -
          -

          {Title}

          -

          {description}

          -
          - {genre} - {Year} +
          +

          {Title}

          +
          +

          {description}

          +
          + {genre} + {Year} +
        • From 496128061542b0a98131249e1cc8be6d0b9b3c80 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 00:24:42 +0300 Subject: [PATCH 023/144] feat: add total results & ui improvements --- src/entities/movie/ui/Movie.tsx | 4 ++-- src/features/Search/Search.tsx | 1 + .../Search/context/SearchProvider.tsx | 15 +++++++++--- src/pages/MainLayout.tsx | 2 +- src/shared/types/types.ts | 2 +- src/widgets/Header/Header.tsx | 6 ++++- src/widgets/Header/ui/Logo.tsx | 13 +++++++++++ src/widgets/Header/ui/TotalResults.tsx | 23 +++++++++++++++++++ src/widgets/Main/Main.tsx | 2 +- 9 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 src/widgets/Header/ui/Logo.tsx create mode 100644 src/widgets/Header/ui/TotalResults.tsx diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 8326bd2..59f06ee 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -23,7 +23,7 @@ class Movie extends Component { async componentDidMount() { const movieData = await getMovie(this.props.data.imdbID); const genre = movieData.Genre.split(', ').slice(0, 2).join('/'); - const description = `${movieData.Plot.slice(0, 60)}...`; + const description = `${movieData.Plot.slice(0, 55)}...`; this.setState({ description, @@ -41,7 +41,7 @@ class Movie extends Component { return (
        • {`The diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 98d6601..4e56b7d 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -25,6 +25,7 @@ class Search extends Component { handleSearch = async () => { const res = await getMovieList(this.state.searchQuery); this.context.updateMovies(res.Search); + this.context.updateTotalResults(Number(res.totalResults)); }; render() { diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx index 423a646..54fd5e1 100644 --- a/src/features/Search/context/SearchProvider.tsx +++ b/src/features/Search/context/SearchProvider.tsx @@ -6,11 +6,13 @@ import { MovieList } from '../../../shared/types/types.ts'; interface ISearchProviderState { query: string; movies: MovieList | null; + totalResults: number; } export interface ISearchContext extends ISearchProviderState { updateQuery: (newQuery: string) => void; updateMovies: (newMovies: MovieList) => void; + updateTotalResults: (newTotal: number) => void; } export const SearchContext = createContext({ @@ -21,27 +23,34 @@ class SearchProvider extends Component { state = { query: '', movies: null, + totalResults: 0, }; updateQuery = (newQuery: string) => { this.setState({ query: newQuery }); }; - updateMovies = async (newMovies: MovieList) => { + updateMovies = (newMovies: MovieList) => { this.setState({ movies: newMovies }); }; + updateTotalResults = (newTotal: number) => { + this.setState({ totalResults: newTotal }); + }; + render() { - const { query, movies } = this.state; - const { updateQuery, updateMovies } = this; + const { query, movies, totalResults } = this.state; + const { updateQuery, updateMovies, updateTotalResults } = this; return ( {this.props.children} diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 3f19143..383bff6 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -11,7 +11,7 @@ class MainLayout extends Component {
          -
          +
          diff --git a/src/shared/types/types.ts b/src/shared/types/types.ts index 837e8b2..a55cc7d 100644 --- a/src/shared/types/types.ts +++ b/src/shared/types/types.ts @@ -15,7 +15,7 @@ export type ApiMovieListResponse = Readonly<{ }>; export type ApiErrorResponse = Readonly<{ - Error: 'Incorrect IMDb ID.'; + Error: string; Response: 'False'; }>; diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 6cd90f4..99efcf4 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -1,12 +1,16 @@ import { Component } from 'react'; +import Logo from './ui/Logo.tsx'; +import TotalResults from './ui/TotalResults.tsx'; import Search from '../../features/Search/Search.tsx'; class Header extends Component { render() { return ( -
          +
          + +
          ); } diff --git a/src/widgets/Header/ui/Logo.tsx b/src/widgets/Header/ui/Logo.tsx new file mode 100644 index 0000000..0876c25 --- /dev/null +++ b/src/widgets/Header/ui/Logo.tsx @@ -0,0 +1,13 @@ +import { Component } from 'react'; + +class Logo extends Component { + render() { + return ( + + CINEMACITY + + ); + } +} + +export default Logo; diff --git a/src/widgets/Header/ui/TotalResults.tsx b/src/widgets/Header/ui/TotalResults.tsx new file mode 100644 index 0000000..93a99f1 --- /dev/null +++ b/src/widgets/Header/ui/TotalResults.tsx @@ -0,0 +1,23 @@ +import { Component } from 'react'; + +import { + ISearchContext, + SearchContext, +} from '../../../features/Search/context/SearchProvider.tsx'; + +class TotalResults extends Component { + static contextType = SearchContext; + + declare context: ISearchContext; + + render() { + return ( +

          + Found {this.context.totalResults}{' '} + total results +

          + ); + } +} + +export default TotalResults; diff --git a/src/widgets/Main/Main.tsx b/src/widgets/Main/Main.tsx index 4e3e15b..bd351af 100644 --- a/src/widgets/Main/Main.tsx +++ b/src/widgets/Main/Main.tsx @@ -6,7 +6,7 @@ class Main extends Component { render() { const { children } = this.props; - return
          {children}
          ; + return
          {children}
          ; } } From e8741ce653f64ff97e5b2862314ef2eea3a1ad8d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 00:32:57 +0300 Subject: [PATCH 024/144] fix: app adaptive --- src/features/Search/Search.tsx | 2 +- src/pages/ui/GradientBackground.tsx | 6 +++--- src/widgets/Header/Header.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 4e56b7d..14f5782 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -30,7 +30,7 @@ class Search extends Component { render() { return ( -
          +
          this.setState({ searchQuery: newVal })} diff --git a/src/pages/ui/GradientBackground.tsx b/src/pages/ui/GradientBackground.tsx index 1c5ef00..134e21f 100644 --- a/src/pages/ui/GradientBackground.tsx +++ b/src/pages/ui/GradientBackground.tsx @@ -6,9 +6,9 @@ class GradientBackground extends Component { <>
          -
          -
          -
          +
          +
          +
          diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 99efcf4..279b3eb 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -7,7 +7,7 @@ import Search from '../../features/Search/Search.tsx'; class Header extends Component { render() { return ( -
          +
          From db87ed47f722d111db533557df03bc646f8b40f3 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 09:48:17 +0300 Subject: [PATCH 025/144] fix: header adaptive --- src/entities/movie/ui/Movie.tsx | 2 +- src/features/Search/ui/Button.tsx | 2 +- src/widgets/Header/Header.tsx | 2 +- src/widgets/Header/ui/Logo.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 59f06ee..2bd8da3 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -39,7 +39,7 @@ class Movie extends Component { const poster = isPosterExist ? Poster : ReactLogo; return ( -
        • +
        • { return ( diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 279b3eb..d04bf79 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -7,7 +7,7 @@ import Search from '../../features/Search/Search.tsx'; class Header extends Component { render() { return ( -
          +
          diff --git a/src/widgets/Header/ui/Logo.tsx b/src/widgets/Header/ui/Logo.tsx index 0876c25..d9ceb5b 100644 --- a/src/widgets/Header/ui/Logo.tsx +++ b/src/widgets/Header/ui/Logo.tsx @@ -3,7 +3,7 @@ import { Component } from 'react'; class Logo extends Component { render() { return ( - + CINEMACITY ); From c5af7416c30426841aa9344468fd13b24245d426 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 10:14:19 +0300 Subject: [PATCH 026/144] fix: app adaptive --- src/entities/movie/ui/Movie.tsx | 4 ++-- src/features/ProductList/MovieList.tsx | 2 +- src/pages/MainLayout.tsx | 2 +- src/widgets/Main/Main.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 2bd8da3..ad5e1b1 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -39,9 +39,9 @@ class Movie extends Component { const poster = isPosterExist ? Poster : ReactLogo; return ( -
        • +
        • {`The diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index 0a1c994..b9863b6 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -24,7 +24,7 @@ class MovieList extends Component { // TODO: add loading spinner return ( -
            +
              {movies.map((movie) => ( ))} diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 383bff6..fbc4c6b 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -11,7 +11,7 @@ class MainLayout extends Component {
              -
              +
              diff --git a/src/widgets/Main/Main.tsx b/src/widgets/Main/Main.tsx index bd351af..8013d43 100644 --- a/src/widgets/Main/Main.tsx +++ b/src/widgets/Main/Main.tsx @@ -6,7 +6,7 @@ class Main extends Component { render() { const { children } = this.props; - return
              {children}
              ; + return
              {children}
              ; } } From 2db829b1bb156d63fc5865d23156914a1e2e8069 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 10:15:59 +0300 Subject: [PATCH 027/144] refactor: rename interfaces --- src/entities/movie/ui/Movie.tsx | 6 +++--- src/features/ProductList/MovieList.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index ad5e1b1..59009ed 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -5,16 +5,16 @@ import { NOT_EXIST } from '../../../shared/const/const.ts'; import { Movie as MovieData } from '../../../shared/types/types.ts'; import { getMovie } from '../api/apiMovie.ts'; -interface IProductCardProps { +interface IMovieProps { data: MovieData; } -interface IProductCardState { +interface IMovieState { description: string; genre: string; } -class Movie extends Component { +class Movie extends Component { state = { description: '', genre: '', diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index b9863b6..4968d17 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -7,11 +7,11 @@ import { SearchContext, } from '../Search/context/SearchProvider.tsx'; -interface IProductListState { +interface IMovieListState { movies: MovieListData | null; } -class MovieList extends Component { +class MovieList extends Component { static contextType = SearchContext; declare context: ISearchContext; From 79e9db0700b1e192a7eaf2370f6cc024efcefc2f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 11:18:20 +0300 Subject: [PATCH 028/144] refactor: change movie card ui --- src/entities/movie/ui/Movie.tsx | 12 +++++------- src/features/Search/ui/Input.tsx | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 59009ed..905d0f5 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -23,7 +23,7 @@ class Movie extends Component { async componentDidMount() { const movieData = await getMovie(this.props.data.imdbID); const genre = movieData.Genre.split(', ').slice(0, 2).join('/'); - const description = `${movieData.Plot.slice(0, 55)}...`; + const description = `${movieData.Plot.slice(0, 42)}...`; this.setState({ description, @@ -39,9 +39,9 @@ class Movie extends Component { const poster = isPosterExist ? Poster : ReactLogo; return ( -
            • +
            • {`The @@ -49,10 +49,8 @@ class Movie extends Component {

              {Title}

              {description}

              -
              - {genre} - {Year} -
              + {genre} + {Year}
        • diff --git a/src/features/Search/ui/Input.tsx b/src/features/Search/ui/Input.tsx index 944147a..b5b43ab 100644 --- a/src/features/Search/ui/Input.tsx +++ b/src/features/Search/ui/Input.tsx @@ -24,7 +24,7 @@ class Input extends Component { render() { return ( Date: Fri, 27 Oct 2023 12:29:32 +0300 Subject: [PATCH 029/144] feat: add loading state --- src/features/ProductList/MovieList.tsx | 7 +- src/features/Search/Search.tsx | 2 + .../Search/context/SearchProvider.tsx | 14 +++- src/index.css | 79 +++++++++++++++++++ src/pages/MainLayout.tsx | 2 +- src/shared/ui/Spinner.tsx | 16 ++++ tailwind.config.js | 13 +++ 7 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 src/shared/ui/Spinner.tsx diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index 4968d17..366da1e 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -2,6 +2,7 @@ import { Component } from 'react'; import Movie from '../../entities/movie/ui/Movie.tsx'; import { MovieList as MovieListData } from '../../shared/types/types.ts'; +import Spinner from '../../shared/ui/Spinner.tsx'; import { ISearchContext, SearchContext, @@ -17,11 +18,9 @@ class MovieList extends Component { declare context: ISearchContext; render() { - const { movies } = this.context; + const { movies, isLoading } = this.context; - if (!movies) return null; - - // TODO: add loading spinner + if (!movies || isLoading) return ; return (
            diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 14f5782..4aa6963 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -23,9 +23,11 @@ class Search extends Component { } handleSearch = async () => { + this.context.updateIsLoading(true); const res = await getMovieList(this.state.searchQuery); this.context.updateMovies(res.Search); this.context.updateTotalResults(Number(res.totalResults)); + this.context.updateIsLoading(false); }; render() { diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx index 54fd5e1..4b2394b 100644 --- a/src/features/Search/context/SearchProvider.tsx +++ b/src/features/Search/context/SearchProvider.tsx @@ -7,12 +7,14 @@ interface ISearchProviderState { query: string; movies: MovieList | null; totalResults: number; + isLoading: boolean; } export interface ISearchContext extends ISearchProviderState { updateQuery: (newQuery: string) => void; updateMovies: (newMovies: MovieList) => void; updateTotalResults: (newTotal: number) => void; + updateIsLoading: (isLoading: boolean) => void; } export const SearchContext = createContext({ @@ -24,6 +26,7 @@ class SearchProvider extends Component { query: '', movies: null, totalResults: 0, + isLoading: false, }; updateQuery = (newQuery: string) => { @@ -38,9 +41,14 @@ class SearchProvider extends Component { this.setState({ totalResults: newTotal }); }; + updateIsLoading = (isLoading: boolean) => { + this.setState({ isLoading }); + }; + render() { - const { query, movies, totalResults } = this.state; - const { updateQuery, updateMovies, updateTotalResults } = this; + const { query, movies, totalResults, isLoading } = this.state; + const { updateQuery, updateMovies, updateTotalResults, updateIsLoading } = + this; return ( { query, movies, totalResults, + isLoading, updateQuery, updateMovies, updateTotalResults, + updateIsLoading, }}> {this.props.children} diff --git a/src/index.css b/src/index.css index 104f876..02e9473 100644 --- a/src/index.css +++ b/src/index.css @@ -13,4 +13,83 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + + .loader { + width: 10px; + height: 10px; + border-radius: 50%; + display: block; + margin: 15px auto; + position: relative; + color: #fff; + left: -100px; + box-sizing: border-box; + animation: shadowRolling 2s linear infinite; + } + + @keyframes shadowRolling { + 0% { + box-shadow: + 0px 0 rgba(255, 255, 255, 0), + 0px 0 rgba(255, 255, 255, 0), + 0px 0 rgba(255, 255, 255, 0), + 0px 0 rgba(255, 255, 255, 0); + } + 12% { + box-shadow: + 100px 0 white, + 0px 0 rgba(255, 255, 255, 0), + 0px 0 rgba(255, 255, 255, 0), + 0px 0 rgba(255, 255, 255, 0); + } + 25% { + box-shadow: + 110px 0 white, + 100px 0 white, + 0px 0 rgba(255, 255, 255, 0), + 0px 0 rgba(255, 255, 255, 0); + } + 36% { + box-shadow: + 120px 0 white, + 110px 0 white, + 100px 0 white, + 0px 0 rgba(255, 255, 255, 0); + } + 50% { + box-shadow: + 130px 0 white, + 120px 0 white, + 110px 0 white, + 100px 0 white; + } + 62% { + box-shadow: + 200px 0 rgba(255, 255, 255, 0), + 130px 0 white, + 120px 0 white, + 110px 0 white; + } + 75% { + box-shadow: + 200px 0 rgba(255, 255, 255, 0), + 200px 0 rgba(255, 255, 255, 0), + 130px 0 white, + 120px 0 white; + } + 87% { + box-shadow: + 200px 0 rgba(255, 255, 255, 0), + 200px 0 rgba(255, 255, 255, 0), + 200px 0 rgba(255, 255, 255, 0), + 130px 0 white; + } + 100% { + box-shadow: + 200px 0 rgba(255, 255, 255, 0), + 200px 0 rgba(255, 255, 255, 0), + 200px 0 rgba(255, 255, 255, 0), + 200px 0 rgba(255, 255, 255, 0); + } + } } diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index fbc4c6b..e2b2b2c 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -8,7 +8,7 @@ import Main from '../widgets/Main/Main.tsx'; class MainLayout extends Component { render() { return ( -
            +
            diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Spinner.tsx new file mode 100644 index 0000000..2131f85 --- /dev/null +++ b/src/shared/ui/Spinner.tsx @@ -0,0 +1,16 @@ +import { Component } from 'react'; + +import { createPortal } from 'react-dom'; + +class Spinner extends Component { + render() { + return createPortal( +
            + +
            , + document.body, + ); + } +} + +export default Spinner; diff --git a/tailwind.config.js b/tailwind.config.js index 6d32d8f..b8b5247 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -9,6 +9,19 @@ export default { height: { screen: '100dvh', }, + keyframes: { + 'fade-in': { + '0%': { + opacity: 0, + }, + '100%': { + opacity: 1, + }, + }, + }, + animation: { + 'fade-in': 'fade-in .5s cubic-bezier(0.86, 0, 0.07, 1)', + }, }, fontFamily: { sans: 'Poppins, monospace, sans-serif', From b15bad0a7ac559ed583b3b62166063f45fc03287 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 14:10:54 +0300 Subject: [PATCH 030/144] feat: add error boundary & not found component --- src/app/App.tsx | 10 ++++-- src/features/ProductList/MovieList.tsx | 4 ++- src/features/ProductList/ui/ErrorBoundary.tsx | 32 +++++++++++++++++++ src/features/ProductList/ui/NotFound.tsx | 21 ++++++++++++ src/features/Search/Search.tsx | 16 +++++++--- src/index.css | 5 +++ 6 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 src/features/ProductList/ui/ErrorBoundary.tsx create mode 100644 src/features/ProductList/ui/NotFound.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index 4bf85a4..c9e3d5f 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,14 +1,18 @@ import { Component } from 'react'; +import ErrorBoundary from '../features/ProductList/ui/ErrorBoundary.tsx'; import SearchProvider from '../features/Search/context/SearchProvider.tsx'; import MainLayout from '../pages/MainLayout.tsx'; class App extends Component { render() { return ( - - - + Something went wrong

            }> + + + +
            ); } } diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index 366da1e..36e5287 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -1,5 +1,6 @@ import { Component } from 'react'; +import NotFound from './ui/NotFound.tsx'; import Movie from '../../entities/movie/ui/Movie.tsx'; import { MovieList as MovieListData } from '../../shared/types/types.ts'; import Spinner from '../../shared/ui/Spinner.tsx'; @@ -20,7 +21,8 @@ class MovieList extends Component { render() { const { movies, isLoading } = this.context; - if (!movies || isLoading) return ; + if (!movies?.length) return ; + if (isLoading) return ; return (
              diff --git a/src/features/ProductList/ui/ErrorBoundary.tsx b/src/features/ProductList/ui/ErrorBoundary.tsx new file mode 100644 index 0000000..af12522 --- /dev/null +++ b/src/features/ProductList/ui/ErrorBoundary.tsx @@ -0,0 +1,32 @@ +import { Component, ReactNode } from 'react'; + +import { IChildren } from '../../../shared/types/interfaces.ts'; + +interface IErrorBoundaryState { + hasError?: boolean; +} + +interface IErrorBoundaryProps extends IChildren { + fallback: ReactNode; +} + +class ErrorBoundary extends Component< + IErrorBoundaryProps, + IErrorBoundaryState +> { + state = { + hasError: false, + }; + + static getDerivedStateFromError = () => ({ + hasError: true, + }); + + render() { + if (this.state.hasError) return this.props.fallback; + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src/features/ProductList/ui/NotFound.tsx b/src/features/ProductList/ui/NotFound.tsx new file mode 100644 index 0000000..ee1f668 --- /dev/null +++ b/src/features/ProductList/ui/NotFound.tsx @@ -0,0 +1,21 @@ +import { Component } from 'react'; + +class NotFound extends Component { + render() { + return ( +
              + + No Movies Found. 🥺 + +

              + You're search did not match any movies. +

              +

              + Please try again. 👉👈 +

              +
              + ); + } +} + +export default NotFound; diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 4aa6963..b905074 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -23,11 +23,17 @@ class Search extends Component { } handleSearch = async () => { - this.context.updateIsLoading(true); - const res = await getMovieList(this.state.searchQuery); - this.context.updateMovies(res.Search); - this.context.updateTotalResults(Number(res.totalResults)); - this.context.updateIsLoading(false); + try { + this.context.updateIsLoading(true); + const res = await getMovieList(this.state.searchQuery); + this.context.updateMovies(res.Search); + this.context.updateTotalResults(Number(res.totalResults)); + } catch (e) { + this.context.updateMovies([]); + this.context.updateTotalResults(0); + } finally { + this.context.updateIsLoading(false); + } }; render() { diff --git a/src/index.css b/src/index.css index 02e9473..2b361b8 100644 --- a/src/index.css +++ b/src/index.css @@ -3,6 +3,11 @@ @tailwind utilities; @layer base { + ::selection { + color: theme(colors.lime.400); + background-color: theme(colors.lime.50); + } + * { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; From 5ead68c4ea2b52813bebd3ca2d8df5fe73d0905b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 14:35:52 +0300 Subject: [PATCH 031/144] refactor: change to handle fetch logic in context --- src/features/ProductList/MovieList.tsx | 6 ++-- src/features/Search/Search.tsx | 13 +------- .../Search/context/SearchProvider.tsx | 33 ++++++++++++------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index 36e5287..6df00a7 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -20,9 +20,11 @@ class MovieList extends Component { render() { const { movies, isLoading } = this.context; + const isNoMovies = !movies?.length; + const firstRender = movies === null; - if (!movies?.length) return ; - if (isLoading) return ; + if (isLoading || firstRender) return ; + if (isNoMovies) return ; return (
                diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index b905074..b19bff9 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -3,7 +3,6 @@ import { Component } from 'react'; import { ISearchContext, SearchContext } from './context/SearchProvider.tsx'; import Button from './ui/Button.tsx'; import Input from './ui/Input.tsx'; -import { getMovieList } from '../../entities/movie/api/apiMovie.ts'; interface ISearchState { searchQuery: string; @@ -23,17 +22,7 @@ class Search extends Component { } handleSearch = async () => { - try { - this.context.updateIsLoading(true); - const res = await getMovieList(this.state.searchQuery); - this.context.updateMovies(res.Search); - this.context.updateTotalResults(Number(res.totalResults)); - } catch (e) { - this.context.updateMovies([]); - this.context.updateTotalResults(0); - } finally { - this.context.updateIsLoading(false); - } + this.context.fetchMovies(this.state.searchQuery); }; render() { diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx index 4b2394b..8f3264d 100644 --- a/src/features/Search/context/SearchProvider.tsx +++ b/src/features/Search/context/SearchProvider.tsx @@ -1,5 +1,6 @@ import { Component, createContext } from 'react'; +import { getMovieList } from '../../../entities/movie/api/apiMovie.ts'; import { IChildren } from '../../../shared/types/interfaces.ts'; import { MovieList } from '../../../shared/types/types.ts'; @@ -12,11 +13,14 @@ interface ISearchProviderState { export interface ISearchContext extends ISearchProviderState { updateQuery: (newQuery: string) => void; - updateMovies: (newMovies: MovieList) => void; - updateTotalResults: (newTotal: number) => void; + updateMovies: (newMovies: MovieList, totalResults: number) => void; updateIsLoading: (isLoading: boolean) => void; + fetchMovies: (searchQuery: string) => void; } +const NO_RESULTS = 0; +const NO_MOVIES: MovieList = []; + export const SearchContext = createContext({ query: '', } as ISearchContext); @@ -33,22 +37,29 @@ class SearchProvider extends Component { this.setState({ query: newQuery }); }; - updateMovies = (newMovies: MovieList) => { - this.setState({ movies: newMovies }); - }; - - updateTotalResults = (newTotal: number) => { - this.setState({ totalResults: newTotal }); + updateMovies = (newMovies: MovieList, totalResults: number) => { + this.setState({ movies: newMovies, totalResults }); }; updateIsLoading = (isLoading: boolean) => { this.setState({ isLoading }); }; + fetchMovies = async (searchQuery: string) => { + try { + this.updateIsLoading(true); + const res = await getMovieList(searchQuery); + this.updateMovies(res.Search, Number(res.totalResults)); + } catch (e) { + this.updateMovies(NO_MOVIES, NO_RESULTS); + } finally { + this.updateIsLoading(false); + } + }; + render() { const { query, movies, totalResults, isLoading } = this.state; - const { updateQuery, updateMovies, updateTotalResults, updateIsLoading } = - this; + const { updateQuery, updateMovies, updateIsLoading, fetchMovies } = this; return ( { isLoading, updateQuery, updateMovies, - updateTotalResults, updateIsLoading, + fetchMovies, }}> {this.props.children} From 4884194e6834d4d9028e7a7273927c7c45068669 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 14:38:12 +0300 Subject: [PATCH 032/144] refactor: change not found animation --- src/features/ProductList/ui/NotFound.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/ProductList/ui/NotFound.tsx b/src/features/ProductList/ui/NotFound.tsx index ee1f668..6defc32 100644 --- a/src/features/ProductList/ui/NotFound.tsx +++ b/src/features/ProductList/ui/NotFound.tsx @@ -3,14 +3,14 @@ import { Component } from 'react'; class NotFound extends Component { render() { return ( -
                - +
                + No Movies Found. 🥺 -

                +

                You're search did not match any movies.

                -

                +

                Please try again. 👉👈

                From 09cf55c9d8d6b18b72518b91dd3d8e006b8aef02 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 14:50:14 +0300 Subject: [PATCH 033/144] feat: implement to store query in local storage --- src/features/Search/Search.tsx | 26 ++++++++++++++++------ src/features/Search/ui/Input.tsx | 37 -------------------------------- src/shared/const/const.ts | 1 + 3 files changed, 20 insertions(+), 44 deletions(-) delete mode 100644 src/features/Search/ui/Input.tsx diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index b19bff9..39730fc 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -2,7 +2,7 @@ import { Component } from 'react'; import { ISearchContext, SearchContext } from './context/SearchProvider.tsx'; import Button from './ui/Button.tsx'; -import Input from './ui/Input.tsx'; +import { LOCAL_STORAGE_SEARCH_QUERY } from '../../shared/const/const.ts'; interface ISearchState { searchQuery: string; @@ -18,21 +18,33 @@ class Search extends Component { }; componentDidMount() { - void this.handleSearch(); + const storedQuery = localStorage.getItem(LOCAL_STORAGE_SEARCH_QUERY); + + if (storedQuery) { + this.setState({ searchQuery: storedQuery }); + void this.handleSearch(storedQuery); + return; + } + + void this.handleSearch(''); } - handleSearch = async () => { - this.context.fetchMovies(this.state.searchQuery); + handleSearch = async (query: string) => { + this.context.fetchMovies(query); + localStorage.setItem(LOCAL_STORAGE_SEARCH_QUERY, query); }; render() { return (
                - this.setState({ searchQuery: newVal })} + onChange={(e) => this.setState({ searchQuery: e.target.value })} /> -
                ); } diff --git a/src/features/Search/ui/Input.tsx b/src/features/Search/ui/Input.tsx deleted file mode 100644 index b5b43ab..0000000 --- a/src/features/Search/ui/Input.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Component, SyntheticEvent } from 'react'; - -interface IInputState { - val: string; -} - -interface IInputProps { - value?: string; - onChange?: (newVal: string) => void; -} - -class Input extends Component { - state = { - val: this.props.value ?? '', - }; - - handleChange = (e: SyntheticEvent) => { - const { value } = e.target as HTMLInputElement; - - this.props.onChange?.(value); - this.setState({ val: value }); - }; - - render() { - return ( - - ); - } -} - -export default Input; diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts index e964bb9..0ae3d8d 100644 --- a/src/shared/const/const.ts +++ b/src/shared/const/const.ts @@ -2,3 +2,4 @@ export const API_KEY = 'dbb72d83'; export const API_URL = `https://www.omdbapi.com/?apikey=${API_KEY}`; export const QUERY_FALLBACK = 'shazam'; export const NOT_EXIST = 'N/A'; +export const LOCAL_STORAGE_SEARCH_QUERY = 'search-query'; From ed1be69673901a8d2dfecaf408ef6ecdd445f3fb Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 14:59:26 +0300 Subject: [PATCH 034/144] refactor: adjust search component --- src/features/Search/Search.tsx | 12 ++++++++---- src/features/Search/ui/Button.tsx | 20 -------------------- src/widgets/Header/ui/TotalResults.tsx | 2 +- 3 files changed, 9 insertions(+), 25 deletions(-) delete mode 100644 src/features/Search/ui/Button.tsx diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 39730fc..afc7e92 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -1,7 +1,6 @@ import { Component } from 'react'; import { ISearchContext, SearchContext } from './context/SearchProvider.tsx'; -import Button from './ui/Button.tsx'; import { LOCAL_STORAGE_SEARCH_QUERY } from '../../shared/const/const.ts'; interface ISearchState { @@ -36,15 +35,20 @@ class Search extends Component { render() { return ( -
                +
                this.setState({ searchQuery: e.target.value })} /> -
                ); } diff --git a/src/features/Search/ui/Button.tsx b/src/features/Search/ui/Button.tsx deleted file mode 100644 index d0694dd..0000000 --- a/src/features/Search/ui/Button.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Component } from 'react'; - -interface ISearchBtnProps { - onClick?: () => void; -} - -class Button extends Component { - render() { - return ( - - ); - } -} - -export default Button; diff --git a/src/widgets/Header/ui/TotalResults.tsx b/src/widgets/Header/ui/TotalResults.tsx index 93a99f1..52c9c7b 100644 --- a/src/widgets/Header/ui/TotalResults.tsx +++ b/src/widgets/Header/ui/TotalResults.tsx @@ -12,7 +12,7 @@ class TotalResults extends Component { render() { return ( -

                +

                Found {this.context.totalResults}{' '} total results

                From 856586d5106884c2b8b0fb7b24848085057136da Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 17:01:55 +0300 Subject: [PATCH 035/144] feat: add nice movie hover effect --- src/entities/movie/ui/Movie.tsx | 57 ++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 905d0f5..531de1c 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,4 +1,6 @@ -import { Component } from 'react'; +import { Component, createRef, MouseEvent } from 'react'; + +import colors from 'tailwindcss/colors'; import ReactLogo from '../../../assets/reactJS-logo.png'; import { NOT_EXIST } from '../../../shared/const/const.ts'; @@ -20,6 +22,8 @@ class Movie extends Component { genre: '', }; + movieRef = createRef(); + async componentDidMount() { const movieData = await getMovie(this.props.data.imdbID); const genre = movieData.Genre.split(', ').slice(0, 2).join('/'); @@ -31,6 +35,22 @@ class Movie extends Component { }); } + handleMouseMove = (e: MouseEvent) => { + const rect = this.movieRef.current?.getBoundingClientRect(); + + if (rect) { + const pointerX = e.clientX - rect.left; + const pointerY = e.clientY - rect.top; + + if (this.movieRef.current) + this.movieRef.current.style.background = `radial-gradient(circle at ${pointerX}px ${pointerY}px, ${colors.pink['950']} 0%, ${colors.neutral['950']} 160px)`; + } + }; + + handleMouseOut = () => { + if (this.movieRef.current) this.movieRef.current.style.background = ''; + }; + render() { const { Poster, Title, Year } = this.props.data; const { description, genre } = this.state; @@ -39,20 +59,27 @@ class Movie extends Component { const poster = isPosterExist ? Poster : ReactLogo; return ( -
              • - {`The -
                -

                {Title}

                -
                -

                {description}

                - {genre} - {Year} -
                -
                +
              • +
                + {`The +
                +

                {Title}

                +
                +

                {description}

                + {genre} + {Year} +
                +
                +
              • ); } From bfa91107032c0c44bd4924f030efdda106807854 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 17:02:32 +0300 Subject: [PATCH 036/144] chore: adjust eslint rule --- .eslintrc.cjs | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6a57cab..d4b2472 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -26,6 +26,7 @@ module.exports = { rules: { // TODO: remove 👇 after 1 module 'react/jsx-no-constructed-context-values': 0, + 'import/no-extraneous-dependencies': 0, 'react/static-property-placement': 0, 'react/destructuring-assignment': 0, 'react/require-default-props': 0, From 0f8525a441e6e2ad81f0ff6539fa03674aba1fc6 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 17:23:40 +0300 Subject: [PATCH 037/144] feat: add nice not found hover effect --- src/features/ProductList/ui/NotFound.tsx | 49 ++++++++++++++++++------ 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/src/features/ProductList/ui/NotFound.tsx b/src/features/ProductList/ui/NotFound.tsx index 6defc32..85df328 100644 --- a/src/features/ProductList/ui/NotFound.tsx +++ b/src/features/ProductList/ui/NotFound.tsx @@ -1,18 +1,45 @@ -import { Component } from 'react'; +import { Component, createRef, MouseEvent } from 'react'; + +import colors from 'tailwindcss/colors'; class NotFound extends Component { + movieRef = createRef(); + + handleMouseMove = (e: MouseEvent) => { + const rect = this.movieRef.current?.getBoundingClientRect(); + + if (rect) { + const pointerX = e.clientX - rect.left; + const pointerY = e.clientY - rect.top; + + if (this.movieRef.current) + this.movieRef.current.style.background = `radial-gradient(circle at ${pointerX}px ${pointerY}px, rgb(112, 26, 117, 0.5) 0%, ${colors.transparent} 160px)`; + } + }; + + handleMouseOut = () => { + if (this.movieRef.current) this.movieRef.current.style.background = ''; + }; + render() { return ( -
                - - No Movies Found. 🥺 - -

                - You're search did not match any movies. -

                -

                - Please try again. 👉👈 -

                +
                +
                + + No Movies Found. 🥺 + +

                + You're search did not match any movies. +

                +

                + Please try again. 👉👈 +

                +
                ); } From cdfab7f197e3c4bc4916e83429a131befb31f00c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 17:29:45 +0300 Subject: [PATCH 038/144] refactor: adjust ui --- src/entities/movie/ui/Movie.tsx | 2 +- src/shared/ui/Spinner.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 531de1c..a558929 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -74,7 +74,7 @@ class Movie extends Component {

                {Title}

                -

                {description}

                +

                {description}

                {genre} {Year}
                diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Spinner.tsx index 2131f85..0669cf4 100644 --- a/src/shared/ui/Spinner.tsx +++ b/src/shared/ui/Spinner.tsx @@ -5,8 +5,8 @@ import { createPortal } from 'react-dom'; class Spinner extends Component { render() { return createPortal( -
                - +
                +
                , document.body, ); From 4e07c64790768541cf12f9e3259eaebf4ff848ea Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 18:01:31 +0300 Subject: [PATCH 039/144] feat: add logo emoji --- src/shared/const/const.ts | 1 + src/widgets/Header/ui/Logo.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts index 0ae3d8d..6b8f90b 100644 --- a/src/shared/const/const.ts +++ b/src/shared/const/const.ts @@ -3,3 +3,4 @@ export const API_URL = `https://www.omdbapi.com/?apikey=${API_KEY}`; export const QUERY_FALLBACK = 'shazam'; export const NOT_EXIST = 'N/A'; export const LOCAL_STORAGE_SEARCH_QUERY = 'search-query'; +export const APP_TITLE = 'CINEMANIA'; diff --git a/src/widgets/Header/ui/Logo.tsx b/src/widgets/Header/ui/Logo.tsx index d9ceb5b..97ea96c 100644 --- a/src/widgets/Header/ui/Logo.tsx +++ b/src/widgets/Header/ui/Logo.tsx @@ -1,10 +1,13 @@ import { Component } from 'react'; +import { APP_TITLE } from '../../../shared/const/const.ts'; + class Logo extends Component { render() { return ( - - CINEMACITY + + 🍿 + {APP_TITLE} ); } From 9024fddd96db316768e6e820494142fe2f57502f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 18:12:09 +0300 Subject: [PATCH 040/144] feat: change app title --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index ec4c7e5..21a5e48 100644 --- a/index.html +++ b/index.html @@ -4,14 +4,14 @@ + href="data:image/svg+xml,🍿️" /> - React components + Cinemania | Dive into Movie Wonderland
                From 1a4448b30f4ff0cdce698938537a629bb7502d7d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 18:32:19 +0300 Subject: [PATCH 041/144] feat: minor ui improvements --- src/entities/movie/ui/Movie.tsx | 8 ++++---- src/index.css | 2 +- src/widgets/Header/ui/Logo.tsx | 4 ++-- src/widgets/Header/ui/TotalResults.tsx | 2 +- tailwind.config.js | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index a558929..06a11fa 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -27,7 +27,7 @@ class Movie extends Component { async componentDidMount() { const movieData = await getMovie(this.props.data.imdbID); const genre = movieData.Genre.split(', ').slice(0, 2).join('/'); - const description = `${movieData.Plot.slice(0, 42)}...`; + const description = `${movieData.Plot.slice(0, 38)}...`; this.setState({ description, @@ -59,7 +59,7 @@ class Movie extends Component { const poster = isPosterExist ? Poster : ReactLogo; return ( -
              • +
              • { />

                {Title}

                -
                -

                {description}

                +
                +

                {description}

                {genre} {Year}
                diff --git a/src/index.css b/src/index.css index 2b361b8..bbd4f05 100644 --- a/src/index.css +++ b/src/index.css @@ -4,7 +4,7 @@ @layer base { ::selection { - color: theme(colors.lime.400); + color: theme(colors.neutral.950); background-color: theme(colors.lime.50); } diff --git a/src/widgets/Header/ui/Logo.tsx b/src/widgets/Header/ui/Logo.tsx index 97ea96c..871ca90 100644 --- a/src/widgets/Header/ui/Logo.tsx +++ b/src/widgets/Header/ui/Logo.tsx @@ -5,10 +5,10 @@ import { APP_TITLE } from '../../../shared/const/const.ts'; class Logo extends Component { render() { return ( - +

                🍿 {APP_TITLE} - +

                ); } } diff --git a/src/widgets/Header/ui/TotalResults.tsx b/src/widgets/Header/ui/TotalResults.tsx index 52c9c7b..3cae10d 100644 --- a/src/widgets/Header/ui/TotalResults.tsx +++ b/src/widgets/Header/ui/TotalResults.tsx @@ -14,7 +14,7 @@ class TotalResults extends Component { return (

                Found {this.context.totalResults}{' '} - total results + movies

                ); } diff --git a/tailwind.config.js b/tailwind.config.js index b8b5247..ce4dd43 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -20,7 +20,7 @@ export default { }, }, animation: { - 'fade-in': 'fade-in .5s cubic-bezier(0.86, 0, 0.07, 1)', + 'fade-in': 'fade-in .8s cubic-bezier(0.86, 0, 0.07, 1)', }, }, fontFamily: { From 89ddcfc4b5719b66f6927c1f137a53911a37a335 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 22:03:46 +0300 Subject: [PATCH 042/144] fix: to cut trailing spaces --- src/features/Search/Search.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index afc7e92..459ad2a 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -29,8 +29,8 @@ class Search extends Component { } handleSearch = async (query: string) => { - this.context.fetchMovies(query); - localStorage.setItem(LOCAL_STORAGE_SEARCH_QUERY, query); + this.context.fetchMovies(query.trim()); + localStorage.setItem(LOCAL_STORAGE_SEARCH_QUERY, query.trim()); }; render() { From 4465b016c7d7173a8ff81ae4373fca2ad13e1132 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 22:50:08 +0300 Subject: [PATCH 043/144] feat: add springish animation --- src/entities/movie/ui/Movie.tsx | 13 +++++++++- src/features/ProductList/MovieList.tsx | 4 +-- src/features/ProductList/ui/NotFound.tsx | 12 +++------ src/index.css | 32 ++++++++++++++++++++++++ tailwind.config.js | 10 ++++++++ 5 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 06a11fa..7aa5630 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -9,6 +9,7 @@ import { getMovie } from '../api/apiMovie.ts'; interface IMovieProps { data: MovieData; + delay: number; } interface IMovieState { @@ -24,7 +25,15 @@ class Movie extends Component { movieRef = createRef(); + containerRef = createRef(); + async componentDidMount() { + if (this.containerRef.current) { + this.containerRef.current.style.animationDelay = `0.${String( + this.props.delay, + )}s`; + } + const movieData = await getMovie(this.props.data.imdbID); const genre = movieData.Genre.split(', ').slice(0, 2).join('/'); const description = `${movieData.Plot.slice(0, 38)}...`; @@ -59,7 +68,9 @@ class Movie extends Component { const poster = isPosterExist ? Poster : ReactLogo; return ( -
              • +
              • { return (
                  - {movies.map((movie) => ( - + {movies.map((movie, i) => ( + ))}
                ); diff --git a/src/features/ProductList/ui/NotFound.tsx b/src/features/ProductList/ui/NotFound.tsx index 85df328..48c4699 100644 --- a/src/features/ProductList/ui/NotFound.tsx +++ b/src/features/ProductList/ui/NotFound.tsx @@ -23,22 +23,18 @@ class NotFound extends Component { render() { return ( -
                +
                - + No Movies Found. 🥺 -

                - You're search did not match any movies. -

                -

                - Please try again. 👉👈 -

                +

                You're search did not match any movies.

                +

                Please try again. 👉👈

                ); diff --git a/src/index.css b/src/index.css index bbd4f05..2566698 100644 --- a/src/index.css +++ b/src/index.css @@ -97,4 +97,36 @@ 200px 0 rgba(255, 255, 255, 0); } } + + @keyframes springish { + 0.00% { + transform: translate3d(30px, 0, 0) scale(0.7); + opacity: 0; + } + 11.51% { + transform: translate3d(-13.02px, 0, 0) scale(1.1301999999999999); + } + 24.15% { + transform: translate3d(5.43px, 0, 0) scale(0.9457); + opacity: 1; + } + 36.79% { + transform: translate3d(-2.26px, 0, 0) scale(1.0226); + } + 49.43% { + transform: translate3d(0.94px, 0, 0) scale(0.9906); + } + 62.07% { + transform: translate3d(-0.39px, 0, 0) scale(1.0039); + } + 74.72% { + transform: translate3d(0.16px, 0, 0) scale(0.9984); + } + 87.36% { + transform: translate3d(-0.07px, 0, 0) scale(1.0007); + } + 100.00% { + transform: translate3d(0.03px, 0, 0) scale(0.9997); + } + } } diff --git a/tailwind.config.js b/tailwind.config.js index ce4dd43..a2ca48b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -18,9 +18,19 @@ export default { opacity: 1, }, }, + 'movie-fade-in': { + '0%': { + translate: '0 90px', + }, + '100%': { + translate: '0 0', + }, + }, }, animation: { 'fade-in': 'fade-in .8s cubic-bezier(0.86, 0, 0.07, 1)', + springish: + 'springish 1.58s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', }, }, fontFamily: { From 322c5ae4769f973eb23ef93fbf5ed2e24fdafbb4 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 23:07:37 +0300 Subject: [PATCH 044/144] refactor: move all loading logic to spinner --- src/features/ProductList/MovieList.tsx | 5 +---- src/shared/ui/Spinner.tsx | 11 +++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index 737529d..c6bc757 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -3,7 +3,6 @@ import { Component } from 'react'; import NotFound from './ui/NotFound.tsx'; import Movie from '../../entities/movie/ui/Movie.tsx'; import { MovieList as MovieListData } from '../../shared/types/types.ts'; -import Spinner from '../../shared/ui/Spinner.tsx'; import { ISearchContext, SearchContext, @@ -19,11 +18,9 @@ class MovieList extends Component { declare context: ISearchContext; render() { - const { movies, isLoading } = this.context; + const { movies } = this.context; const isNoMovies = !movies?.length; - const firstRender = movies === null; - if (isLoading || firstRender) return ; if (isNoMovies) return ; return ( diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Spinner.tsx index 0669cf4..8c95ef2 100644 --- a/src/shared/ui/Spinner.tsx +++ b/src/shared/ui/Spinner.tsx @@ -2,8 +2,19 @@ import { Component } from 'react'; import { createPortal } from 'react-dom'; +import { + ISearchContext, + SearchContext, +} from '../../features/Search/context/SearchProvider.tsx'; + class Spinner extends Component { + static contextType = SearchContext; + + declare context: ISearchContext; + render() { + if (!this.context.isLoading) return null; + return createPortal(
                From 9100d3e86346479e78ac4189a98df11fb061da91 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Oct 2023 23:08:35 +0300 Subject: [PATCH 045/144] refactor: change hover color --- src/entities/movie/ui/Movie.tsx | 2 +- src/pages/MainLayout.tsx | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 7aa5630..780e539 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -52,7 +52,7 @@ class Movie extends Component { const pointerY = e.clientY - rect.top; if (this.movieRef.current) - this.movieRef.current.style.background = `radial-gradient(circle at ${pointerX}px ${pointerY}px, ${colors.pink['950']} 0%, ${colors.neutral['950']} 160px)`; + this.movieRef.current.style.background = `radial-gradient(circle at ${pointerX}px ${pointerY}px, rgb(112, 26, 117, 0.4) 0%, ${colors.neutral['950']} 190px)`; } }; diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index e2b2b2c..201168c 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -2,22 +2,27 @@ import { Component } from 'react'; import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/ProductList/MovieList.tsx'; +import Spinner from '../shared/ui/Spinner.tsx'; import Header from '../widgets/Header/Header.tsx'; import Main from '../widgets/Main/Main.tsx'; class MainLayout extends Component { render() { return ( -
                - + <> + -
                -
                -
                - -
                +
                + + +
                +
                +
                + +
                +
                -
                + ); } } From 2d2bdaddcdd934e6320a55ee267e3ced8703f419 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 28 Oct 2023 14:13:03 +0300 Subject: [PATCH 046/144] feat: ui improvements --- src/features/ProductList/MovieList.tsx | 5 +++-- src/index.css | 18 ++++++++++++++++++ src/pages/ui/GradientBackground.tsx | 16 +++++++--------- tailwind.config.js | 26 ++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index c6bc757..c1efb71 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -18,10 +18,11 @@ class MovieList extends Component { declare context: ISearchContext; render() { - const { movies } = this.context; + const { movies, isLoading } = this.context; const isNoMovies = !movies?.length; - if (isNoMovies) return ; + if (isNoMovies && !isLoading) return ; + if (isNoMovies) return null; return (
                  diff --git a/src/index.css b/src/index.css index 2566698..fe2aa84 100644 --- a/src/index.css +++ b/src/index.css @@ -130,3 +130,21 @@ } } } + +@layer utilities { + .animation-delay-2000 { + animation-delay: 2s; + } + + .animation-delay-4000 { + animation-delay: 4s; + } + + .animation-delay-6000 { + animation-delay: 6s; + } + + .animation-delay-8000 { + animation-delay: 8s; + } +} diff --git a/src/pages/ui/GradientBackground.tsx b/src/pages/ui/GradientBackground.tsx index 134e21f..b8d59dc 100644 --- a/src/pages/ui/GradientBackground.tsx +++ b/src/pages/ui/GradientBackground.tsx @@ -3,15 +3,13 @@ import { Component } from 'react'; class GradientBackground extends Component { render() { return ( - <> -
                  - -
                  -
                  -
                  -
                  -
                  - +
                  +
                  +
                  +
                  +
                  +
                  +
                  ); } } diff --git a/tailwind.config.js b/tailwind.config.js index a2ca48b..d9b2266 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -26,11 +26,37 @@ export default { translate: '0 0', }, }, + float: { + '0%': { + translate3d: '0', + rotate: 0, + }, + '20%': { + transform: 'translate3d(-50px, 15px, 0)', + rotate: '7deg', + scale: '0.9', + }, + '40%': { + rotate: '-7deg', + transform: 'translate3d(-30px, -15px, 0)', + }, + '60%': { + rotate: '10deg', + scale: 1.2, + transform: 'translate3d(30px, 20px, 0)', + }, + '100%': { + translate3d: '0', + rotate: 0, + scale: 1, + }, + }, }, animation: { 'fade-in': 'fade-in .8s cubic-bezier(0.86, 0, 0.07, 1)', springish: 'springish 1.58s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', + float: 'float 20s ease-in-out infinite', }, }, fontFamily: { From 594182b4488a3a6c37c4f227e83812e711526186 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 28 Oct 2023 18:22:03 +0300 Subject: [PATCH 047/144] feat: add search icon --- src/assets/search.svg | 3 +++ src/features/Search/Search.tsx | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 src/assets/search.svg diff --git a/src/assets/search.svg b/src/assets/search.svg new file mode 100644 index 0000000..4341045 --- /dev/null +++ b/src/assets/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 459ad2a..fb9e24b 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -1,6 +1,7 @@ import { Component } from 'react'; import { ISearchContext, SearchContext } from './context/SearchProvider.tsx'; +import searchIcon from '../../assets/search.svg'; import { LOCAL_STORAGE_SEARCH_QUERY } from '../../shared/const/const.ts'; interface ISearchState { @@ -37,7 +38,7 @@ class Search extends Component { return (
                  { />
                  ); From 3ecbc85e3c3fa8de59167608a7ba048b705cc3de Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 28 Oct 2023 22:21:59 +0300 Subject: [PATCH 048/144] refactor: change layout to component composition style --- src/pages/MainLayout.tsx | 13 ++++++++++--- src/widgets/Header/Header.tsx | 10 +++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 201168c..0488b39 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -2,8 +2,11 @@ import { Component } from 'react'; import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/ProductList/MovieList.tsx'; +import Search from '../features/Search/Search.tsx'; import Spinner from '../shared/ui/Spinner.tsx'; import Header from '../widgets/Header/Header.tsx'; +import Logo from '../widgets/Header/ui/Logo.tsx'; +import TotalResults from '../widgets/Header/ui/TotalResults.tsx'; import Main from '../widgets/Main/Main.tsx'; class MainLayout extends Component { @@ -15,12 +18,16 @@ class MainLayout extends Component {
                  -
                  -
                  +
                  +
                  + + + +
                  -
                  +
                  ); diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index d04bf79..41f9a78 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -1,16 +1,12 @@ import { Component } from 'react'; -import Logo from './ui/Logo.tsx'; -import TotalResults from './ui/TotalResults.tsx'; -import Search from '../../features/Search/Search.tsx'; +import { IChildren } from '../../shared/types/interfaces.ts'; -class Header extends Component { +class Header extends Component { render() { return (
                  - - - + {this.props.children}
                  ); } From 2548eb9fbe4d346871e533d80873bed30e77563d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 28 Oct 2023 22:29:09 +0300 Subject: [PATCH 049/144] refactor: change fall query to "all" --- src/shared/const/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts index 6b8f90b..8ed4950 100644 --- a/src/shared/const/const.ts +++ b/src/shared/const/const.ts @@ -1,6 +1,6 @@ export const API_KEY = 'dbb72d83'; export const API_URL = `https://www.omdbapi.com/?apikey=${API_KEY}`; -export const QUERY_FALLBACK = 'shazam'; +export const QUERY_FALLBACK = 'all'; export const NOT_EXIST = 'N/A'; export const LOCAL_STORAGE_SEARCH_QUERY = 'search-query'; export const APP_TITLE = 'CINEMANIA'; From 66c12dc1c422d720ddc860c99ea0376be8a6cd36 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 00:50:58 +0300 Subject: [PATCH 050/144] feat: add logo animation --- src/index.css | 32 ++++++++++++++++++++++++++++++++ src/widgets/Header/ui/Logo.tsx | 15 +++++++++++++-- tailwind.config.js | 4 +++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/index.css b/src/index.css index fe2aa84..4c15896 100644 --- a/src/index.css +++ b/src/index.css @@ -129,6 +129,38 @@ transform: translate3d(0.03px, 0, 0) scale(0.9997); } } + + @keyframes springish-letter { + 0.00% { + transform: translate3d(119.4px, 0, 0); + opacity: 0; + } + 10.97% { + transform: translate3d(-44.48px, 0, 0); + } + 23.69% { + transform: translate3d(15.02px, 0, 0); + opacity: 1; + } + 36.41% { + transform: translate3d(-5.07px, 0, 0); + } + 49.12% { + transform: translate3d(1.71px, 0, 0); + } + 61.84% { + transform: translate3d(-0.58px, 0, 0); + } + 74.56% { + transform: translate3d(0.2px, 0, 0); + } + 87.28% { + transform: translate3d(-0.07px, 0, 0); + } + 100.00% { + transform: translate3d(0.02px, 0, 0); + } + } } @layer utilities { diff --git a/src/widgets/Header/ui/Logo.tsx b/src/widgets/Header/ui/Logo.tsx index 871ca90..bfdc692 100644 --- a/src/widgets/Header/ui/Logo.tsx +++ b/src/widgets/Header/ui/Logo.tsx @@ -6,8 +6,19 @@ class Logo extends Component { render() { return (

                  - 🍿 - {APP_TITLE} +
                  + {['🍿', ...APP_TITLE.split('')].map((letter, i) => ( + + {letter} + + ))} +

                  ); } diff --git a/tailwind.config.js b/tailwind.config.js index d9b2266..9ffd825 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -53,9 +53,11 @@ export default { }, }, animation: { - 'fade-in': 'fade-in .8s cubic-bezier(0.86, 0, 0.07, 1)', + 'fade-in': 'fade-in .8s cubic-bezier(0.86, 0, 0.07, 1) both', springish: 'springish 1.58s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', + 'springish-letter': + 'springish-letter 2.73s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', float: 'float 20s ease-in-out infinite', }, }, From ce11582c76b79804656533d5965ba7339ece9700 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 13:23:21 +0200 Subject: [PATCH 051/144] refactor: change logo font --- index.html | 8 +++++--- src/index.css | 29 ++++++++++------------------- src/shared/const/const.ts | 1 - src/widgets/Header/const/const.ts | 2 ++ src/widgets/Header/ui/Logo.tsx | 29 ++++++++++++++--------------- tailwind.config.js | 3 ++- 6 files changed, 33 insertions(+), 39 deletions(-) create mode 100644 src/widgets/Header/const/const.ts diff --git a/index.html b/index.html index 21a5e48..9c50dab 100644 --- a/index.html +++ b/index.html @@ -7,9 +7,11 @@ href="data:image/svg+xml,🍿️" /> - - - + + + Cinemania | Dive into Movie Wonderland diff --git a/src/index.css b/src/index.css index 4c15896..072d218 100644 --- a/src/index.css +++ b/src/index.css @@ -132,33 +132,24 @@ @keyframes springish-letter { 0.00% { - transform: translate3d(119.4px, 0, 0); + transform: translate3d(100px, 0, 0); opacity: 0; } - 10.97% { - transform: translate3d(-44.48px, 0, 0); - } - 23.69% { - transform: translate3d(15.02px, 0, 0); + 17.50% { + transform: translate3d(-23.4px, 0, 0); opacity: 1; } - 36.41% { - transform: translate3d(-5.07px, 0, 0); - } - 49.12% { - transform: translate3d(1.71px, 0, 0); - } - 61.84% { - transform: translate3d(-0.58px, 0, 0); + 38.13% { + transform: translate3d(4.82px, 0, 0); } - 74.56% { - transform: translate3d(0.2px, 0, 0); + 58.75% { + transform: translate3d(-0.99px, 0, 0); } - 87.28% { - transform: translate3d(-0.07px, 0, 0); + 79.38% { + transform: translate3d(0.21px, 0, 0); } 100.00% { - transform: translate3d(0.02px, 0, 0); + transform: translate3d(-0.04px, 0, 0); } } } diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts index 8ed4950..db26105 100644 --- a/src/shared/const/const.ts +++ b/src/shared/const/const.ts @@ -3,4 +3,3 @@ export const API_URL = `https://www.omdbapi.com/?apikey=${API_KEY}`; export const QUERY_FALLBACK = 'all'; export const NOT_EXIST = 'N/A'; export const LOCAL_STORAGE_SEARCH_QUERY = 'search-query'; -export const APP_TITLE = 'CINEMANIA'; diff --git a/src/widgets/Header/const/const.ts b/src/widgets/Header/const/const.ts new file mode 100644 index 0000000..dee8d3e --- /dev/null +++ b/src/widgets/Header/const/const.ts @@ -0,0 +1,2 @@ +export const APP_TITLE = 'cinemania'; +export const LOGO_LETTERS = ['🍿', ...APP_TITLE.split('')]; diff --git a/src/widgets/Header/ui/Logo.tsx b/src/widgets/Header/ui/Logo.tsx index bfdc692..2eb26f6 100644 --- a/src/widgets/Header/ui/Logo.tsx +++ b/src/widgets/Header/ui/Logo.tsx @@ -1,24 +1,23 @@ import { Component } from 'react'; -import { APP_TITLE } from '../../../shared/const/const.ts'; +import { LOGO_LETTERS } from '../const/const.ts'; class Logo extends Component { render() { return ( -

                  -
                  - {['🍿', ...APP_TITLE.split('')].map((letter, i) => ( - - {letter} - - ))} -
                  +

                  + {LOGO_LETTERS.map((letter, i) => ( + + {letter} + + ))}

                  ); } diff --git a/tailwind.config.js b/tailwind.config.js index 9ffd825..3427bb0 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -57,12 +57,13 @@ export default { springish: 'springish 1.58s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', 'springish-letter': - 'springish-letter 2.73s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', + 'springish-letter 1.55s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', float: 'float 20s ease-in-out infinite', }, }, fontFamily: { sans: 'Poppins, monospace, sans-serif', + quicksand: 'Quicksand, sans-serif', }, }, plugins: [], From 49bfd623512f99ade959fe736f215d6630a83eee Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 14:43:18 +0200 Subject: [PATCH 052/144] refactor: improve app ui --- src/widgets/Header/Header.tsx | 31 +++++++++++++++++++++++++++++-- src/widgets/Header/const/const.ts | 1 + tailwind.config.js | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 41f9a78..3cf45c5 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -1,11 +1,38 @@ -import { Component } from 'react'; +import { Component, createRef } from 'react'; +import { SCROLL_THRESHOLD } from './const/const.ts'; import { IChildren } from '../../shared/types/interfaces.ts'; class Header extends Component { + containerRef = createRef(); + + componentDidMount() { + window.addEventListener('scroll', this.handleScroll); + } + + componentWillUnmount() { + window.removeEventListener('scroll', this.handleScroll); + } + + handleScroll = () => { + if (!this.containerRef.current) return; + + const isThresholdScrolled = + document.body.scrollTop > SCROLL_THRESHOLD || + document.documentElement.scrollTop > SCROLL_THRESHOLD; + + if (isThresholdScrolled) { + this.containerRef.current.classList.add('backdrop-blur-xl'); + } else { + this.containerRef.current.classList.remove('backdrop-blur-xl'); + } + }; + render() { return ( -
                  +
                  {this.props.children}
                  ); diff --git a/src/widgets/Header/const/const.ts b/src/widgets/Header/const/const.ts index dee8d3e..868ff98 100644 --- a/src/widgets/Header/const/const.ts +++ b/src/widgets/Header/const/const.ts @@ -1,2 +1,3 @@ export const APP_TITLE = 'cinemania'; export const LOGO_LETTERS = ['🍿', ...APP_TITLE.split('')]; +export const SCROLL_THRESHOLD = 10; diff --git a/tailwind.config.js b/tailwind.config.js index 3427bb0..9ad338f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -55,7 +55,7 @@ export default { animation: { 'fade-in': 'fade-in .8s cubic-bezier(0.86, 0, 0.07, 1) both', springish: - 'springish 1.58s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', + 'springish 1.72s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', 'springish-letter': 'springish-letter 1.55s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', float: 'float 20s ease-in-out infinite', From 33fa8a3a37862c9e40843664f807d9e27cbfc54f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 14:56:55 +0200 Subject: [PATCH 053/144] feat: add keys to handle search --- src/features/Search/Search.tsx | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index fb9e24b..0f4a8eb 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -1,4 +1,4 @@ -import { Component } from 'react'; +import { Component, createRef } from 'react'; import { ISearchContext, SearchContext } from './context/SearchProvider.tsx'; import searchIcon from '../../assets/search.svg'; @@ -13,22 +13,43 @@ class Search extends Component { declare context: ISearchContext; + inputRef = createRef(); + state = { searchQuery: '', }; componentDidMount() { + document.addEventListener('keydown', this.handleKeydown); + const storedQuery = localStorage.getItem(LOCAL_STORAGE_SEARCH_QUERY); if (storedQuery) { this.setState({ searchQuery: storedQuery }); - void this.handleSearch(storedQuery); - return; + // void this.handleSearch(storedQuery); + // return; } - void this.handleSearch(''); + // void this.handleSearch(''); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.handleKeydown); } + handleKeydown = (e: KeyboardEvent) => { + const isEnterPressed = e.key === 'Enter'; + const isInputFocus = document.activeElement === this.inputRef.current; + + if (isEnterPressed && !isInputFocus) { + this.inputRef.current?.focus(); + this.setState({ searchQuery: '' }); + } else if (isEnterPressed && isInputFocus) { + void this.handleSearch(this.state.searchQuery); + this.inputRef.current?.blur(); + } + }; + handleSearch = async (query: string) => { this.context.fetchMovies(query.trim()); localStorage.setItem(LOCAL_STORAGE_SEARCH_QUERY, query.trim()); @@ -38,6 +59,7 @@ class Search extends Component { return (
                  Date: Sun, 29 Oct 2023 15:02:35 +0200 Subject: [PATCH 054/144] feat: blur input by pressing escape --- src/features/Search/Search.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 0f4a8eb..ae66a44 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -39,12 +39,21 @@ class Search extends Component { handleKeydown = (e: KeyboardEvent) => { const isEnterPressed = e.key === 'Enter'; + const isEscPressed = e.key === 'Escape'; const isInputFocus = document.activeElement === this.inputRef.current; + const noActiveElement = document.activeElement === document.body; - if (isEnterPressed && !isInputFocus) { + if (isEscPressed && isInputFocus) { + this.inputRef.current?.blur(); + } + + if (isEnterPressed && noActiveElement) { this.inputRef.current?.focus(); this.setState({ searchQuery: '' }); - } else if (isEnterPressed && isInputFocus) { + return; + } + + if (isEnterPressed && isInputFocus) { void this.handleSearch(this.state.searchQuery); this.inputRef.current?.blur(); } From fb487ebc4e2458dfdea7314f40c9b735b5b5717a Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 15:03:02 +0200 Subject: [PATCH 055/144] feat: blur input by pressing escape --- src/features/Search/Search.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index ae66a44..9c8f8a8 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -26,11 +26,11 @@ class Search extends Component { if (storedQuery) { this.setState({ searchQuery: storedQuery }); - // void this.handleSearch(storedQuery); - // return; + void this.handleSearch(storedQuery); + return; } - // void this.handleSearch(''); + void this.handleSearch(''); } componentWillUnmount() { From 54ea534b59f42a02e4d0ff2b05d86af72c8cca4e Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 15:11:39 +0200 Subject: [PATCH 056/144] refactor: change header paddings --- src/widgets/Header/Header.tsx | 2 +- src/widgets/Header/ui/Logo.tsx | 2 +- src/widgets/Header/ui/TotalResults.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 3cf45c5..46563e7 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -32,7 +32,7 @@ class Header extends Component { return (
                  + className="sticky top-0 z-10 flex w-full flex-wrap items-center justify-between gap-x-6 gap-y-4 p-6 sm:gap-0 lg:px-12 xl:px-20"> {this.props.children}
                  ); diff --git a/src/widgets/Header/ui/Logo.tsx b/src/widgets/Header/ui/Logo.tsx index 2eb26f6..db131e0 100644 --- a/src/widgets/Header/ui/Logo.tsx +++ b/src/widgets/Header/ui/Logo.tsx @@ -5,7 +5,7 @@ import { LOGO_LETTERS } from '../const/const.ts'; class Logo extends Component { render() { return ( -

                  +

                  {LOGO_LETTERS.map((letter, i) => ( +

                  Found {this.context.totalResults}{' '} movies

                  From 9b0feab41e4f666b1f14c78b8602b1a0b6aeda61 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 15:29:04 +0200 Subject: [PATCH 057/144] refactor: encapsulate radial hover in separate function --- .eslintrc.cjs | 3 ++- src/entities/movie/ui/Movie.tsx | 28 +++++++++++--------- src/features/ProductList/ui/NotFound.tsx | 22 ++++++++------- src/shared/lib/helpers/animateRadialHover.ts | 27 +++++++++++++++++++ 4 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 src/shared/lib/helpers/animateRadialHover.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d4b2472..deec369 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -32,9 +32,10 @@ module.exports = { 'react/require-default-props': 0, 'react/state-in-constructor': ['error', 'never'], 'react/prefer-stateless-function': 0, - 'no-void': 0, 'react/react-in-jsx-scope': 0, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'no-void': 0, + 'no-param-reassign': 0, 'import/extensions': [ 'error', 'ignorePackages', diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 780e539..de85e76 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,9 +1,8 @@ import { Component, createRef, MouseEvent } from 'react'; -import colors from 'tailwindcss/colors'; - import ReactLogo from '../../../assets/reactJS-logo.png'; import { NOT_EXIST } from '../../../shared/const/const.ts'; +import createRadialHover from '../../../shared/lib/helpers/animateRadialHover.ts'; import { Movie as MovieData } from '../../../shared/types/types.ts'; import { getMovie } from '../api/apiMovie.ts'; @@ -27,6 +26,16 @@ class Movie extends Component { containerRef = createRef(); + radialHover; + + cleanUp; + + constructor(props: IMovieProps) { + super(props); + + [this.radialHover, this.cleanUp] = createRadialHover(); + } + async componentDidMount() { if (this.containerRef.current) { this.containerRef.current.style.animationDelay = `0.${String( @@ -44,20 +53,13 @@ class Movie extends Component { }); } - handleMouseMove = (e: MouseEvent) => { - const rect = this.movieRef.current?.getBoundingClientRect(); - - if (rect) { - const pointerX = e.clientX - rect.left; - const pointerY = e.clientY - rect.top; - - if (this.movieRef.current) - this.movieRef.current.style.background = `radial-gradient(circle at ${pointerX}px ${pointerY}px, rgb(112, 26, 117, 0.4) 0%, ${colors.neutral['950']} 190px)`; - } + handleMouseMove = (e: MouseEvent) => { + if (this.movieRef.current) this.radialHover(this.movieRef.current, e); }; handleMouseOut = () => { - if (this.movieRef.current) this.movieRef.current.style.background = ''; + if (this.movieRef.current) + this.cleanUp(this.movieRef.current as HTMLDivElement); }; render() { diff --git a/src/features/ProductList/ui/NotFound.tsx b/src/features/ProductList/ui/NotFound.tsx index 48c4699..55b5b69 100644 --- a/src/features/ProductList/ui/NotFound.tsx +++ b/src/features/ProductList/ui/NotFound.tsx @@ -1,24 +1,26 @@ import { Component, createRef, MouseEvent } from 'react'; -import colors from 'tailwindcss/colors'; +import createRadialHover from '../../../shared/lib/helpers/animateRadialHover.ts'; class NotFound extends Component { movieRef = createRef(); - handleMouseMove = (e: MouseEvent) => { - const rect = this.movieRef.current?.getBoundingClientRect(); + radialHover; - if (rect) { - const pointerX = e.clientX - rect.left; - const pointerY = e.clientY - rect.top; + cleanUp; - if (this.movieRef.current) - this.movieRef.current.style.background = `radial-gradient(circle at ${pointerX}px ${pointerY}px, rgb(112, 26, 117, 0.5) 0%, ${colors.transparent} 160px)`; - } + constructor(props: NonNullable) { + super(props); + + [this.radialHover, this.cleanUp] = createRadialHover(); + } + + handleMouseMove = (e: MouseEvent) => { + if (this.movieRef.current) this.radialHover(this.movieRef.current, e); }; handleMouseOut = () => { - if (this.movieRef.current) this.movieRef.current.style.background = ''; + if (this.movieRef.current) this.cleanUp(this.movieRef.current); }; render() { diff --git a/src/shared/lib/helpers/animateRadialHover.ts b/src/shared/lib/helpers/animateRadialHover.ts new file mode 100644 index 0000000..4137791 --- /dev/null +++ b/src/shared/lib/helpers/animateRadialHover.ts @@ -0,0 +1,27 @@ +import { MouseEvent } from 'react'; + +import colors from 'tailwindcss/colors'; + +function animateRadialHover(elem: HTMLElement, evt: MouseEvent) { + const rect = elem.getBoundingClientRect(); + + if (rect) { + const pointerX = evt.clientX - rect.left; + const pointerY = evt.clientY - rect.top; + + elem.style.background = `radial-gradient(circle at ${pointerX}px ${pointerY}px, rgb(112, 26, 117, 0.5) 0%, ${colors.transparent} 160px)`; + } +} + +function cleanUp(elem: HTMLElement) { + elem.style.background = ''; +} + +function createRadialHover(): [ + (elem: HTMLElement, evt: MouseEvent) => void, + (elem: HTMLElement) => void, +] { + return [animateRadialHover, cleanUp]; +} + +export default createRadialHover; From 7770a52be3ab1fc1ae2b2a526f791bdee58907d7 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 15:33:50 +0200 Subject: [PATCH 058/144] refactor: add readable types --- src/shared/lib/helpers/animateRadialHover.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/shared/lib/helpers/animateRadialHover.ts b/src/shared/lib/helpers/animateRadialHover.ts index 4137791..f98aabe 100644 --- a/src/shared/lib/helpers/animateRadialHover.ts +++ b/src/shared/lib/helpers/animateRadialHover.ts @@ -2,6 +2,12 @@ import { MouseEvent } from 'react'; import colors from 'tailwindcss/colors'; +type AnimationFn = (elem: HTMLElement, evt: MouseEvent) => void; + +type CleanUpFn = (elem: HTMLElement) => void; + +type RadialHover = [AnimationFn, CleanUpFn]; + function animateRadialHover(elem: HTMLElement, evt: MouseEvent) { const rect = elem.getBoundingClientRect(); @@ -17,10 +23,7 @@ function cleanUp(elem: HTMLElement) { elem.style.background = ''; } -function createRadialHover(): [ - (elem: HTMLElement, evt: MouseEvent) => void, - (elem: HTMLElement) => void, -] { +function createRadialHover(): RadialHover { return [animateRadialHover, cleanUp]; } From 500c38cde69c336f5c171e3cd78036f12051427e Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 16:56:44 +0200 Subject: [PATCH 059/144] chore: add custom scroll library --- package-lock.json | 16 ++++++++++++++++ package.json | 2 ++ 2 files changed, 18 insertions(+) diff --git a/package-lock.json b/package-lock.json index 53c85b5..c86d34a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "react-ts-tw-template", "version": "0.0.0", "dependencies": { + "overlayscrollbars": "^2.4.4", + "overlayscrollbars-react": "^0.5.3", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -4603,6 +4605,20 @@ "node": ">= 0.8.0" } }, + "node_modules/overlayscrollbars": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.4.4.tgz", + "integrity": "sha512-792lwASLr3FlZER+/P7NseFQjffDEcQOg6HtyBSLrnb3crH+Ybk0tzaljQVQZs0pjGF/xFjyvMKin6whkL0RnQ==" + }, + "node_modules/overlayscrollbars-react": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.3.tgz", + "integrity": "sha512-mq9D9tbfSeq0cti1kKMf3B3AzsEGwHcRIDX/K49CvYkHz/tKeU38GiahDkIPKTMEAp6lzKCo4x1eJZA6ZFYOxQ==", + "peerDependencies": { + "overlayscrollbars": "^2.0.0", + "react": ">=16.8.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", diff --git a/package.json b/package.json index 8b84163..b8d3463 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "precommit": "pnpm lint-staged && pnpm type-check" }, "dependencies": { + "overlayscrollbars": "^2.4.4", + "overlayscrollbars-react": "^0.5.3", "react": "^18.2.0", "react-dom": "^18.2.0" }, From e452955e74873e5ce927ba3cd356a5bc2766144e Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 16:57:54 +0200 Subject: [PATCH 060/144] feat: add nice scroll bar --- src/main.tsx | 4 +++- src/pages/MainLayout.tsx | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 3286290..208f562 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,12 @@ +import 'overlayscrollbars/overlayscrollbars.css'; import React from 'react'; import ReactDOM from 'react-dom/client'; -import './index.css'; import App from './app/App.tsx'; +import './index.css'; + ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 0488b39..a827bd6 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,5 +1,7 @@ import { Component } from 'react'; +import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; + import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/ProductList/MovieList.tsx'; import Search from '../features/Search/Search.tsx'; @@ -12,7 +14,16 @@ import Main from '../widgets/Main/Main.tsx'; class MainLayout extends Component { render() { return ( - <> +
                  @@ -29,7 +40,7 @@ class MainLayout extends Component {
                  - +
                  ); } } From 81d5d75b4f1e7a1751aecc4e0a6dbb64e557e175 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 17:18:06 +0200 Subject: [PATCH 061/144] feat: add initial animation --- src/features/ProductList/MovieList.tsx | 2 +- src/features/Search/Search.tsx | 2 +- src/index.css | 27 ++++++++++++++++---------- src/widgets/Header/ui/TotalResults.tsx | 2 +- tailwind.config.js | 4 ++-- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index c1efb71..5ca543e 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -25,7 +25,7 @@ class MovieList extends Component { if (isNoMovies) return null; return ( -
                    +
                      {movies.map((movie, i) => ( ))} diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 9c8f8a8..74d28b0 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -66,7 +66,7 @@ class Search extends Component { render() { return ( -
                      +
                      +

                      Found {this.context.totalResults}{' '} movies

                      diff --git a/tailwind.config.js b/tailwind.config.js index 9ad338f..b31539a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -53,11 +53,11 @@ export default { }, }, animation: { - 'fade-in': 'fade-in .8s cubic-bezier(0.86, 0, 0.07, 1) both', + 'fade-in': 'fade-in .5s cubic-bezier(0.86, 0, 0.07, 1) both', springish: 'springish 1.72s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', 'springish-letter': - 'springish-letter 1.55s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', + 'springish-letter 1.8s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', float: 'float 20s ease-in-out infinite', }, }, From 18a429a02c8c3c3bed969e0ab13f381a80c8cceb Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 17:22:51 +0200 Subject: [PATCH 062/144] fix: header blur --- src/widgets/Header/Header.tsx | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 46563e7..63e7cec 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -1,38 +1,11 @@ -import { Component, createRef } from 'react'; +import { Component } from 'react'; -import { SCROLL_THRESHOLD } from './const/const.ts'; import { IChildren } from '../../shared/types/interfaces.ts'; class Header extends Component { - containerRef = createRef(); - - componentDidMount() { - window.addEventListener('scroll', this.handleScroll); - } - - componentWillUnmount() { - window.removeEventListener('scroll', this.handleScroll); - } - - handleScroll = () => { - if (!this.containerRef.current) return; - - const isThresholdScrolled = - document.body.scrollTop > SCROLL_THRESHOLD || - document.documentElement.scrollTop > SCROLL_THRESHOLD; - - if (isThresholdScrolled) { - this.containerRef.current.classList.add('backdrop-blur-xl'); - } else { - this.containerRef.current.classList.remove('backdrop-blur-xl'); - } - }; - render() { return ( -
                      +
                      {this.props.children}
                      ); From 3e995d0b2336efdf81d53497fdc13aba6602b552 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 18:03:02 +0200 Subject: [PATCH 063/144] refactor: change initial load animation --- src/features/ProductList/MovieList.tsx | 2 +- src/features/Search/Search.tsx | 2 +- src/widgets/Header/Header.tsx | 2 +- src/widgets/Header/ui/TotalResults.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index 5ca543e..a913107 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -25,7 +25,7 @@ class MovieList extends Component { if (isNoMovies) return null; return ( -
                        +
                          {movies.map((movie, i) => ( ))} diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 74d28b0..149d657 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -66,7 +66,7 @@ class Search extends Component { render() { return ( -
                          +
                          { render() { return ( -
                          +
                          {this.props.children}
                          ); diff --git a/src/widgets/Header/ui/TotalResults.tsx b/src/widgets/Header/ui/TotalResults.tsx index d23f23a..e100271 100644 --- a/src/widgets/Header/ui/TotalResults.tsx +++ b/src/widgets/Header/ui/TotalResults.tsx @@ -12,7 +12,7 @@ class TotalResults extends Component { render() { return ( -

                          +

                          Found {this.context.totalResults}{' '} movies

                          From 23b7ee2209bb973e911ab018f1ecfa3fb28daa7c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 18:46:20 +0200 Subject: [PATCH 064/144] feat: add error boundary fall back ui test --- src/app/App.tsx | 6 +-- src/features/ProductList/ui/NotFound.tsx | 45 ++++-------------- src/features/Search/Search.tsx | 20 ++++---- src/pages/MainLayout.tsx | 19 +++++++- src/shared/ui/Button.tsx | 33 +++++++++++++ .../ui/ErrorBoundary.tsx | 2 +- src/shared/ui/FallbackUi.tsx | 23 +++++++++ src/shared/ui/Modal.tsx | 47 +++++++++++++++++++ 8 files changed, 145 insertions(+), 50 deletions(-) create mode 100644 src/shared/ui/Button.tsx rename src/{features/ProductList => shared}/ui/ErrorBoundary.tsx (88%) create mode 100644 src/shared/ui/FallbackUi.tsx create mode 100644 src/shared/ui/Modal.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index c9e3d5f..1e7f0f5 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,14 +1,14 @@ import { Component } from 'react'; -import ErrorBoundary from '../features/ProductList/ui/ErrorBoundary.tsx'; import SearchProvider from '../features/Search/context/SearchProvider.tsx'; import MainLayout from '../pages/MainLayout.tsx'; +import ErrorBoundary from '../shared/ui/ErrorBoundary.tsx'; +import FallbackUi from '../shared/ui/FallbackUi.tsx'; class App extends Component { render() { return ( - Something went wrong

                          }> + }> diff --git a/src/features/ProductList/ui/NotFound.tsx b/src/features/ProductList/ui/NotFound.tsx index 55b5b69..7a2fa6f 100644 --- a/src/features/ProductList/ui/NotFound.tsx +++ b/src/features/ProductList/ui/NotFound.tsx @@ -1,44 +1,17 @@ -import { Component, createRef, MouseEvent } from 'react'; +import { Component } from 'react'; -import createRadialHover from '../../../shared/lib/helpers/animateRadialHover.ts'; +import Modal from '../../../shared/ui/Modal.tsx'; class NotFound extends Component { - movieRef = createRef(); - - radialHover; - - cleanUp; - - constructor(props: NonNullable) { - super(props); - - [this.radialHover, this.cleanUp] = createRadialHover(); - } - - handleMouseMove = (e: MouseEvent) => { - if (this.movieRef.current) this.radialHover(this.movieRef.current, e); - }; - - handleMouseOut = () => { - if (this.movieRef.current) this.cleanUp(this.movieRef.current); - }; - render() { return ( -
                          -
                          - - No Movies Found. 🥺 - -

                          You're search did not match any movies.

                          -

                          Please try again. 👉👈

                          -
                          -
                          + + + No Movies Found. 🥺 + +

                          You're search did not match any movies.

                          +

                          Please try again. 👉👈

                          +
                          ); } } diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 149d657..110145c 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -3,6 +3,7 @@ import { Component, createRef } from 'react'; import { ISearchContext, SearchContext } from './context/SearchProvider.tsx'; import searchIcon from '../../assets/search.svg'; import { LOCAL_STORAGE_SEARCH_QUERY } from '../../shared/const/const.ts'; +import Button from '../../shared/ui/Button.tsx'; interface ISearchState { searchQuery: string; @@ -26,11 +27,11 @@ class Search extends Component { if (storedQuery) { this.setState({ searchQuery: storedQuery }); - void this.handleSearch(storedQuery); - return; + // void this.handleSearch(storedQuery); + // return; } - void this.handleSearch(''); + // void this.handleSearch(''); } componentWillUnmount() { @@ -75,13 +76,14 @@ class Search extends Component { value={this.state.searchQuery} onChange={(e) => this.setState({ searchQuery: e.target.value })} /> - + className="absolute bottom-0 right-0 top-0 m-auto flex scale-105 items-center gap-2 active:scale-105 active:duration-75 peer-focus:-translate-y-0.5"> + <> + + Search + +
                          ); } diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index a827bd6..0720f02 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -5,14 +5,25 @@ import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/ProductList/MovieList.tsx'; import Search from '../features/Search/Search.tsx'; +import Button from '../shared/ui/Button.tsx'; import Spinner from '../shared/ui/Spinner.tsx'; import Header from '../widgets/Header/Header.tsx'; import Logo from '../widgets/Header/ui/Logo.tsx'; import TotalResults from '../widgets/Header/ui/TotalResults.tsx'; import Main from '../widgets/Main/Main.tsx'; -class MainLayout extends Component { +interface IMainLayoutState { + isError: boolean; +} + +class MainLayout extends Component { + state = { + isError: false, + }; + render() { + if (this.state.isError) throw new Error('test error'); + return ( +
                      diff --git a/src/shared/ui/Button.tsx b/src/shared/ui/Button.tsx new file mode 100644 index 0000000..539c446 --- /dev/null +++ b/src/shared/ui/Button.tsx @@ -0,0 +1,33 @@ +import { Component } from 'react'; + +import { IChildren } from '../types/interfaces.ts'; + +const buttonTypes = { + filled: + 'rounded-full bg-lime-400 px-4 py-3 font-semibold text-gray-950 transition-all duration-200 hover:scale-110 focus:outline-0 focus:ring focus:ring-lime-300 focus:ring-offset-2 focus:ring-offset-black/70 sm:px-6', + empty: + 'border border-lime-300 rounded-full px-4 py-3 font-semibold text-lime-300 transition-all duration-200 hover:bg-lime-200/30 focus:outline-0 focus:ring focus:ring-lime-300 focus:ring-offset-2 focus:ring-offset-black/70 sm:px-6', +}; + +interface IButtonProps extends IChildren { + onClick?: () => void; + type?: keyof typeof buttonTypes; + className?: string; +} + +class Button extends Component { + render() { + return ( + + ); + } +} + +export default Button; diff --git a/src/features/ProductList/ui/ErrorBoundary.tsx b/src/shared/ui/ErrorBoundary.tsx similarity index 88% rename from src/features/ProductList/ui/ErrorBoundary.tsx rename to src/shared/ui/ErrorBoundary.tsx index af12522..9a63593 100644 --- a/src/features/ProductList/ui/ErrorBoundary.tsx +++ b/src/shared/ui/ErrorBoundary.tsx @@ -1,6 +1,6 @@ import { Component, ReactNode } from 'react'; -import { IChildren } from '../../../shared/types/interfaces.ts'; +import { IChildren } from '../types/interfaces.ts'; interface IErrorBoundaryState { hasError?: boolean; diff --git a/src/shared/ui/FallbackUi.tsx b/src/shared/ui/FallbackUi.tsx new file mode 100644 index 0000000..a43b265 --- /dev/null +++ b/src/shared/ui/FallbackUi.tsx @@ -0,0 +1,23 @@ +import { Component } from 'react'; + +import Modal from './Modal.tsx'; + +class FallbackUi extends Component { + render() { + return ( +
                      + +

                      + Something went really wrong 😱 +

                      +

                      If you faced any issues, please contact our support team

                      + + cinemania-help@gmail.com + +
                      +
                      + ); + } +} + +export default FallbackUi; diff --git a/src/shared/ui/Modal.tsx b/src/shared/ui/Modal.tsx new file mode 100644 index 0000000..763061c --- /dev/null +++ b/src/shared/ui/Modal.tsx @@ -0,0 +1,47 @@ +import { Component, createRef, MouseEvent } from 'react'; + +import createRadialHover from '../lib/helpers/animateRadialHover.ts'; +import { IChildren } from '../types/interfaces.ts'; + +interface IModalProps extends IChildren { + className?: string; +} + +class Modal extends Component { + modalRef = createRef(); + + radialHover; + + cleanUp; + + constructor(props: IModalProps) { + super(props); + + [this.radialHover, this.cleanUp] = createRadialHover(); + } + + handleMouseMove = (e: MouseEvent) => { + if (this.modalRef.current) this.radialHover(this.modalRef.current, e); + }; + + handleMouseOut = () => { + if (this.modalRef.current) this.cleanUp(this.modalRef.current); + }; + + render() { + return ( +
                      +
                      + {this.props.children} +
                      +
                      + ); + } +} + +export default Modal; From f835fe7783beb588d3b61c64ebaf08668033e6d4 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 18:54:53 +0200 Subject: [PATCH 065/144] fix: adaptive issues --- src/shared/ui/FallbackUi.tsx | 2 +- src/shared/ui/Modal.tsx | 4 +++- src/widgets/Header/Header.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/shared/ui/FallbackUi.tsx b/src/shared/ui/FallbackUi.tsx index a43b265..16483d7 100644 --- a/src/shared/ui/FallbackUi.tsx +++ b/src/shared/ui/FallbackUi.tsx @@ -5,7 +5,7 @@ import Modal from './Modal.tsx'; class FallbackUi extends Component { render() { return ( -
                      +

                      Something went really wrong 😱 diff --git a/src/shared/ui/Modal.tsx b/src/shared/ui/Modal.tsx index 763061c..1c8cc0d 100644 --- a/src/shared/ui/Modal.tsx +++ b/src/shared/ui/Modal.tsx @@ -36,7 +36,9 @@ class Modal extends Component { onMouseMove={this.handleMouseMove} onMouseOut={this.handleMouseOut} onBlur={this.handleMouseOut} - className={`h-full w-full px-6 py-10 ${this.props.className ?? ''} `}> + className={`h-full w-full px-2 py-8 md:px-6 md:py-10 ${ + this.props.className ?? '' + }`}> {this.props.children}

                      diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 03cb4ff..b8e6c92 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -5,7 +5,7 @@ import { IChildren } from '../../shared/types/interfaces.ts'; class Header extends Component { render() { return ( -
                      +
                      {this.props.children}
                      ); From a87deb5e121b9b458e572dd85aac93f6e9fa71b4 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 23:38:20 +0200 Subject: [PATCH 066/144] chore: add locomotive scroll --- package-lock.json | 79 ++++++++++++++++++++++++----------------------- package.json | 3 +- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index c86d34a..c2ce816 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,7 @@ "name": "react-ts-tw-template", "version": "0.0.0", "dependencies": { - "overlayscrollbars": "^2.4.4", - "overlayscrollbars-react": "^0.5.3", + "locomotive-scroll": "^5.0.0-beta.8", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -802,9 +801,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", - "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -1005,6 +1004,11 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@studio-freight/lenis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@studio-freight/lenis/-/lenis-1.0.12.tgz", + "integrity": "sha512-iN3JQ6qJVc+L7vh8NjlDibw9dvjjifvlJ+s0UAkfwty+vW9MJxRfszb5X/eAhcbDLZW6MlvjRHoH9WSQH3J8eA==" + }, "node_modules/@types/babel__core": { "version": "7.20.3", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", @@ -1065,9 +1069,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", - "integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", + "version": "18.2.33", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz", + "integrity": "sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -1311,9 +1315,9 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1773,9 +1777,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001553", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz", - "integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==", + "version": "1.0.30001557", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001557.tgz", + "integrity": "sha512-91oR7hLNUP3gG6MLU+n96em322a8Xzes8wWdBKhLgUoiJsAF5irZnxSUCbc+qUZXNnPCfUwLOi9ZCZpkvjQajw==", "dev": true, "funding": [ { @@ -2164,9 +2168,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.565", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.565.tgz", - "integrity": "sha512-XbMoT6yIvg2xzcbs5hCADi0dXBh4//En3oFXmtPX+jiyyiCTiM9DGFT2SLottjpEs9Z8Mh8SqahbR96MaHfuSg==", + "version": "1.4.569", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.569.tgz", + "integrity": "sha512-LsrJjZ0IbVy12ApW3gpYpcmHS3iRxH4bkKOW98y1/D+3cvDUWGcbzbsFinfUS8knpcZk/PG/2p/RnkMCYN7PVg==", "dev": true }, "node_modules/emoji-regex": { @@ -2732,9 +2736,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.3.tgz", - "integrity": "sha512-Hh0wv8bUNY877+sI0BlCUlsS0TYYQqvzEwJsJJPM2WF4RnTStSnSR3zdJYa2nPOJgg3UghXi54lVyMSmpCalzA==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.4.tgz", + "integrity": "sha512-eD83+65e8YPVg6603Om2iCIwcQJf/y7++MWm4tACtEswFLYMwxwVWAfwN+e19f5Ad/FOyyNg9Dfi5lXhH3Y3rA==", "dev": true, "peerDependencies": { "eslint": ">=7" @@ -4194,6 +4198,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/locomotive-scroll": { + "version": "5.0.0-beta.8", + "resolved": "https://registry.npmjs.org/locomotive-scroll/-/locomotive-scroll-5.0.0-beta.8.tgz", + "integrity": "sha512-4Nq12voKr5719aaySZg3FX/gQKQIAaAC4NkBiU27aKEKFnYcTeOB1r8VO5bGpEIOGQo94wsiJQNZuRb3TsxjHA==", + "dependencies": { + "@studio-freight/lenis": "1.0.12" + }, + "engines": { + "node": ">=17" + } + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4605,20 +4620,6 @@ "node": ">= 0.8.0" } }, - "node_modules/overlayscrollbars": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.4.4.tgz", - "integrity": "sha512-792lwASLr3FlZER+/P7NseFQjffDEcQOg6HtyBSLrnb3crH+Ybk0tzaljQVQZs0pjGF/xFjyvMKin6whkL0RnQ==" - }, - "node_modules/overlayscrollbars-react": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.3.tgz", - "integrity": "sha512-mq9D9tbfSeq0cti1kKMf3B3AzsEGwHcRIDX/K49CvYkHz/tKeU38GiahDkIPKTMEAp6lzKCo4x1eJZA6ZFYOxQ==", - "peerDependencies": { - "overlayscrollbars": "^2.0.0", - "react": ">=16.8.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5855,9 +5856,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", - "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", + "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -5865,10 +5866,10 @@ "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.18.2", + "jiti": "^1.19.1", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", diff --git a/package.json b/package.json index b8d3463..3982ca2 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,7 @@ "precommit": "pnpm lint-staged && pnpm type-check" }, "dependencies": { - "overlayscrollbars": "^2.4.4", - "overlayscrollbars-react": "^0.5.3", + "locomotive-scroll": "^5.0.0-beta.8", "react": "^18.2.0", "react-dom": "^18.2.0" }, From 3a39db1a468bfd41925cc69834d32b2403c2e3fd Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 23:40:40 +0200 Subject: [PATCH 067/144] feat: add smooth scroll --- src/features/Search/Search.tsx | 6 +++--- src/index.css | 15 +++++++++++++++ src/main.tsx | 1 - src/pages/MainLayout.tsx | 24 ++++++++++++------------ src/shared/ui/FallbackUi.tsx | 4 ++-- 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 110145c..b8c7557 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -27,11 +27,11 @@ class Search extends Component { if (storedQuery) { this.setState({ searchQuery: storedQuery }); - // void this.handleSearch(storedQuery); - // return; + void this.handleSearch(storedQuery); + return; } - // void this.handleSearch(''); + void this.handleSearch(''); } componentWillUnmount() { diff --git a/src/index.css b/src/index.css index df32f2e..7f791b5 100644 --- a/src/index.css +++ b/src/index.css @@ -8,6 +8,21 @@ background-color: theme(colors.lime.50); } + ::-webkit-scrollbar { + width: 16px; + height: 16px; + } + + ::-webkit-scrollbar-track { + background: theme(colors.neutral.900); + } + + ::-webkit-scrollbar-thumb { + background: theme(colors.neutral.500); + border-radius: 999px; + border: 4px solid theme(colors.neutral.900); + } + * { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; diff --git a/src/main.tsx b/src/main.tsx index 208f562..531f86a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,4 +1,3 @@ -import 'overlayscrollbars/overlayscrollbars.css'; import React from 'react'; import ReactDOM from 'react-dom/client'; diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 0720f02..e12e0e7 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,6 +1,6 @@ import { Component } from 'react'; -import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; +import LocomotiveScroll from 'locomotive-scroll'; import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/ProductList/MovieList.tsx'; @@ -21,20 +21,20 @@ class MainLayout extends Component { isError: false, }; + componentDidMount() { + // eslint-disable-next-line no-new + new LocomotiveScroll({ + lenisOptions: { + duration: 0.55, + }, + }); + } + render() { if (this.state.isError) throw new Error('test error'); return ( - + <>
                      @@ -57,7 +57,7 @@ class MainLayout extends Component {
                      -
                      + ); } } diff --git a/src/shared/ui/FallbackUi.tsx b/src/shared/ui/FallbackUi.tsx index 16483d7..bf1b386 100644 --- a/src/shared/ui/FallbackUi.tsx +++ b/src/shared/ui/FallbackUi.tsx @@ -5,7 +5,7 @@ import Modal from './Modal.tsx'; class FallbackUi extends Component { render() { return ( -
                      +

                      Something went really wrong 😱 @@ -15,7 +15,7 @@ class FallbackUi extends Component { cinemania-help@gmail.com -

                      + ); } } From c2e0f2176855c2fad317ab4a9ea1e64f169ca928 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 29 Oct 2023 23:56:27 +0200 Subject: [PATCH 068/144] refactor: change logo animation timings --- tailwind.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailwind.config.js b/tailwind.config.js index b31539a..fec4f20 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -57,7 +57,7 @@ export default { springish: 'springish 1.72s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', 'springish-letter': - 'springish-letter 1.8s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', + 'springish-letter 2s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', float: 'float 20s ease-in-out infinite', }, }, From 9ec3c241bb8681917cbcc3be80f28fdea89004f9 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 00:01:28 +0200 Subject: [PATCH 069/144] chore: add base path --- vite.config.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 5a33944..86a5fa0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + base: './', +}); From e15911ac7a217a8ab989fed61669f466713277ac Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 15:37:47 +0200 Subject: [PATCH 070/144] refactor: move inline style to render --- src/entities/movie/ui/Movie.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index de85e76..9b133de 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -37,12 +37,6 @@ class Movie extends Component { } async componentDidMount() { - if (this.containerRef.current) { - this.containerRef.current.style.animationDelay = `0.${String( - this.props.delay, - )}s`; - } - const movieData = await getMovie(this.props.data.imdbID); const genre = movieData.Genre.split(', ').slice(0, 2).join('/'); const description = `${movieData.Plot.slice(0, 38)}...`; @@ -71,6 +65,9 @@ class Movie extends Component { return (
                    • Date: Mon, 30 Oct 2023 15:39:48 +0200 Subject: [PATCH 071/144] refactor: remove magic string --- src/entities/movie/ui/Movie.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 9b133de..b4347db 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -62,11 +62,12 @@ class Movie extends Component { const isPosterExist = Poster !== NOT_EXIST; const poster = isPosterExist ? Poster : ReactLogo; + const animationDelay = `0.${String(this.props.delay)}s`; return (
                    • From e97e9ae74b1ea7f63d8edc53589bf6e8ef4f14ab Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 15:44:09 +0200 Subject: [PATCH 072/144] fix: log error information in console --- src/shared/ui/ErrorBoundary.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/shared/ui/ErrorBoundary.tsx b/src/shared/ui/ErrorBoundary.tsx index 9a63593..98f8a08 100644 --- a/src/shared/ui/ErrorBoundary.tsx +++ b/src/shared/ui/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import { Component, ReactNode } from 'react'; +import { Component, ErrorInfo, ReactNode } from 'react'; import { IChildren } from '../types/interfaces.ts'; @@ -22,6 +22,11 @@ class ErrorBoundary extends Component< hasError: true, }); + componentDidCatch(error: Error, info: ErrorInfo) { + // eslint-disable-next-line no-console + console.log('Error report =', error, info.componentStack); + } + render() { if (this.state.hasError) return this.props.fallback; From 6e3a50e7e9a1aed68f35b79680bc8a83fb617a12 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 17:12:22 +0200 Subject: [PATCH 073/144] refactor: change app component to functional --- src/app/App.tsx | 20 ++++++++------------ src/pages/MainLayout.tsx | 11 ----------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 1e7f0f5..2a6ed48 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,20 +1,16 @@ -import { Component } from 'react'; - import SearchProvider from '../features/Search/context/SearchProvider.tsx'; import MainLayout from '../pages/MainLayout.tsx'; import ErrorBoundary from '../shared/ui/ErrorBoundary.tsx'; import FallbackUi from '../shared/ui/FallbackUi.tsx'; -class App extends Component { - render() { - return ( - }> - - - - - ); - } +function App() { + return ( + }> + + + + + ); } export default App; diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index e12e0e7..ef4035a 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,7 +1,5 @@ import { Component } from 'react'; -import LocomotiveScroll from 'locomotive-scroll'; - import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/ProductList/MovieList.tsx'; import Search from '../features/Search/Search.tsx'; @@ -21,15 +19,6 @@ class MainLayout extends Component { isError: false, }; - componentDidMount() { - // eslint-disable-next-line no-new - new LocomotiveScroll({ - lenisOptions: { - duration: 0.55, - }, - }); - } - render() { if (this.state.isError) throw new Error('test error'); From e83a91da6609ffa27266326ee0a07dd19b304bba Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 17:15:00 +0200 Subject: [PATCH 074/144] refactor: change main layout component to functional --- src/pages/MainLayout.tsx | 72 ++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index ef4035a..c94a977 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,4 +1,4 @@ -import { Component } from 'react'; +import { useState } from 'react'; import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/ProductList/MovieList.tsx'; @@ -10,45 +10,37 @@ import Logo from '../widgets/Header/ui/Logo.tsx'; import TotalResults from '../widgets/Header/ui/TotalResults.tsx'; import Main from '../widgets/Main/Main.tsx'; -interface IMainLayoutState { - isError: boolean; -} - -class MainLayout extends Component { - state = { - isError: false, - }; - - render() { - if (this.state.isError) throw new Error('test error'); - - return ( - <> - - -
                      - - -
                      -
                      - - - - -
                      -
                      - -
                      -
                      -
                      - - ); - } +function MainLayout() { + const [isError, setIsError] = useState(false); + + if (isError) throw new Error('test error'); + + return ( + <> + + +
                      + + +
                      +
                      + + + + +
                      +
                      + +
                      +
                      +
                      + + ); } export default MainLayout; From 7964e0110de524e705d8ca5a7604fdaeef0c1c58 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 17:57:18 +0200 Subject: [PATCH 075/144] chore: change eslint config to fit function components --- .eslintrc.cjs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index deec369..f624073 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -24,14 +24,6 @@ module.exports = { sourceType: 'module', }, rules: { - // TODO: remove 👇 after 1 module - 'react/jsx-no-constructed-context-values': 0, - 'import/no-extraneous-dependencies': 0, - 'react/static-property-placement': 0, - 'react/destructuring-assignment': 0, - 'react/require-default-props': 0, - 'react/state-in-constructor': ['error', 'never'], - 'react/prefer-stateless-function': 0, 'react/react-in-jsx-scope': 0, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 'no-void': 0, From 31e0855b3494bbe5b0a822ff29d6a23e0fb47b13 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:01:59 +0200 Subject: [PATCH 076/144] refactor: change Search provider to functional components --- src/features/Search/const/const.ts | 4 + .../Search/context/SearchProvider.tsx | 170 +++++++++++------- src/features/Search/types/types.ts | 38 ++++ 3 files changed, 144 insertions(+), 68 deletions(-) create mode 100644 src/features/Search/const/const.ts create mode 100644 src/features/Search/types/types.ts diff --git a/src/features/Search/const/const.ts b/src/features/Search/const/const.ts new file mode 100644 index 0000000..e4cb963 --- /dev/null +++ b/src/features/Search/const/const.ts @@ -0,0 +1,4 @@ +import { MovieList } from '../../../shared/types/types.ts'; + +export const NO_MOVIES: MovieList = []; +export const NO_RESULTS = 0; diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx index 8f3264d..0b71d8b 100644 --- a/src/features/Search/context/SearchProvider.tsx +++ b/src/features/Search/context/SearchProvider.tsx @@ -1,82 +1,116 @@ -import { Component, createContext } from 'react'; +import { + createContext, + useCallback, + useContext, + useMemo, + useReducer, +} from 'react'; import { getMovieList } from '../../../entities/movie/api/apiMovie.ts'; import { IChildren } from '../../../shared/types/interfaces.ts'; import { MovieList } from '../../../shared/types/types.ts'; +import { NO_MOVIES, NO_RESULTS } from '../const/const.ts'; +import { + Action, + IInitialState, + ISearchContext, + SearchActions, +} from '../types/types.ts'; -interface ISearchProviderState { - query: string; - movies: MovieList | null; - totalResults: number; - isLoading: boolean; -} - -export interface ISearchContext extends ISearchProviderState { - updateQuery: (newQuery: string) => void; - updateMovies: (newMovies: MovieList, totalResults: number) => void; - updateIsLoading: (isLoading: boolean) => void; - fetchMovies: (searchQuery: string) => void; -} - -const NO_RESULTS = 0; -const NO_MOVIES: MovieList = []; +const initialState: IInitialState = { + query: '', + movies: null, + totalResults: 0, + isLoading: false, +}; export const SearchContext = createContext({ query: '', } as ISearchContext); -class SearchProvider extends Component { - state = { - query: '', - movies: null, - totalResults: 0, - isLoading: false, - }; - - updateQuery = (newQuery: string) => { - this.setState({ query: newQuery }); - }; - - updateMovies = (newMovies: MovieList, totalResults: number) => { - this.setState({ movies: newMovies, totalResults }); - }; - - updateIsLoading = (isLoading: boolean) => { - this.setState({ isLoading }); - }; - - fetchMovies = async (searchQuery: string) => { - try { - this.updateIsLoading(true); - const res = await getMovieList(searchQuery); - this.updateMovies(res.Search, Number(res.totalResults)); - } catch (e) { - this.updateMovies(NO_MOVIES, NO_RESULTS); - } finally { - this.updateIsLoading(false); - } - }; - - render() { - const { query, movies, totalResults, isLoading } = this.state; - const { updateQuery, updateMovies, updateIsLoading, fetchMovies } = this; - - return ( - - {this.props.children} - - ); +function reducer(state: IInitialState, action: Action): IInitialState { + switch (action.type) { + case SearchActions.QUERY_UPDATED: + return { ...state, query: action.payload }; + + case SearchActions.MOVIES_LOADED: + return { + ...state, + movies: action.payload.movies, + totalResults: action.payload.totalResults, + isLoading: false, + }; + + case SearchActions.LOADING: + return { ...state, isLoading: true }; + + default: + throw new Error('The action does not exist!'); } } +function SearchProvider({ children }: IChildren) { + const [{ query, movies, totalResults, isLoading }, dispatch] = useReducer( + reducer, + initialState, + ); + + const updateQuery = useCallback((newQuery: string) => { + dispatch({ type: SearchActions.QUERY_UPDATED, payload: newQuery }); + }, []); + + const updateMovies = useCallback( + (newMovies: MovieList, newTotalResults: number) => { + dispatch({ + type: SearchActions.MOVIES_LOADED, + payload: { + movies: newMovies, + totalResults: newTotalResults, + }, + }); + }, + [], + ); + + const fetchMovies = useCallback( + async (searchQuery: string) => { + try { + dispatch({ type: SearchActions.LOADING }); + const res = await getMovieList(searchQuery); + updateMovies(res.Search, Number(res.totalResults)); + } catch (e) { + updateMovies(NO_MOVIES, NO_RESULTS); + } + }, + [updateMovies], + ); + + const providerValue = useMemo( + () => ({ + query, + movies, + totalResults, + isLoading, + updateQuery, + fetchMovies, + }), + [fetchMovies, isLoading, movies, query, totalResults, updateQuery], + ); + + return ( + + {children} + + ); +} + +export function useSearch() { + const context = useContext(SearchContext); + + if (context === undefined) + throw new Error('SearchContext is used outside the SearchProvider!'); + + return context; +} + export default SearchProvider; diff --git a/src/features/Search/types/types.ts b/src/features/Search/types/types.ts new file mode 100644 index 0000000..26f48a9 --- /dev/null +++ b/src/features/Search/types/types.ts @@ -0,0 +1,38 @@ +import { MovieList } from '../../../shared/types/types.ts'; + +export enum SearchActions { + QUERY_UPDATED = 'search/queryUpdated', + MOVIES_LOADED = 'search/moviesLoaded', + LOADING = 'search/loading', +} + +export interface IInitialState { + query: string; + movies: MovieList | null; + totalResults: number; + isLoading: boolean; +} + +export interface ISearchContext extends IInitialState { + updateQuery: (newQuery: string) => void; + fetchMovies: (searchQuery: string) => void; +} + +export interface IQueryAction { + type: SearchActions.QUERY_UPDATED; + payload: string; +} + +export interface IMoviesAction { + type: SearchActions.MOVIES_LOADED; + payload: { + movies: MovieList | null; + totalResults: number; + }; +} + +export interface ILoadingAction { + type: SearchActions.LOADING; +} + +export type Action = IQueryAction | IMoviesAction | ILoadingAction; From 883dae950772d137bbab02bbd13f9d1cc4132cc5 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:06:44 +0200 Subject: [PATCH 077/144] refactor: change spinner to functional components --- src/shared/ui/Spinner.tsx | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Spinner.tsx index 8c95ef2..4c83069 100644 --- a/src/shared/ui/Spinner.tsx +++ b/src/shared/ui/Spinner.tsx @@ -1,27 +1,18 @@ -import { Component } from 'react'; - import { createPortal } from 'react-dom'; -import { - ISearchContext, - SearchContext, -} from '../../features/Search/context/SearchProvider.tsx'; - -class Spinner extends Component { - static contextType = SearchContext; +import { useSearch } from '../../features/Search/context/SearchProvider.tsx'; - declare context: ISearchContext; +function Spinner() { + const { isLoading } = useSearch(); - render() { - if (!this.context.isLoading) return null; + if (!isLoading) return null; - return createPortal( -
                      - -
                      , - document.body, - ); - } + return createPortal( +
                      + +
                      , + document.body, + ); } export default Spinner; From da7bdb0fdee5e1d31d2c8005c2b187d8f424536c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:08:03 +0200 Subject: [PATCH 078/144] refactor: change gradient background to functional components --- src/pages/ui/GradientBackground.tsx | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/pages/ui/GradientBackground.tsx b/src/pages/ui/GradientBackground.tsx index b8d59dc..9937f17 100644 --- a/src/pages/ui/GradientBackground.tsx +++ b/src/pages/ui/GradientBackground.tsx @@ -1,17 +1,13 @@ -import { Component } from 'react'; - -class GradientBackground extends Component { - render() { - return ( -
                      -
                      -
                      -
                      -
                      -
                      -
                      - ); - } +function GradientBackground() { + return ( +
                      +
                      +
                      +
                      +
                      +
                      +
                      + ); } export default GradientBackground; From 88627c9c0ea026bf8d669f682eb63dafcf2af5fc Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:08:47 +0200 Subject: [PATCH 079/144] refactor: change header to functional components --- src/widgets/Header/Header.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index b8e6c92..6596d61 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -1,15 +1,11 @@ -import { Component } from 'react'; - import { IChildren } from '../../shared/types/interfaces.ts'; -class Header extends Component { - render() { - return ( -
                      - {this.props.children} -
                      - ); - } +function Header({ children }: IChildren) { + return ( +
                      + {children} +
                      + ); } export default Header; From 0c18ae5041610fe52c09fc485b26913a9a8fca5b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:09:27 +0200 Subject: [PATCH 080/144] refactor: change logo to functional components --- src/widgets/Header/ui/Logo.tsx | 38 +++++++++++++++------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/widgets/Header/ui/Logo.tsx b/src/widgets/Header/ui/Logo.tsx index db131e0..a568aaf 100644 --- a/src/widgets/Header/ui/Logo.tsx +++ b/src/widgets/Header/ui/Logo.tsx @@ -1,26 +1,22 @@ -import { Component } from 'react'; - import { LOGO_LETTERS } from '../const/const.ts'; -class Logo extends Component { - render() { - return ( -

                      - {LOGO_LETTERS.map((letter, i) => ( - - {letter} - - ))} -

                      - ); - } +function Logo() { + return ( +

                      + {LOGO_LETTERS.map((letter, i) => ( + + {letter} + + ))} +

                      + ); } export default Logo; From f737f495f69c20979af02c1db67d8d3936862c7c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:36:05 +0200 Subject: [PATCH 081/144] refactor: change search to functional components --- src/features/Search/Search.tsx | 155 +++++++++--------- .../Search/context/SearchProvider.tsx | 20 +-- src/features/Search/hooks/useSearch.ts | 14 ++ src/shared/ui/Spinner.tsx | 2 +- 4 files changed, 92 insertions(+), 99 deletions(-) create mode 100644 src/features/Search/hooks/useSearch.ts diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index b8c7557..16959f8 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -1,92 +1,83 @@ -import { Component, createRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; -import { ISearchContext, SearchContext } from './context/SearchProvider.tsx'; +import useSearch from './hooks/useSearch.ts'; import searchIcon from '../../assets/search.svg'; import { LOCAL_STORAGE_SEARCH_QUERY } from '../../shared/const/const.ts'; import Button from '../../shared/ui/Button.tsx'; -interface ISearchState { - searchQuery: string; -} - -class Search extends Component { - static contextType = SearchContext; - - declare context: ISearchContext; - - inputRef = createRef(); - - state = { - searchQuery: '', - }; - - componentDidMount() { - document.addEventListener('keydown', this.handleKeydown); - +// TODO - split the component into smaller ones + +function Search() { + const [query, setQuery] = useState(''); + const inputRef = useRef(null); + const { fetchMovies } = useSearch(); + + const handleSearch = useCallback( + async (newQuery: string) => { + fetchMovies(newQuery.trim()); + }, + [fetchMovies], + ); + + const handleKeydown = useCallback( + (e: KeyboardEvent) => { + const isEnterPressed = e.key === 'Enter'; + const isEscPressed = e.key === 'Escape'; + const isInputFocus = document.activeElement === inputRef.current; + const noActiveElement = document.activeElement === document.body; + + if (isEscPressed && isInputFocus) { + inputRef.current?.blur(); + } + + if (isEnterPressed && noActiveElement) { + inputRef.current?.focus(); + setQuery(''); + return; + } + + if (isEnterPressed && isInputFocus) { + void handleSearch(query); + inputRef.current?.blur(); + } + }, + [handleSearch, query], + ); + + useEffect(() => { const storedQuery = localStorage.getItem(LOCAL_STORAGE_SEARCH_QUERY); - if (storedQuery) { - this.setState({ searchQuery: storedQuery }); - void this.handleSearch(storedQuery); - return; - } - - void this.handleSearch(''); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.handleKeydown); - } - - handleKeydown = (e: KeyboardEvent) => { - const isEnterPressed = e.key === 'Enter'; - const isEscPressed = e.key === 'Escape'; - const isInputFocus = document.activeElement === this.inputRef.current; - const noActiveElement = document.activeElement === document.body; - - if (isEscPressed && isInputFocus) { - this.inputRef.current?.blur(); - } - - if (isEnterPressed && noActiveElement) { - this.inputRef.current?.focus(); - this.setState({ searchQuery: '' }); - return; - } - - if (isEnterPressed && isInputFocus) { - void this.handleSearch(this.state.searchQuery); - this.inputRef.current?.blur(); - } - }; - - handleSearch = async (query: string) => { - this.context.fetchMovies(query.trim()); - localStorage.setItem(LOCAL_STORAGE_SEARCH_QUERY, query.trim()); - }; - - render() { - return ( -
                      - this.setState({ searchQuery: e.target.value })} - /> - -
                      - ); - } + if (!storedQuery) return; + + setQuery(storedQuery); + void fetchMovies(storedQuery); + }, [fetchMovies]); + + useEffect(() => { + document.addEventListener('keydown', handleKeydown); + return () => document.removeEventListener('keydown', handleKeydown); + }, [handleKeydown]); + + return ( +
                      + setQuery(e.target.value)} + /> + +
                      + ); } export default Search; diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx index 0b71d8b..abff0ea 100644 --- a/src/features/Search/context/SearchProvider.tsx +++ b/src/features/Search/context/SearchProvider.tsx @@ -1,12 +1,7 @@ -import { - createContext, - useCallback, - useContext, - useMemo, - useReducer, -} from 'react'; +import { createContext, useCallback, useMemo, useReducer } from 'react'; import { getMovieList } from '../../../entities/movie/api/apiMovie.ts'; +import { LOCAL_STORAGE_SEARCH_QUERY } from '../../../shared/const/const.ts'; import { IChildren } from '../../../shared/types/interfaces.ts'; import { MovieList } from '../../../shared/types/types.ts'; import { NO_MOVIES, NO_RESULTS } from '../const/const.ts'; @@ -74,6 +69,8 @@ function SearchProvider({ children }: IChildren) { const fetchMovies = useCallback( async (searchQuery: string) => { + localStorage.setItem(LOCAL_STORAGE_SEARCH_QUERY, searchQuery); + try { dispatch({ type: SearchActions.LOADING }); const res = await getMovieList(searchQuery); @@ -104,13 +101,4 @@ function SearchProvider({ children }: IChildren) { ); } -export function useSearch() { - const context = useContext(SearchContext); - - if (context === undefined) - throw new Error('SearchContext is used outside the SearchProvider!'); - - return context; -} - export default SearchProvider; diff --git a/src/features/Search/hooks/useSearch.ts b/src/features/Search/hooks/useSearch.ts new file mode 100644 index 0000000..52ba0ea --- /dev/null +++ b/src/features/Search/hooks/useSearch.ts @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import { SearchContext } from '../context/SearchProvider.tsx'; + +function useSearch() { + const context = useContext(SearchContext); + + if (context === undefined) + throw new Error('SearchContext is used outside the SearchProvider!'); + + return context; +} + +export default useSearch; diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Spinner.tsx index 4c83069..fbb28bf 100644 --- a/src/shared/ui/Spinner.tsx +++ b/src/shared/ui/Spinner.tsx @@ -1,6 +1,6 @@ import { createPortal } from 'react-dom'; -import { useSearch } from '../../features/Search/context/SearchProvider.tsx'; +import useSearch from '../../features/Search/hooks/useSearch.ts'; function Spinner() { const { isLoading } = useSearch(); From ab6e9b8c4c5dfb3f9957051d9bdcca33f3456312 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:40:31 +0200 Subject: [PATCH 082/144] refactor: change button to functional components --- src/shared/ui/Button.tsx | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/shared/ui/Button.tsx b/src/shared/ui/Button.tsx index 539c446..29ac689 100644 --- a/src/shared/ui/Button.tsx +++ b/src/shared/ui/Button.tsx @@ -1,4 +1,4 @@ -import { Component } from 'react'; +import { memo } from 'react'; import { IChildren } from '../types/interfaces.ts'; @@ -15,19 +15,20 @@ interface IButtonProps extends IChildren { className?: string; } -class Button extends Component { - render() { - return ( - - ); - } -} +const Button = memo(function Button({ + type = 'filled', + onClick, + className = '', + children, +}: IButtonProps) { + return ( + + ); +}); export default Button; From 26c25cf75892f56d80a1a1c4ec4170d48dfd7e38 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:41:18 +0200 Subject: [PATCH 083/144] chore: disable unnecessary eslint rules --- .eslintrc.cjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f624073..3d7d9d7 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -24,6 +24,8 @@ module.exports = { sourceType: 'module', }, rules: { + 'prefer-arrow-callback': 0, + 'react/require-default-props': 0, 'react/react-in-jsx-scope': 0, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 'no-void': 0, From 1db1e405b98a6c839eb8437fd10afa54946369c8 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:43:04 +0200 Subject: [PATCH 084/144] refactor: change total results to functional component --- src/widgets/Header/ui/TotalResults.tsx | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/widgets/Header/ui/TotalResults.tsx b/src/widgets/Header/ui/TotalResults.tsx index e100271..5ac8720 100644 --- a/src/widgets/Header/ui/TotalResults.tsx +++ b/src/widgets/Header/ui/TotalResults.tsx @@ -1,23 +1,13 @@ -import { Component } from 'react'; +import useSearch from '../../../features/Search/hooks/useSearch.ts'; -import { - ISearchContext, - SearchContext, -} from '../../../features/Search/context/SearchProvider.tsx'; +function TotalResults() { + const { totalResults } = useSearch(); -class TotalResults extends Component { - static contextType = SearchContext; - - declare context: ISearchContext; - - render() { - return ( -

                      - Found {this.context.totalResults}{' '} - movies -

                      - ); - } + return ( +

                      + Found {totalResults} movies +

                      + ); } export default TotalResults; From e67b3df6bc67ab10e0cab621df1cede8fe5341a5 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:43:39 +0200 Subject: [PATCH 085/144] refactor: change main to functional component --- src/widgets/Main/Main.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/widgets/Main/Main.tsx b/src/widgets/Main/Main.tsx index 8013d43..db9b327 100644 --- a/src/widgets/Main/Main.tsx +++ b/src/widgets/Main/Main.tsx @@ -1,13 +1,7 @@ -import { Component } from 'react'; - import { IChildren } from '../../shared/types/interfaces.ts'; -class Main extends Component { - render() { - const { children } = this.props; - - return
                      {children}
                      ; - } +function Main({ children }: IChildren) { + return
                      {children}
                      ; } export default Main; From c726d5d8d731484152b0ff3d083be4e1ed3aedf6 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:48:54 +0200 Subject: [PATCH 086/144] refactor: change movie list to functional component --- src/features/ProductList/MovieList.tsx | 48 +++++++++----------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/src/features/ProductList/MovieList.tsx b/src/features/ProductList/MovieList.tsx index a913107..5c55a0e 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/ProductList/MovieList.tsx @@ -1,37 +1,21 @@ -import { Component } from 'react'; - import NotFound from './ui/NotFound.tsx'; import Movie from '../../entities/movie/ui/Movie.tsx'; -import { MovieList as MovieListData } from '../../shared/types/types.ts'; -import { - ISearchContext, - SearchContext, -} from '../Search/context/SearchProvider.tsx'; - -interface IMovieListState { - movies: MovieListData | null; -} - -class MovieList extends Component { - static contextType = SearchContext; - - declare context: ISearchContext; - - render() { - const { movies, isLoading } = this.context; - const isNoMovies = !movies?.length; - - if (isNoMovies && !isLoading) return ; - if (isNoMovies) return null; - - return ( -
                        - {movies.map((movie, i) => ( - - ))} -
                      - ); - } +import useSearch from '../Search/hooks/useSearch.ts'; + +function MovieList() { + const { movies, isLoading } = useSearch(); + const isNoMovies = !movies?.length; + + if (isNoMovies && !isLoading) return ; + if (isNoMovies) return null; + + return ( +
                        + {movies.map((movie, i) => ( + + ))} +
                      + ); } export default MovieList; From 798c0bf1d5f82935300875548e112f5ecc6faa31 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:56:33 +0200 Subject: [PATCH 087/144] refactor: change movie to functional component --- src/entities/movie/ui/Movie.tsx | 133 ++++++++++++++------------------ 1 file changed, 58 insertions(+), 75 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index b4347db..b2f8cca 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,4 +1,4 @@ -import { Component, createRef, MouseEvent } from 'react'; +import { MouseEvent, useEffect, useRef, useState } from 'react'; import ReactLogo from '../../../assets/reactJS-logo.png'; import { NOT_EXIST } from '../../../shared/const/const.ts'; @@ -11,89 +11,72 @@ interface IMovieProps { delay: number; } -interface IMovieState { - description: string; - genre: string; -} - -class Movie extends Component { - state = { - description: '', - genre: '', - }; - - movieRef = createRef(); - - containerRef = createRef(); +// TODO - divide the component into smaller ones - radialHover; +function Movie({ data, delay }: IMovieProps) { + const [description, setDescription] = useState(''); + const [genre, setGenre] = useState(''); - cleanUp; + const movieRef = useRef(null); + const containerRef = useRef(null); - constructor(props: IMovieProps) { - super(props); + const { Poster, Title, Year } = data; - [this.radialHover, this.cleanUp] = createRadialHover(); - } + const isPosterExist = Poster !== NOT_EXIST; + const poster = isPosterExist ? Poster : ReactLogo; + const animationDelay = `0.${String(delay)}s`; - async componentDidMount() { - const movieData = await getMovie(this.props.data.imdbID); - const genre = movieData.Genre.split(', ').slice(0, 2).join('/'); - const description = `${movieData.Plot.slice(0, 38)}...`; + const [radialHover, cleanUp] = createRadialHover(); - this.setState({ - description, - genre, - }); - } - - handleMouseMove = (e: MouseEvent) => { - if (this.movieRef.current) this.radialHover(this.movieRef.current, e); + const handleMouseMove = (e: MouseEvent) => { + if (movieRef.current) radialHover(movieRef.current, e); }; - handleMouseOut = () => { - if (this.movieRef.current) - this.cleanUp(this.movieRef.current as HTMLDivElement); + const handleMouseOut = () => { + if (movieRef.current) cleanUp(movieRef.current as HTMLDivElement); }; - render() { - const { Poster, Title, Year } = this.props.data; - const { description, genre } = this.state; - - const isPosterExist = Poster !== NOT_EXIST; - const poster = isPosterExist ? Poster : ReactLogo; - const animationDelay = `0.${String(this.props.delay)}s`; - - return ( -
                    • -
                      - {`The -
                      -

                      {Title}

                      -
                      -

                      {description}

                      - {genre} - {Year} -
                      -
                      -
                      -
                    • - ); - } + useEffect(() => { + async function fetchMovies() { + const movieData = await getMovie(data.imdbID); + const slicedGenre = movieData.Genre.split(', ').slice(0, 2).join('/'); + const slicedDescription = `${movieData.Plot.slice(0, 38)}...`; + + setDescription(slicedDescription); + setGenre(slicedGenre); + } + + void fetchMovies(); + }, [data.imdbID]); + + return ( +
                    • +
                      + {`The +
                      +

                      {Title}

                      +
                      +

                      {description}

                      + {genre} + {Year} +
                      +
                      +
                      +
                    • + ); } - export default Movie; From 66732ee06d684c642f1a073b928a66d4cb315125 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 18:58:31 +0200 Subject: [PATCH 088/144] refactor: change fallbackUi to functional component --- src/shared/ui/FallbackUi.tsx | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/shared/ui/FallbackUi.tsx b/src/shared/ui/FallbackUi.tsx index bf1b386..df87d3a 100644 --- a/src/shared/ui/FallbackUi.tsx +++ b/src/shared/ui/FallbackUi.tsx @@ -1,23 +1,19 @@ -import { Component } from 'react'; - import Modal from './Modal.tsx'; -class FallbackUi extends Component { - render() { - return ( -
                      - -

                      - Something went really wrong 😱 -

                      -

                      If you faced any issues, please contact our support team

                      - - cinemania-help@gmail.com - -
                      -
                      - ); - } +function FallbackUi() { + return ( +
                      + +

                      + Something went really wrong 😱 +

                      +

                      If you faced any issues, please contact our support team

                      + + cinemania-help@gmail.com + +
                      +
                      + ); } export default FallbackUi; From 8c93ea2e769a156e528b0432cde672c490a2fc9d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 19:06:26 +0200 Subject: [PATCH 089/144] refactor: change modal to functional component --- src/shared/ui/Modal.tsx | 52 ++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/src/shared/ui/Modal.tsx b/src/shared/ui/Modal.tsx index 1c8cc0d..ee30f36 100644 --- a/src/shared/ui/Modal.tsx +++ b/src/shared/ui/Modal.tsx @@ -1,4 +1,4 @@ -import { Component, createRef, MouseEvent } from 'react'; +import { MouseEvent, useRef } from 'react'; import createRadialHover from '../lib/helpers/animateRadialHover.ts'; import { IChildren } from '../types/interfaces.ts'; @@ -7,43 +7,31 @@ interface IModalProps extends IChildren { className?: string; } -class Modal extends Component { - modalRef = createRef(); +function Modal({ className = '', children }: IModalProps) { + const modalRef = useRef(null); - radialHover; + const [radialHover, cleanUp] = createRadialHover(); - cleanUp; - - constructor(props: IModalProps) { - super(props); - - [this.radialHover, this.cleanUp] = createRadialHover(); - } - - handleMouseMove = (e: MouseEvent) => { - if (this.modalRef.current) this.radialHover(this.modalRef.current, e); + const handleMouseMove = (e: MouseEvent) => { + if (modalRef.current) radialHover(modalRef.current, e); }; - handleMouseOut = () => { - if (this.modalRef.current) this.cleanUp(this.modalRef.current); + const handleMouseOut = () => { + if (modalRef.current) cleanUp(modalRef.current); }; - render() { - return ( -
                      -
                      - {this.props.children} -
                      -
                      - ); - } + return ( +
                      +
                      + {children} +
                      +
                      + ); } export default Modal; From d49f5091fedacea79346729ae9415c91c1c6301b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 19:09:29 +0200 Subject: [PATCH 090/144] fix: eslint errors --- src/shared/ui/ErrorBoundary.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/shared/ui/ErrorBoundary.tsx b/src/shared/ui/ErrorBoundary.tsx index 98f8a08..e699399 100644 --- a/src/shared/ui/ErrorBoundary.tsx +++ b/src/shared/ui/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import { Component, ErrorInfo, ReactNode } from 'react'; +import { Component, ReactNode } from 'react'; import { IChildren } from '../types/interfaces.ts'; @@ -14,23 +14,25 @@ class ErrorBoundary extends Component< IErrorBoundaryProps, IErrorBoundaryState > { - state = { - hasError: false, - }; + constructor(props: IErrorBoundaryProps) { + super(props); + + this.state = { + hasError: false, + }; + } static getDerivedStateFromError = () => ({ hasError: true, }); - componentDidCatch(error: Error, info: ErrorInfo) { - // eslint-disable-next-line no-console - console.log('Error report =', error, info.componentStack); - } - render() { - if (this.state.hasError) return this.props.fallback; + const { hasError } = this.state; + const { fallback, children } = this.props; + + if (hasError) return fallback; - return this.props.children; + return children; } } From da1cbed124f605e3dbd8bf0dabc16b781fe5007f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 19:12:34 +0200 Subject: [PATCH 091/144] refactor: change not found to functional component --- src/features/ProductList/ui/NotFound.tsx | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/features/ProductList/ui/NotFound.tsx b/src/features/ProductList/ui/NotFound.tsx index 7a2fa6f..79166db 100644 --- a/src/features/ProductList/ui/NotFound.tsx +++ b/src/features/ProductList/ui/NotFound.tsx @@ -1,19 +1,15 @@ -import { Component } from 'react'; - import Modal from '../../../shared/ui/Modal.tsx'; -class NotFound extends Component { - render() { - return ( - - - No Movies Found. 🥺 - -

                      You're search did not match any movies.

                      -

                      Please try again. 👉👈

                      -
                      - ); - } +function NotFound() { + return ( + + + No Movies Found. 🥺 + +

                      You're search did not match any movies.

                      +

                      Please try again. 👉👈

                      +
                      + ); } export default NotFound; From 9f93d36993a058df2f49a5af90d39c18168f40c1 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 19:13:14 +0200 Subject: [PATCH 092/144] chore: disable unnecessary eslint rule --- .eslintrc.cjs | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 3d7d9d7..ac4d8cb 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -30,6 +30,7 @@ module.exports = { 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 'no-void': 0, 'no-param-reassign': 0, + 'import/no-extraneous-dependencies': 0, 'import/extensions': [ 'error', 'ignorePackages', From b6ab88a26948d5959a41cc2f94582c261fa6cfd1 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 30 Oct 2023 23:28:01 +0200 Subject: [PATCH 093/144] refactor: change custom scroll --- package-lock.json | 64 +++++++++++++++++++------ package.json | 3 +- src/index.css | 89 +++++++++++++++++++++++++++++++++++ src/pages/MainLayout.tsx | 4 +- src/shared/hooks/useScroll.ts | 35 ++++++++++++++ src/widgets/Header/Header.tsx | 5 +- src/widgets/Main/Main.tsx | 6 ++- 7 files changed, 188 insertions(+), 18 deletions(-) create mode 100644 src/shared/hooks/useScroll.ts diff --git a/package-lock.json b/package-lock.json index c2ce816..4e94b5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,12 @@ "name": "react-ts-tw-template", "version": "0.0.0", "dependencies": { - "locomotive-scroll": "^5.0.0-beta.8", + "locomotive-scroll": "^4.1.4", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { + "@types/locomotive-scroll": "^4.1.2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.0.0", @@ -1004,11 +1005,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@studio-freight/lenis": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@studio-freight/lenis/-/lenis-1.0.12.tgz", - "integrity": "sha512-iN3JQ6qJVc+L7vh8NjlDibw9dvjjifvlJ+s0UAkfwty+vW9MJxRfszb5X/eAhcbDLZW6MlvjRHoH9WSQH3J8eA==" - }, "node_modules/@types/babel__core": { "version": "7.20.3", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", @@ -1062,6 +1058,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/locomotive-scroll": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/locomotive-scroll/-/locomotive-scroll-4.1.2.tgz", + "integrity": "sha512-K4/YWVuLf+xW5lXPue8RdWAm96dVPlyn8ISqxGdK9QflLFy82cDsvpyHJchcKtfp+qNV9OZ11nq56T5oa8jogA==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.9", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", @@ -1645,6 +1647,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bezier-easing": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", + "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==" + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -1663,6 +1670,11 @@ "node": ">=8" } }, + "node_modules/bindall-standalone": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bindall-standalone/-/bindall-standalone-1.0.5.tgz", + "integrity": "sha512-HDI7YBWXVJk/eoGz+e4lYQQJnYp1ZHcUvAY71lVptLMhQnDm86vD73AGPw2qIlgYR3P0bjmoAcXiA8qhFejBhA==" + }, "node_modules/bplist-parser": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", @@ -4099,6 +4111,11 @@ "language-subtag-registry": "~0.3.2" } }, + "node_modules/lethargy": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/lethargy/-/lethargy-1.0.9.tgz", + "integrity": "sha512-nFM8blpCF9rqIL5mRAaTGc78W8oQixVtsD86jbEPvcI13+lDUYJf3R7DZQQL7tCiBpbGpGKMX2gwJFO9hiaOkg==" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4199,14 +4216,13 @@ } }, "node_modules/locomotive-scroll": { - "version": "5.0.0-beta.8", - "resolved": "https://registry.npmjs.org/locomotive-scroll/-/locomotive-scroll-5.0.0-beta.8.tgz", - "integrity": "sha512-4Nq12voKr5719aaySZg3FX/gQKQIAaAC4NkBiU27aKEKFnYcTeOB1r8VO5bGpEIOGQo94wsiJQNZuRb3TsxjHA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/locomotive-scroll/-/locomotive-scroll-4.1.4.tgz", + "integrity": "sha512-6i98cFF2SKg6wIPpwVPuo2FG8qL3USsdDeew78TEYZyLoqleMWNfkSDpWA6mPym4dOfTIBXc678VmGlkgx3fTA==", "dependencies": { - "@studio-freight/lenis": "1.0.12" - }, - "engines": { - "node": ">=17" + "bezier-easing": "^2.1.0", + "smoothscroll-polyfill": "^0.4.4", + "virtual-scroll": "^1.5.2" } }, "node_modules/lodash.merge": { @@ -4438,7 +4454,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5592,6 +5607,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/smoothscroll-polyfill": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz", + "integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==" + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -5928,6 +5948,11 @@ "node": ">=0.8" } }, + "node_modules/tiny-emitter": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-1.2.0.tgz", + "integrity": "sha512-rWjF00inHeWtT5UbQYAXoMI4hL6TRMqohuKCsODyPYYmfAxqfMnXLsIeNrbdPEkNxlk++rojVilTnI9IVmEBtA==" + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -6180,6 +6205,17 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/virtual-scroll": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/virtual-scroll/-/virtual-scroll-1.5.2.tgz", + "integrity": "sha512-7jDHwlKbHUho7CYU/HojE/VKFH8GV9P5fVWP2HCa7dRUOpVvwl93OBOKIIcb2mKd+vqsbVR/0zl0X70+3sUZqA==", + "dependencies": { + "bindall-standalone": "^1.0.5", + "lethargy": "^1.0.2", + "object-assign": "^4.0.1", + "tiny-emitter": "^1.0.0" + } + }, "node_modules/vite": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", diff --git a/package.json b/package.json index 3982ca2..fb0c547 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ "precommit": "pnpm lint-staged && pnpm type-check" }, "dependencies": { - "locomotive-scroll": "^5.0.0-beta.8", + "locomotive-scroll": "^4.1.4", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { + "@types/locomotive-scroll": "^4.1.2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.0.0", diff --git a/src/index.css b/src/index.css index 7f791b5..58df0cc 100644 --- a/src/index.css +++ b/src/index.css @@ -193,3 +193,92 @@ animation-delay: 8s; } } + +html.has-scroll-smooth { + overflow: hidden; +} + +html.has-scroll-dragging { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.has-scroll-smooth body { + overflow: hidden; +} + +.has-scroll-smooth [data-scroll-container] { + min-height: 100vh; +} + +[data-scroll-direction='horizontal'] [data-scroll-container] { + height: 100vh; + display: inline-block; + white-space: nowrap; +} + +[data-scroll-direction='horizontal'] [data-scroll-section] { + display: inline-block; + vertical-align: top; + white-space: nowrap; + height: 100%; +} + +.c-scrollbar { + position: absolute; + right: 0; + top: 0; + width: 11px; + height: 100%; + transform-origin: center right; + transition: + transform 0.3s, + opacity 0.3s; + opacity: 0; + z-index: 999; +} + +.c-scrollbar:hover, +.has-scroll-scrolling .c-scrollbar, +.has-scroll-dragging .c-scrollbar { + opacity: 1; +} + +[data-scroll-direction='horizontal'] .c-scrollbar { + width: 100%; + height: 10px; + top: auto; + bottom: 0; + transform: scaleY(1); +} + +[data-scroll-direction='horizontal'] .c-scrollbar:hover { + transform: scaleY(1.3); +} + +.c-scrollbar_thumb { + position: absolute; + top: 0; + right: 0; + background-color: theme(colors.neutral.400); + opacity: 0.5; + width: 6.3px; + border-radius: 10px; + margin: 4px; + cursor: -webkit-grab; + cursor: grab; + z-index: 999; + box-shadow: 3px 6px 10px theme(colors.neutral.950); +} + +.has-scroll-dragging .c-scrollbar_thumb { + cursor: -webkit-grabbing; + cursor: grabbing; +} + +[data-scroll-direction='horizontal'] .c-scrollbar_thumb { + right: auto; + bottom: 0; +} diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index c94a977..1432a94 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/ProductList/MovieList.tsx'; import Search from '../features/Search/Search.tsx'; +import useScroll from '../shared/hooks/useScroll.ts'; import Button from '../shared/ui/Button.tsx'; import Spinner from '../shared/ui/Spinner.tsx'; import Header from '../widgets/Header/Header.tsx'; @@ -12,6 +13,7 @@ import Main from '../widgets/Main/Main.tsx'; function MainLayout() { const [isError, setIsError] = useState(false); + const containerRef = useScroll(); if (isError) throw new Error('test error'); @@ -19,7 +21,7 @@ function MainLayout() { <> -
                      +
                      diff --git a/src/shared/hooks/useScroll.ts b/src/shared/hooks/useScroll.ts new file mode 100644 index 0000000..5fdf03a --- /dev/null +++ b/src/shared/hooks/useScroll.ts @@ -0,0 +1,35 @@ +import { useEffect, useRef } from 'react'; + +import LocomotiveScroll from 'locomotive-scroll'; + +function useScroll() { + const containerRef = useRef(null); + const scrollRef = useRef(); + const observerRef = useRef(); + + useEffect(() => { + if (containerRef.current) { + containerRef.current.setAttribute('data-scroll-container', 'true'); + + scrollRef.current = new LocomotiveScroll({ + el: containerRef.current, + smooth: true, + lerp: 0.22, + }); + + observerRef.current = new ResizeObserver( + () => scrollRef.current?.update(), + ); + observerRef.current.observe(containerRef.current); + } + + return () => { + scrollRef.current?.destroy(); + observerRef.current?.disconnect(); + }; + }, []); + + return containerRef; +} + +export default useScroll; diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 6596d61..1a8bf8e 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -2,7 +2,10 @@ import { IChildren } from '../../shared/types/interfaces.ts'; function Header({ children }: IChildren) { return ( -
                      +
                      {children}
                      ); diff --git a/src/widgets/Main/Main.tsx b/src/widgets/Main/Main.tsx index db9b327..9fc8f1e 100644 --- a/src/widgets/Main/Main.tsx +++ b/src/widgets/Main/Main.tsx @@ -1,7 +1,11 @@ import { IChildren } from '../../shared/types/interfaces.ts'; function Main({ children }: IChildren) { - return
                      {children}
                      ; + return ( +
                      + {children} +
                      + ); } export default Main; From e676ee046496eab0d6c92f222b51adb3f6047515 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 31 Oct 2023 15:47:11 +0200 Subject: [PATCH 094/144] refactor: abstract useEffects into separate hooks --- src/features/Search/Search.tsx | 70 +++++++++---------- src/features/Search/const/const.ts | 2 + .../Search/context/SearchProvider.tsx | 16 ++++- src/shared/hooks/useKey.ts | 21 ++++++ src/shared/hooks/useLocalStorageState.ts | 16 +++++ 5 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 src/shared/hooks/useKey.ts create mode 100644 src/shared/hooks/useLocalStorageState.ts diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 16959f8..295192c 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -1,17 +1,26 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +/* eslint-disable @typescript-eslint/no-use-before-define */ +import { useCallback, useRef } from 'react'; + +import { ENTER_KEY, ESCAPE_KEY } from './const/const.ts'; import useSearch from './hooks/useSearch.ts'; import searchIcon from '../../assets/search.svg'; import { LOCAL_STORAGE_SEARCH_QUERY } from '../../shared/const/const.ts'; +import useKey from '../../shared/hooks/useKey.ts'; +import useLocalStorageState from '../../shared/hooks/useLocalStorageState.ts'; import Button from '../../shared/ui/Button.tsx'; -// TODO - split the component into smaller ones - function Search() { - const [query, setQuery] = useState(''); + const [query, setQuery] = useLocalStorageState( + '', + LOCAL_STORAGE_SEARCH_QUERY, + ); const inputRef = useRef(null); const { fetchMovies } = useSearch(); + useKey(ENTER_KEY, handleEnter); + useKey(ESCAPE_KEY, handleEscape); + const handleSearch = useCallback( async (newQuery: string) => { fetchMovies(newQuery.trim()); @@ -19,44 +28,29 @@ function Search() { [fetchMovies], ); - const handleKeydown = useCallback( - (e: KeyboardEvent) => { - const isEnterPressed = e.key === 'Enter'; - const isEscPressed = e.key === 'Escape'; - const isInputFocus = document.activeElement === inputRef.current; - const noActiveElement = document.activeElement === document.body; - - if (isEscPressed && isInputFocus) { - inputRef.current?.blur(); - } - - if (isEnterPressed && noActiveElement) { - inputRef.current?.focus(); - setQuery(''); - return; - } - - if (isEnterPressed && isInputFocus) { - void handleSearch(query); - inputRef.current?.blur(); - } - }, - [handleSearch, query], - ); + function handleEnter() { + const isInputFocus = document.activeElement === inputRef.current; + const noActiveElement = document.activeElement === document.body; - useEffect(() => { - const storedQuery = localStorage.getItem(LOCAL_STORAGE_SEARCH_QUERY); + if (noActiveElement) { + inputRef.current?.focus(); + setQuery(''); + return; + } - if (!storedQuery) return; + if (isInputFocus) { + void handleSearch(query); + inputRef.current?.blur(); + } + } - setQuery(storedQuery); - void fetchMovies(storedQuery); - }, [fetchMovies]); + function handleEscape() { + const isInputFocus = document.activeElement === inputRef.current; - useEffect(() => { - document.addEventListener('keydown', handleKeydown); - return () => document.removeEventListener('keydown', handleKeydown); - }, [handleKeydown]); + if (isInputFocus) { + inputRef.current?.blur(); + } + } return (
                      diff --git a/src/features/Search/const/const.ts b/src/features/Search/const/const.ts index e4cb963..33318e3 100644 --- a/src/features/Search/const/const.ts +++ b/src/features/Search/const/const.ts @@ -2,3 +2,5 @@ import { MovieList } from '../../../shared/types/types.ts'; export const NO_MOVIES: MovieList = []; export const NO_RESULTS = 0; +export const ENTER_KEY = 'Enter'; +export const ESCAPE_KEY = 'Escape'; diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx index abff0ea..20e5b5b 100644 --- a/src/features/Search/context/SearchProvider.tsx +++ b/src/features/Search/context/SearchProvider.tsx @@ -1,4 +1,10 @@ -import { createContext, useCallback, useMemo, useReducer } from 'react'; +import { + createContext, + useCallback, + useEffect, + useMemo, + useReducer, +} from 'react'; import { getMovieList } from '../../../entities/movie/api/apiMovie.ts'; import { LOCAL_STORAGE_SEARCH_QUERY } from '../../../shared/const/const.ts'; @@ -82,6 +88,14 @@ function SearchProvider({ children }: IChildren) { [updateMovies], ); + useEffect(() => { + const storedQuery = localStorage.getItem(LOCAL_STORAGE_SEARCH_QUERY); + + if (storedQuery === null) return; + + void fetchMovies(storedQuery); + }, [fetchMovies]); + const providerValue = useMemo( () => ({ query, diff --git a/src/shared/hooks/useKey.ts b/src/shared/hooks/useKey.ts new file mode 100644 index 0000000..40d7747 --- /dev/null +++ b/src/shared/hooks/useKey.ts @@ -0,0 +1,21 @@ +import { useCallback, useEffect } from 'react'; + +function useKey(key: string, action: () => void) { + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + const keyMatched = e.key.toLowerCase() === key.toLowerCase(); + + if (keyMatched) { + action(); + } + }, + [action, key], + ); + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); +} + +export default useKey; diff --git a/src/shared/hooks/useLocalStorageState.ts b/src/shared/hooks/useLocalStorageState.ts new file mode 100644 index 0000000..ba84ed6 --- /dev/null +++ b/src/shared/hooks/useLocalStorageState.ts @@ -0,0 +1,16 @@ +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +function useLocalStorageState(initialState: string, key: string) { + const storedValue = localStorage.getItem(key); + const init = storedValue || initialState; + + const [value, setValue] = useState(init); + + useEffect(() => { + localStorage.setItem(key, value); + }, [value, key]); + + return [value, setValue] as [string, Dispatch>]; +} + +export default useLocalStorageState; From c7705f8df0825c43db4e8b72f89bddfa93baa596 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 31 Oct 2023 16:48:29 +0200 Subject: [PATCH 095/144] refactor: make gradient background more brighter --- src/pages/ui/GradientBackground.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/ui/GradientBackground.tsx b/src/pages/ui/GradientBackground.tsx index 9937f17..9d111e2 100644 --- a/src/pages/ui/GradientBackground.tsx +++ b/src/pages/ui/GradientBackground.tsx @@ -1,11 +1,11 @@ function GradientBackground() { return ( -
                      -
                      -
                      -
                      -
                      -
                      +
                      +
                      +
                      +
                      +
                      +
                      ); } From 2a304d9d2834c7f943956edcff167d2f85d1cedc Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 31 Oct 2023 16:50:13 +0200 Subject: [PATCH 096/144] refactor: app small adjustments --- src/index.css | 17 +---------------- src/shared/hooks/useScroll.ts | 2 +- src/widgets/Header/ui/Logo.tsx | 2 +- tailwind.config.js | 4 ++-- 4 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/index.css b/src/index.css index 58df0cc..9e7c58a 100644 --- a/src/index.css +++ b/src/index.css @@ -8,21 +8,6 @@ background-color: theme(colors.lime.50); } - ::-webkit-scrollbar { - width: 16px; - height: 16px; - } - - ::-webkit-scrollbar-track { - background: theme(colors.neutral.900); - } - - ::-webkit-scrollbar-thumb { - background: theme(colors.neutral.500); - border-radius: 999px; - border: 4px solid theme(colors.neutral.900); - } - * { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; @@ -262,7 +247,7 @@ html.has-scroll-dragging { position: absolute; top: 0; right: 0; - background-color: theme(colors.neutral.400); + background-color: theme(colors.neutral.500); opacity: 0.5; width: 6.3px; border-radius: 10px; diff --git a/src/shared/hooks/useScroll.ts b/src/shared/hooks/useScroll.ts index 5fdf03a..a0bbd18 100644 --- a/src/shared/hooks/useScroll.ts +++ b/src/shared/hooks/useScroll.ts @@ -14,7 +14,7 @@ function useScroll() { scrollRef.current = new LocomotiveScroll({ el: containerRef.current, smooth: true, - lerp: 0.22, + lerp: 0.2, }); observerRef.current = new ResizeObserver( diff --git a/src/widgets/Header/ui/Logo.tsx b/src/widgets/Header/ui/Logo.tsx index a568aaf..aa0417a 100644 --- a/src/widgets/Header/ui/Logo.tsx +++ b/src/widgets/Header/ui/Logo.tsx @@ -2,7 +2,7 @@ import { LOGO_LETTERS } from '../const/const.ts'; function Logo() { return ( -

                      +

                      {LOGO_LETTERS.map((letter, i) => ( Date: Tue, 31 Oct 2023 16:51:51 +0200 Subject: [PATCH 097/144] refactor: remove error button --- src/pages/MainLayout.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 1432a94..c04759c 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,10 +1,7 @@ -import { useState } from 'react'; - import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/ProductList/MovieList.tsx'; import Search from '../features/Search/Search.tsx'; import useScroll from '../shared/hooks/useScroll.ts'; -import Button from '../shared/ui/Button.tsx'; import Spinner from '../shared/ui/Spinner.tsx'; import Header from '../widgets/Header/Header.tsx'; import Logo from '../widgets/Header/ui/Logo.tsx'; @@ -12,11 +9,8 @@ import TotalResults from '../widgets/Header/ui/TotalResults.tsx'; import Main from '../widgets/Main/Main.tsx'; function MainLayout() { - const [isError, setIsError] = useState(false); const containerRef = useScroll(); - if (isError) throw new Error('test error'); - return ( <> @@ -28,12 +22,6 @@ function MainLayout() {
                      -
                      From fbed2536e6278f6f37aa401adbce2347d89753c6 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 31 Oct 2023 19:27:59 +0200 Subject: [PATCH 098/144] chore: add react router --- package-lock.json | 41 ++++++++++++++++++++++++++++++++++++++++- package.json | 3 ++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e94b5d..14a9d3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "locomotive-scroll": "^4.1.4", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.18.0" }, "devDependencies": { "@types/locomotive-scroll": "^4.1.2", @@ -1005,6 +1006,14 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@remix-run/router": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", + "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.3", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", @@ -5084,6 +5093,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz", + "integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==", + "dependencies": { + "@remix-run/router": "1.11.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz", + "integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==", + "dependencies": { + "@remix-run/router": "1.11.0", + "react-router": "6.18.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index fb0c547..d314422 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "dependencies": { "locomotive-scroll": "^4.1.4", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.18.0" }, "devDependencies": { "@types/locomotive-scroll": "^4.1.2", From 3ecbe68954c60fe94ed10ada7e519d47a921bc36 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 31 Oct 2023 19:29:39 +0200 Subject: [PATCH 099/144] feat: add pagination --- src/app/App.tsx | 14 +++--- src/entities/movie/api/apiMovie.ts | 11 +++-- src/entities/movie/ui/Movie.tsx | 35 ++++++++------- .../{ProductList => MovieList}/MovieList.tsx | 16 ++++--- .../ui/NotFound.tsx | 0 src/features/MovieList/ui/Pagination.tsx | 43 +++++++++++++++++++ src/features/Search/Search.tsx | 13 ++++-- .../Search/context/SearchProvider.tsx | 22 +++++++--- src/features/Search/types/types.ts | 2 +- src/pages/MainLayout.tsx | 2 +- src/shared/const/const.ts | 2 + src/shared/hooks/useUrl.ts | 25 +++++++++++ src/shared/ui/Button.tsx | 7 ++- 13 files changed, 148 insertions(+), 44 deletions(-) rename src/features/{ProductList => MovieList}/MovieList.tsx (53%) rename src/features/{ProductList => MovieList}/ui/NotFound.tsx (100%) create mode 100644 src/features/MovieList/ui/Pagination.tsx create mode 100644 src/shared/hooks/useUrl.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index 2a6ed48..15230ab 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,3 +1,5 @@ +import { HashRouter } from 'react-router-dom'; + import SearchProvider from '../features/Search/context/SearchProvider.tsx'; import MainLayout from '../pages/MainLayout.tsx'; import ErrorBoundary from '../shared/ui/ErrorBoundary.tsx'; @@ -5,11 +7,13 @@ import FallbackUi from '../shared/ui/FallbackUi.tsx'; function App() { return ( - }> - - - - + + }> + + + + + ); } diff --git a/src/entities/movie/api/apiMovie.ts b/src/entities/movie/api/apiMovie.ts index 8645c90..0916b07 100644 --- a/src/entities/movie/api/apiMovie.ts +++ b/src/entities/movie/api/apiMovie.ts @@ -1,4 +1,8 @@ -import { API_URL, QUERY_FALLBACK } from '../../../shared/const/const.ts'; +import { + API_URL, + DEFAULT_PAGE, + QUERY_FALLBACK, +} from '../../../shared/const/const.ts'; import { ApiErrorResponse, ApiMovieListResponse, @@ -6,9 +10,10 @@ import { } from '../../../shared/types/types.ts'; export async function getMovieList( - query: string, + query: string = QUERY_FALLBACK, + page: number = DEFAULT_PAGE, ): Promise { - const response = await fetch(`${API_URL}&s=${query || QUERY_FALLBACK}`); + const response = await fetch(`${API_URL}&s=${query}&page=${page}`); if (!response.ok) throw new Error('Something went wrong fetching movies!'); diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index b2f8cca..b0eb396 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,10 +1,9 @@ -import { MouseEvent, useEffect, useRef, useState } from 'react'; +import { MouseEvent, useRef } from 'react'; import ReactLogo from '../../../assets/reactJS-logo.png'; import { NOT_EXIST } from '../../../shared/const/const.ts'; import createRadialHover from '../../../shared/lib/helpers/animateRadialHover.ts'; import { Movie as MovieData } from '../../../shared/types/types.ts'; -import { getMovie } from '../api/apiMovie.ts'; interface IMovieProps { data: MovieData; @@ -14,8 +13,8 @@ interface IMovieProps { // TODO - divide the component into smaller ones function Movie({ data, delay }: IMovieProps) { - const [description, setDescription] = useState(''); - const [genre, setGenre] = useState(''); + // const [description, setDescription] = useState(''); + // const [genre, setGenre] = useState(''); const movieRef = useRef(null); const containerRef = useRef(null); @@ -36,18 +35,18 @@ function Movie({ data, delay }: IMovieProps) { if (movieRef.current) cleanUp(movieRef.current as HTMLDivElement); }; - useEffect(() => { - async function fetchMovies() { - const movieData = await getMovie(data.imdbID); - const slicedGenre = movieData.Genre.split(', ').slice(0, 2).join('/'); - const slicedDescription = `${movieData.Plot.slice(0, 38)}...`; - - setDescription(slicedDescription); - setGenre(slicedGenre); - } - - void fetchMovies(); - }, [data.imdbID]); + // useEffect(() => { + // async function fetchMovies() { + // const movieData = await getMovie(data.imdbID); + // const slicedGenre = movieData.Genre.split(', ').slice(0, 2).join('/'); + // const slicedDescription = `${movieData.Plot.slice(0, 38)}...`; + // + // setDescription(slicedDescription); + // setGenre(slicedGenre); + // } + // + // void fetchMovies(); + // }, [data.imdbID]); return (
                    • {Title}

                      -

                      {description}

                      - {genre} + {/*

                      {description}

                      */} + {/* {genre} */} {Year}
                    • diff --git a/src/features/ProductList/MovieList.tsx b/src/features/MovieList/MovieList.tsx similarity index 53% rename from src/features/ProductList/MovieList.tsx rename to src/features/MovieList/MovieList.tsx index 5c55a0e..545685c 100644 --- a/src/features/ProductList/MovieList.tsx +++ b/src/features/MovieList/MovieList.tsx @@ -1,20 +1,26 @@ import NotFound from './ui/NotFound.tsx'; +import Pagination from './ui/Pagination.tsx'; import Movie from '../../entities/movie/ui/Movie.tsx'; import useSearch from '../Search/hooks/useSearch.ts'; function MovieList() { const { movies, isLoading } = useSearch(); + const isNoMovies = !movies?.length; if (isNoMovies && !isLoading) return ; if (isNoMovies) return null; return ( -
                        - {movies.map((movie, i) => ( - - ))} -
                      + <> +
                        + {movies.map((movie, i) => ( + + ))} +
                      + + + ); } diff --git a/src/features/ProductList/ui/NotFound.tsx b/src/features/MovieList/ui/NotFound.tsx similarity index 100% rename from src/features/ProductList/ui/NotFound.tsx rename to src/features/MovieList/ui/NotFound.tsx diff --git a/src/features/MovieList/ui/Pagination.tsx b/src/features/MovieList/ui/Pagination.tsx new file mode 100644 index 0000000..200fc80 --- /dev/null +++ b/src/features/MovieList/ui/Pagination.tsx @@ -0,0 +1,43 @@ +import { useCallback } from 'react'; + +import { DEFAULT_PAGE, PAGE_PARAM } from '../../../shared/const/const.ts'; +import useUrl from '../../../shared/hooks/useUrl.ts'; +import Button from '../../../shared/ui/Button.tsx'; +import useSearch from '../../Search/hooks/useSearch.ts'; + +function Pagination() { + const { setUrl, readUrl } = useUrl(); + const { fetchMovies, query } = useSearch(); + + const currPage = Number(readUrl(PAGE_PARAM)); + const isPrevDisabled = currPage === 1; + const isPage = Boolean(currPage); + + const handleNextPage = useCallback(() => { + const newPage = currPage + 1; + + setUrl(PAGE_PARAM, isPage ? String(newPage) : String(DEFAULT_PAGE)); + + fetchMovies(query, newPage); + }, [isPage, setUrl, fetchMovies, query, currPage]); + + const handlePrevPage = useCallback(() => { + if (!isPage || currPage === 1) return; + + const newPage = currPage - 1; + + setUrl(PAGE_PARAM, String(newPage)); + fetchMovies(query, newPage); + }, [isPage, currPage, setUrl, fetchMovies, query]); + + return ( +
                      + + +
                      + ); +} + +export default Pagination; diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 295192c..721536e 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -5,9 +5,14 @@ import { useCallback, useRef } from 'react'; import { ENTER_KEY, ESCAPE_KEY } from './const/const.ts'; import useSearch from './hooks/useSearch.ts'; import searchIcon from '../../assets/search.svg'; -import { LOCAL_STORAGE_SEARCH_QUERY } from '../../shared/const/const.ts'; +import { + DEFAULT_PAGE, + LOCAL_STORAGE_SEARCH_QUERY, + PAGE_PARAM, +} from '../../shared/const/const.ts'; import useKey from '../../shared/hooks/useKey.ts'; import useLocalStorageState from '../../shared/hooks/useLocalStorageState.ts'; +import useUrl from '../../shared/hooks/useUrl.ts'; import Button from '../../shared/ui/Button.tsx'; function Search() { @@ -17,15 +22,17 @@ function Search() { ); const inputRef = useRef(null); const { fetchMovies } = useSearch(); + const { setUrl } = useUrl(); useKey(ENTER_KEY, handleEnter); useKey(ESCAPE_KEY, handleEscape); const handleSearch = useCallback( async (newQuery: string) => { + setUrl(PAGE_PARAM, String(DEFAULT_PAGE)); fetchMovies(newQuery.trim()); }, - [fetchMovies], + [fetchMovies, setUrl], ); function handleEnter() { @@ -64,7 +71,7 @@ function Search() { /> - -
                      + +

                  ); } diff --git a/src/features/Pagination/ui/Button.tsx b/src/features/Pagination/ui/Button.tsx new file mode 100644 index 0000000..14171bd --- /dev/null +++ b/src/features/Pagination/ui/Button.tsx @@ -0,0 +1,47 @@ +interface IButtonProps { + disabled: boolean; + onClick: () => void; + position: 'left' | 'right'; + children: string; +} + +function Button({ disabled, onClick, position, children }: IButtonProps) { + const isLeftPosition = position === 'left'; + const btnPosition = isLeftPosition + ? 'left-0 md:-left-16' + : 'right-0 md:-right-16'; + const arrowDirectionMain = isLeftPosition + ? 'group-hover:-translate-x-14' + : 'group-hover:translate-x-14'; + const arrowDirectionSecondSnooze = isLeftPosition + ? 'translate-x-14' + : '-translate-x-14'; + + return ( + + ); +} + +export default Button; diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 671ca2c..d7e03b1 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,5 +1,6 @@ import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/MovieList/MovieList.tsx'; +import Pagination from '../features/Pagination/Pagination.tsx'; import Search from '../features/Search/Search.tsx'; import useScroll from '../shared/hooks/useScroll.ts'; import Spinner from '../shared/ui/Spinner.tsx'; @@ -14,10 +15,8 @@ function MainLayout() { return ( <> -
                  -
                  @@ -27,6 +26,7 @@ function MainLayout() {
                  +
                  diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Spinner.tsx index fbb28bf..eb7f587 100644 --- a/src/shared/ui/Spinner.tsx +++ b/src/shared/ui/Spinner.tsx @@ -8,7 +8,7 @@ function Spinner() { if (!isLoading) return null; return createPortal( -
                  +
                  , document.body, diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 1a8bf8e..796e2e3 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -3,7 +3,7 @@ import { IChildren } from '../../shared/types/interfaces.ts'; function Header({ children }: IChildren) { return (
                  {children} diff --git a/src/widgets/Main/Main.tsx b/src/widgets/Main/Main.tsx index 9fc8f1e..242b189 100644 --- a/src/widgets/Main/Main.tsx +++ b/src/widgets/Main/Main.tsx @@ -2,7 +2,7 @@ import { IChildren } from '../../shared/types/interfaces.ts'; function Main({ children }: IChildren) { return ( -
                  +
                  {children}
                  ); diff --git a/tailwind.config.js b/tailwind.config.js index 821b027..786fc32 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -26,6 +26,16 @@ export default { translate: '0 0', }, }, + 'pagination-fade-in': { + '0%': { + scale: '0.3', + opacity: 0, + }, + '100%': { + scale: '1', + opacity: 1, + }, + }, float: { '0%': { translate3d: '0', @@ -53,7 +63,9 @@ export default { }, }, animation: { - 'fade-in': 'fade-in .5s cubic-bezier(0.86, 0, 0.07, 1) both', + 'fade-in': 'fade-in 1s cubic-bezier(0.86, 0, 0.07, 1) both', + 'pagination-fade-in': + 'Pagination-fade-in 1.4s cubic-bezier(.25,1.55,.6,1) both', springish: 'springish 1.72s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', 'springish-letter': From 74cabd5c7e3984e577c37ef00f4b0a1201f60150 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 1 Nov 2023 22:44:01 +0200 Subject: [PATCH 101/144] refactor: pagination button click timing --- src/features/Pagination/ui/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/Pagination/ui/Button.tsx b/src/features/Pagination/ui/Button.tsx index 14171bd..96b9787 100644 --- a/src/features/Pagination/ui/Button.tsx +++ b/src/features/Pagination/ui/Button.tsx @@ -26,7 +26,7 @@ function Button({ disabled, onClick, position, children }: IButtonProps) { disabled={disabled} onClick={onClick}>
                  -
                  +
                  Date: Thu, 2 Nov 2023 10:36:22 +0200 Subject: [PATCH 102/144] feat: implement scroll to top on list change --- src/features/MovieList/MovieList.tsx | 12 +++++++++++- src/features/Pagination/Pagination.tsx | 7 ++----- src/pages/MainLayout.tsx | 16 +++++++++++++--- src/shared/hooks/useScroll.ts | 7 +++++-- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/features/MovieList/MovieList.tsx b/src/features/MovieList/MovieList.tsx index e23ab03..f3e831a 100644 --- a/src/features/MovieList/MovieList.tsx +++ b/src/features/MovieList/MovieList.tsx @@ -1,12 +1,22 @@ +import { useEffect } from 'react'; + import NotFound from './ui/NotFound.tsx'; import Movie from '../../entities/movie/ui/Movie.tsx'; import useSearch from '../Search/hooks/useSearch.ts'; -function MovieList() { +interface IMovieListProps { + onScrollTop: () => void; +} + +function MovieList({ onScrollTop }: IMovieListProps) { const { movies, isLoading } = useSearch(); const isNoMovies = !movies?.length; + useEffect(() => { + onScrollTop(); + }, [onScrollTop, movies]); + if (isNoMovies && !isLoading) return ; if (isNoMovies) return null; diff --git a/src/features/Pagination/Pagination.tsx b/src/features/Pagination/Pagination.tsx index 429849b..4ed374d 100644 --- a/src/features/Pagination/Pagination.tsx +++ b/src/features/Pagination/Pagination.tsx @@ -33,10 +33,7 @@ function Pagination() { }, [isPage, currPage, setUrl, fetchMovies, query]); return ( -
                  + <> -
                  + ); } diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index d7e03b1..4dd0ff6 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,3 +1,5 @@ +import { useCallback } from 'react'; + import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/MovieList/MovieList.tsx'; import Pagination from '../features/Pagination/Pagination.tsx'; @@ -10,7 +12,15 @@ import TotalResults from '../widgets/Header/ui/TotalResults.tsx'; import Main from '../widgets/Main/Main.tsx'; function MainLayout() { - const containerRef = useScroll(); + const [containerRef, scrollRef] = useScroll(); + + const handleScrollTop = useCallback( + function handleScrollTop() { + if (scrollRef.current) + scrollRef.current.scrollTo('top', { duration: 100 }); + }, + [scrollRef], + ); return ( <> @@ -24,9 +34,9 @@ function MainLayout() {
                  - + +
                  -
                  diff --git a/src/shared/hooks/useScroll.ts b/src/shared/hooks/useScroll.ts index a0bbd18..e124038 100644 --- a/src/shared/hooks/useScroll.ts +++ b/src/shared/hooks/useScroll.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { RefObject, useEffect, useRef } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; @@ -29,7 +29,10 @@ function useScroll() { }; }, []); - return containerRef; + return [containerRef, scrollRef] as [ + RefObject, + RefObject, + ]; } export default useScroll; From 2f1e3871045c1005e311b22c18debea8cda7770d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 2 Nov 2023 11:09:58 +0200 Subject: [PATCH 103/144] refactor: encapsulate scroll top into separate hook --- src/features/MovieList/MovieList.tsx | 14 +++++++------- src/pages/MainLayout.tsx | 12 +----------- src/shared/hooks/useScrollTop.ts | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 src/shared/hooks/useScrollTop.ts diff --git a/src/features/MovieList/MovieList.tsx b/src/features/MovieList/MovieList.tsx index f3e831a..db7d9a2 100644 --- a/src/features/MovieList/MovieList.tsx +++ b/src/features/MovieList/MovieList.tsx @@ -1,22 +1,22 @@ -import { useEffect } from 'react'; +import { RefObject } from 'react'; + +import LocomotiveScroll from 'locomotive-scroll'; import NotFound from './ui/NotFound.tsx'; import Movie from '../../entities/movie/ui/Movie.tsx'; +import useScrollTop from '../../shared/hooks/useScrollTop.ts'; import useSearch from '../Search/hooks/useSearch.ts'; interface IMovieListProps { - onScrollTop: () => void; + scroll: RefObject; } -function MovieList({ onScrollTop }: IMovieListProps) { +function MovieList({ scroll }: IMovieListProps) { const { movies, isLoading } = useSearch(); + useScrollTop(scroll, movies); const isNoMovies = !movies?.length; - useEffect(() => { - onScrollTop(); - }, [onScrollTop, movies]); - if (isNoMovies && !isLoading) return ; if (isNoMovies) return null; diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 4dd0ff6..19e9df6 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -1,5 +1,3 @@ -import { useCallback } from 'react'; - import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/MovieList/MovieList.tsx'; import Pagination from '../features/Pagination/Pagination.tsx'; @@ -14,14 +12,6 @@ import Main from '../widgets/Main/Main.tsx'; function MainLayout() { const [containerRef, scrollRef] = useScroll(); - const handleScrollTop = useCallback( - function handleScrollTop() { - if (scrollRef.current) - scrollRef.current.scrollTo('top', { duration: 100 }); - }, - [scrollRef], - ); - return ( <> @@ -34,7 +24,7 @@ function MainLayout() {
                  - +
                  diff --git a/src/shared/hooks/useScrollTop.ts b/src/shared/hooks/useScrollTop.ts new file mode 100644 index 0000000..6151315 --- /dev/null +++ b/src/shared/hooks/useScrollTop.ts @@ -0,0 +1,15 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { DependencyList, RefObject, useEffect } from 'react'; + +import LocomotiveScroll from 'locomotive-scroll'; + +function useScrollTop( + scroll: RefObject, + ...deps: DependencyList +) { + useEffect(() => { + scroll.current?.scrollTo('top', { duration: 100 }); + }, [scroll, ...deps]); +} + +export default useScrollTop; From 96d9cdca8ae8505de4b50cb5783c01b5181cb847 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 2 Nov 2023 11:31:35 +0200 Subject: [PATCH 104/144] fix: adaptive issues --- src/pages/MainLayout.tsx | 4 +++- src/shared/hooks/useScroll.ts | 6 +++++- tailwind.config.js | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 19e9df6..87070fc 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -25,8 +25,10 @@ function MainLayout() {
                  -
                  +
                  + +
                  diff --git a/src/shared/hooks/useScroll.ts b/src/shared/hooks/useScroll.ts index e124038..9b14ec6 100644 --- a/src/shared/hooks/useScroll.ts +++ b/src/shared/hooks/useScroll.ts @@ -9,11 +9,15 @@ function useScroll() { useEffect(() => { if (containerRef.current) { - containerRef.current.setAttribute('data-scroll-container', 'true'); + containerRef.current.setAttribute('data-Scroll-container', 'true'); scrollRef.current = new LocomotiveScroll({ el: containerRef.current, smooth: true, + smartphone: { + smooth: true, + }, + touchMultiplier: 6, lerp: 0.2, }); diff --git a/tailwind.config.js b/tailwind.config.js index 786fc32..76938c4 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -65,7 +65,7 @@ export default { animation: { 'fade-in': 'fade-in 1s cubic-bezier(0.86, 0, 0.07, 1) both', 'pagination-fade-in': - 'Pagination-fade-in 1.4s cubic-bezier(.25,1.55,.6,1) both', + 'pagination-fade-in 1.4s cubic-bezier(.25,1.55,.6,1) both', springish: 'springish 1.72s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', 'springish-letter': From 01c81928168e14c32fc113087b84c23333ca9993 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 2 Nov 2023 11:39:15 +0200 Subject: [PATCH 105/144] refactor: minor changes --- src/features/Pagination/Pagination.tsx | 4 ++-- src/features/Pagination/ui/Button.tsx | 2 -- src/pages/MainLayout.tsx | 4 +--- src/widgets/Header/Header.tsx | 5 +---- tailwind.config.js | 2 +- 5 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/features/Pagination/Pagination.tsx b/src/features/Pagination/Pagination.tsx index 4ed374d..baff8b4 100644 --- a/src/features/Pagination/Pagination.tsx +++ b/src/features/Pagination/Pagination.tsx @@ -33,7 +33,7 @@ function Pagination() { }, [isPage, currPage, setUrl, fetchMovies, query]); return ( - <> +
                  - +
                  ); } diff --git a/src/features/Pagination/ui/Button.tsx b/src/features/Pagination/ui/Button.tsx index 96b9787..2f7ce37 100644 --- a/src/features/Pagination/ui/Button.tsx +++ b/src/features/Pagination/ui/Button.tsx @@ -19,8 +19,6 @@ function Button({ disabled, onClick, position, children }: IButtonProps) { return (
                  diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 796e2e3..6596d61 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -2,10 +2,7 @@ import { IChildren } from '../../shared/types/interfaces.ts'; function Header({ children }: IChildren) { return ( -
                  +
                  {children}
                  ); diff --git a/tailwind.config.js b/tailwind.config.js index 76938c4..6f0e83c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -65,7 +65,7 @@ export default { animation: { 'fade-in': 'fade-in 1s cubic-bezier(0.86, 0, 0.07, 1) both', 'pagination-fade-in': - 'pagination-fade-in 1.4s cubic-bezier(.25,1.55,.6,1) both', + 'pagination-fade-in 1s cubic-bezier(.25,1.55,1,1) both', springish: 'springish 1.72s cubic-bezier(0.445, 0.050, 0.550, 0.950) both', 'springish-letter': From a3efa8061a6d9226a9734ccec49193d2d75fd16f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 2 Nov 2023 13:25:38 +0200 Subject: [PATCH 106/144] refactor: change scroll container --- index.html | 2 +- src/entities/movie/ui/Movie.tsx | 1 + src/features/MovieList/MovieList.tsx | 4 +++- src/features/Pagination/Pagination.tsx | 2 +- src/features/Pagination/ui/Button.tsx | 2 +- src/pages/MainLayout.tsx | 2 ++ src/shared/hooks/useScrollTop.ts | 4 ++-- src/widgets/Main/Main.tsx | 6 +----- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/index.html b/index.html index 9c50dab..78c9dfe 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,7 @@ Cinemania | Dive into Movie Wonderland - +
                  diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index b0eb396..46328e4 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -50,6 +50,7 @@ function Movie({ data, delay }: IMovieProps) { return (
                • +
                    {movies.map((movie, i) => ( ))} diff --git a/src/features/Pagination/Pagination.tsx b/src/features/Pagination/Pagination.tsx index baff8b4..5abd09b 100644 --- a/src/features/Pagination/Pagination.tsx +++ b/src/features/Pagination/Pagination.tsx @@ -33,7 +33,7 @@ function Pagination() { }, [isPage, currPage, setUrl, fetchMovies, query]); return ( -
                    +
                • +
                  diff --git a/src/shared/hooks/useScrollTop.ts b/src/shared/hooks/useScrollTop.ts index 6151315..3adc541 100644 --- a/src/shared/hooks/useScrollTop.ts +++ b/src/shared/hooks/useScrollTop.ts @@ -4,11 +4,11 @@ import { DependencyList, RefObject, useEffect } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; function useScrollTop( - scroll: RefObject, + scroll: RefObject | undefined, ...deps: DependencyList ) { useEffect(() => { - scroll.current?.scrollTo('top', { duration: 100 }); + scroll?.current?.scrollTo('top', { duration: 100 }); }, [scroll, ...deps]); } diff --git a/src/widgets/Main/Main.tsx b/src/widgets/Main/Main.tsx index 242b189..7c90448 100644 --- a/src/widgets/Main/Main.tsx +++ b/src/widgets/Main/Main.tsx @@ -1,11 +1,7 @@ import { IChildren } from '../../shared/types/interfaces.ts'; function Main({ children }: IChildren) { - return ( -
                  - {children} -
                  - ); + return
                  {children}
                  ; } export default Main; From 744bd50f60d8fc88d2eef6c5ef19f4488f70c0d0 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 2 Nov 2023 17:42:19 +0200 Subject: [PATCH 107/144] feat: add movie details --- src/entities/movie/ui/Movie.tsx | 41 +++++------- src/features/MovieList/MovieList.tsx | 2 +- src/pages/MainLayout.tsx | 5 +- src/shared/const/const.ts | 1 + src/shared/hooks/useScroll.ts | 18 +++-- src/shared/hooks/useScrollTop.ts | 15 ++++- src/shared/ui/Modal.tsx | 2 +- src/widgets/Main/Main.tsx | 4 +- src/widgets/MovieDetails/MovieDetails.tsx | 81 +++++++++++++++++++++++ tailwind.config.js | 4 ++ 10 files changed, 137 insertions(+), 36 deletions(-) create mode 100644 src/widgets/MovieDetails/MovieDetails.tsx diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 46328e4..6041098 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,7 +1,8 @@ -import { MouseEvent, useRef } from 'react'; +import { MouseEvent, useCallback, useRef } from 'react'; import ReactLogo from '../../../assets/reactJS-logo.png'; -import { NOT_EXIST } from '../../../shared/const/const.ts'; +import { MOVIE_PARAM, NOT_EXIST } from '../../../shared/const/const.ts'; +import useUrl from '../../../shared/hooks/useUrl.ts'; import createRadialHover from '../../../shared/lib/helpers/animateRadialHover.ts'; import { Movie as MovieData } from '../../../shared/types/types.ts'; @@ -11,15 +12,14 @@ interface IMovieProps { } // TODO - divide the component into smaller ones +// TODO - encapsulate radial hover into separate hook function Movie({ data, delay }: IMovieProps) { - // const [description, setDescription] = useState(''); - // const [genre, setGenre] = useState(''); - const movieRef = useRef(null); const containerRef = useRef(null); + const { setUrl } = useUrl(); - const { Poster, Title, Year } = data; + const { Poster, Title, Year, imdbID } = data; const isPosterExist = Poster !== NOT_EXIST; const poster = isPosterExist ? Poster : ReactLogo; @@ -32,46 +32,37 @@ function Movie({ data, delay }: IMovieProps) { }; const handleMouseOut = () => { - if (movieRef.current) cleanUp(movieRef.current as HTMLDivElement); + if (movieRef.current) cleanUp(movieRef.current); }; - // useEffect(() => { - // async function fetchMovies() { - // const movieData = await getMovie(data.imdbID); - // const slicedGenre = movieData.Genre.split(', ').slice(0, 2).join('/'); - // const slicedDescription = `${movieData.Plot.slice(0, 38)}...`; - // - // setDescription(slicedDescription); - // setGenre(slicedGenre); - // } - // - // void fetchMovies(); - // }, [data.imdbID]); + const handleMovieClick = useCallback(() => { + setUrl(MOVIE_PARAM, imdbID); + }, [imdbID, setUrl]); return ( + // TODO - fix errors + // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
                • + className="w-64 animate-springish cursor-pointer overflow-hidden rounded-5xl bg-neutral-950 text-gray-100 transition-all duration-200">
                  + className="h-full space-y-4 rounded-4xl p-2"> {`The

                  {Title}

                  - {/*

                  {description}

                  */} - {/* {genre} */} {Year}
                  diff --git a/src/features/MovieList/MovieList.tsx b/src/features/MovieList/MovieList.tsx index 8502e35..1707551 100644 --- a/src/features/MovieList/MovieList.tsx +++ b/src/features/MovieList/MovieList.tsx @@ -23,7 +23,7 @@ function MovieList({ scroll }: IMovieListProps) { return (
                    + className="m-auto mb-8 flex max-w-6xl animate-fade-in flex-wrap items-center justify-between gap-6 last:m-auto sm:gap-10"> {movies.map((movie, i) => ( ))} diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index f706068..9b5397f 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -11,7 +11,10 @@ import Main from '../widgets/Main/Main.tsx'; import MovieDetails from '../widgets/MovieDetails/MovieDetails.tsx'; function MainLayout() { - const [containerRef, scrollRef] = useScroll(); + const [containerRef, scrollRef] = useScroll< + HTMLDivElement, + HTMLUListElement + >(); return ( <> diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts index 997998d..d7f0b65 100644 --- a/src/shared/const/const.ts +++ b/src/shared/const/const.ts @@ -4,4 +4,5 @@ export const QUERY_FALLBACK = 'all'; export const NOT_EXIST = 'N/A'; export const LOCAL_STORAGE_SEARCH_QUERY = 'search-query'; export const PAGE_PARAM = 'page'; +export const MOVIE_PARAM = 'movie'; export const DEFAULT_PAGE = 1; diff --git a/src/shared/hooks/useScroll.ts b/src/shared/hooks/useScroll.ts index 9b14ec6..0fd8da8 100644 --- a/src/shared/hooks/useScroll.ts +++ b/src/shared/hooks/useScroll.ts @@ -1,11 +1,17 @@ -import { RefObject, useEffect, useRef } from 'react'; +import { MutableRefObject, RefObject, useEffect, useRef } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; -function useScroll() { - const containerRef = useRef(null); +// TODO - change return value from array to object + +function useScroll< + TContainer extends HTMLElement, + TScrollbar extends HTMLElement, +>() { + const containerRef = useRef(); const scrollRef = useRef(); const observerRef = useRef(); + const scrollbarRef = useRef(); useEffect(() => { if (containerRef.current) { @@ -19,6 +25,7 @@ function useScroll() { }, touchMultiplier: 6, lerp: 0.2, + scrollbarContainer: scrollbarRef.current ?? false, }); observerRef.current = new ResizeObserver( @@ -33,9 +40,10 @@ function useScroll() { }; }, []); - return [containerRef, scrollRef] as [ - RefObject, + return [containerRef, scrollRef, scrollbarRef] as [ + MutableRefObject, RefObject, + MutableRefObject, ]; } diff --git a/src/shared/hooks/useScrollTop.ts b/src/shared/hooks/useScrollTop.ts index 3adc541..d52cfd5 100644 --- a/src/shared/hooks/useScrollTop.ts +++ b/src/shared/hooks/useScrollTop.ts @@ -1,14 +1,25 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { DependencyList, RefObject, useEffect } from 'react'; +import { DependencyList, RefObject, useEffect, useRef } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; +import useUrl from './useUrl.ts'; +import { PAGE_PARAM } from '../const/const.ts'; + function useScrollTop( scroll: RefObject | undefined, ...deps: DependencyList ) { + const { readUrl } = useUrl(); + const prevPageRef = useRef(readUrl(PAGE_PARAM)); + useEffect(() => { - scroll?.current?.scrollTo('top', { duration: 100 }); + const currPage = readUrl(PAGE_PARAM) as string; + + if (prevPageRef.current !== currPage) { + prevPageRef.current = currPage; + scroll?.current?.scrollTo('top', { duration: 100 }); + } }, [scroll, ...deps]); } diff --git a/src/shared/ui/Modal.tsx b/src/shared/ui/Modal.tsx index ee30f36..439e8a4 100644 --- a/src/shared/ui/Modal.tsx +++ b/src/shared/ui/Modal.tsx @@ -21,7 +21,7 @@ function Modal({ className = '', children }: IModalProps) { }; return ( -
                    +
                    {children}; + return ( +
                    {children}
                    + ); } export default Main; diff --git a/src/widgets/MovieDetails/MovieDetails.tsx b/src/widgets/MovieDetails/MovieDetails.tsx new file mode 100644 index 0000000..d1caef7 --- /dev/null +++ b/src/widgets/MovieDetails/MovieDetails.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from 'react'; + +import ReactLogo from '../../assets/reactJS-logo.png'; +import { getMovie } from '../../entities/movie/api/apiMovie.ts'; +import { MOVIE_PARAM, NOT_EXIST } from '../../shared/const/const.ts'; +import useUrl from '../../shared/hooks/useUrl.ts'; +import { ApiMovieResponse } from '../../shared/types/types.ts'; + +// TODO - encapsulate useEffect + +function MovieDetails() { + const { readUrl } = useUrl(); + const [movie, setMovie] = useState(null); + + useEffect(() => { + async function fetchMovies() { + const id = readUrl(MOVIE_PARAM); + + if (id) { + const movieData = await getMovie(id); + setMovie(movieData); + } + } + + void fetchMovies(); + }, [readUrl]); + + if (movie === null) return null; + + const { + Poster, + Title, + Runtime, + Genre, + Plot, + Year, + imdbRating, + imdbVotes, + Director, + Actors, + } = movie; + + const hrs = Math.floor(Number(Runtime.slice(0, -4)) / 60); + const min = Math.floor(Number(Runtime.slice(0, -4)) % 60); + const time = `${hrs}h ${min}m`; + const description = `${Plot.slice(0, 150)}...`; + const poster = Poster === NOT_EXIST ? ReactLogo : Poster; + + return ( +
                    +
                    + {`The +
                    +

                    {Title}

                    + + {time} | {Year} + + {Genre} + + ⭐{imdbRating}/10 | 🎟️{imdbVotes} + +

                    {description}

                    +

                    + Directed By: + {Director} +

                    +

                    + Cast: + {Actors} +

                    +
                    +
                    +
                    + ); +} + +export default MovieDetails; diff --git a/tailwind.config.js b/tailwind.config.js index 6f0e83c..70534ec 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -6,6 +6,10 @@ export default { transitionTimingFunction: { bounce: 'cubic-bezier(.25,1.55,.65,1.4)', }, + borderRadius: { + '4xl': '32px', + '5xl': '40px', + }, height: { screen: '100dvh', }, From 37f8b5c1fdbce373f3829653f9ea86c3bd570f92 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 2 Nov 2023 18:28:39 +0200 Subject: [PATCH 108/144] refactor: encapsulate fetching movie details --- src/entities/movie/hooks/useMovie.ts | 15 +++++ .../Search/context/SearchProvider.tsx | 55 ++++++++++++++++--- src/features/Search/types/types.ts | 16 +++++- src/widgets/MovieDetails/MovieDetails.tsx | 30 ++-------- 4 files changed, 82 insertions(+), 34 deletions(-) create mode 100644 src/entities/movie/hooks/useMovie.ts diff --git a/src/entities/movie/hooks/useMovie.ts b/src/entities/movie/hooks/useMovie.ts new file mode 100644 index 0000000..2ff63b3 --- /dev/null +++ b/src/entities/movie/hooks/useMovie.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; + +import useSearch from '../../../features/Search/hooks/useSearch.ts'; + +function useMovie() { + const { fetchMovie, movieDetails } = useSearch(); + + useEffect(() => { + fetchMovie(); + }, [fetchMovie]); + + return movieDetails; +} + +export default useMovie; diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx index 3973a1f..ec415ba 100644 --- a/src/features/Search/context/SearchProvider.tsx +++ b/src/features/Search/context/SearchProvider.tsx @@ -6,15 +6,19 @@ import { useReducer, } from 'react'; -import { getMovieList } from '../../../entities/movie/api/apiMovie.ts'; +import { + getMovie, + getMovieList, +} from '../../../entities/movie/api/apiMovie.ts'; import { DEFAULT_PAGE, LOCAL_STORAGE_SEARCH_QUERY, + MOVIE_PARAM, PAGE_PARAM, } from '../../../shared/const/const.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; import { IChildren } from '../../../shared/types/interfaces.ts'; -import { MovieList } from '../../../shared/types/types.ts'; +import { ApiMovieResponse, MovieList } from '../../../shared/types/types.ts'; import { NO_MOVIES, NO_RESULTS } from '../const/const.ts'; import { Action, @@ -28,6 +32,7 @@ const initialState: IInitialState = { movies: null, totalResults: 0, isLoading: false, + movieDetails: null, }; export const SearchContext = createContext({ @@ -50,16 +55,17 @@ function reducer(state: IInitialState, action: Action): IInitialState { case SearchActions.LOADING: return { ...state, isLoading: true }; + case SearchActions.MOVIE_DETAILS_UPDATED: + return { ...state, movieDetails: action.payload, isLoading: false }; + default: throw new Error('The action does not exist!'); } } function SearchProvider({ children }: IChildren) { - const [{ query, movies, totalResults, isLoading }, dispatch] = useReducer( - reducer, - initialState, - ); + const [{ query, movies, totalResults, isLoading, movieDetails }, dispatch] = + useReducer(reducer, initialState); const { readUrl, setUrl } = useUrl(); const updateQuery = useCallback((newQuery: string) => { @@ -79,6 +85,16 @@ function SearchProvider({ children }: IChildren) { [], ); + const updateMovieDetails = useCallback( + (newMovie: ApiMovieResponse | null) => { + dispatch({ + type: SearchActions.MOVIE_DETAILS_UPDATED, + payload: newMovie, + }); + }, + [], + ); + const fetchMovies = useCallback( async (searchQuery: string, page?: number) => { localStorage.setItem(LOCAL_STORAGE_SEARCH_QUERY, searchQuery); @@ -95,6 +111,20 @@ function SearchProvider({ children }: IChildren) { [updateMovies, updateQuery], ); + const fetchMovie = useCallback(async () => { + try { + dispatch({ type: SearchActions.LOADING }); + const id = readUrl(MOVIE_PARAM); + + if (id) { + const movie = await getMovie(id); + updateMovieDetails(movie); + } + } catch (e) { + updateMovieDetails(null); + } + }, [readUrl, updateMovieDetails]); + useEffect(() => { const storedQuery = localStorage.getItem(LOCAL_STORAGE_SEARCH_QUERY); const page = Number(readUrl(PAGE_PARAM)) || DEFAULT_PAGE; @@ -114,8 +144,19 @@ function SearchProvider({ children }: IChildren) { isLoading, updateQuery, fetchMovies, + fetchMovie, + movieDetails, }), - [fetchMovies, isLoading, movies, query, totalResults, updateQuery], + [ + fetchMovie, + fetchMovies, + isLoading, + movieDetails, + movies, + query, + totalResults, + updateQuery, + ], ); return ( diff --git a/src/features/Search/types/types.ts b/src/features/Search/types/types.ts index ab11949..a024d4f 100644 --- a/src/features/Search/types/types.ts +++ b/src/features/Search/types/types.ts @@ -1,14 +1,16 @@ -import { MovieList } from '../../../shared/types/types.ts'; +import { ApiMovieResponse, MovieList } from '../../../shared/types/types.ts'; export enum SearchActions { QUERY_UPDATED = 'search/queryUpdated', MOVIES_LOADED = 'search/moviesLoaded', LOADING = 'search/loading', + MOVIE_DETAILS_UPDATED = 'searc/movieDetailsUpdated', } export interface IInitialState { query: string; movies: MovieList | null; + movieDetails: ApiMovieResponse | null; totalResults: number; isLoading: boolean; } @@ -16,6 +18,7 @@ export interface IInitialState { export interface ISearchContext extends IInitialState { updateQuery: (newQuery: string) => void; fetchMovies: (searchQuery: string, page?: number) => void; + fetchMovie: () => void; } export interface IQueryAction { @@ -31,8 +34,17 @@ export interface IMoviesAction { }; } +export interface IMoviesDetailsAction { + type: SearchActions.MOVIE_DETAILS_UPDATED; + payload: ApiMovieResponse | null; +} + export interface ILoadingAction { type: SearchActions.LOADING; } -export type Action = IQueryAction | IMoviesAction | ILoadingAction; +export type Action = + | IQueryAction + | IMoviesAction + | ILoadingAction + | IMoviesDetailsAction; diff --git a/src/widgets/MovieDetails/MovieDetails.tsx b/src/widgets/MovieDetails/MovieDetails.tsx index d1caef7..4ab1924 100644 --- a/src/widgets/MovieDetails/MovieDetails.tsx +++ b/src/widgets/MovieDetails/MovieDetails.tsx @@ -1,29 +1,9 @@ -import { useEffect, useState } from 'react'; - import ReactLogo from '../../assets/reactJS-logo.png'; -import { getMovie } from '../../entities/movie/api/apiMovie.ts'; -import { MOVIE_PARAM, NOT_EXIST } from '../../shared/const/const.ts'; -import useUrl from '../../shared/hooks/useUrl.ts'; -import { ApiMovieResponse } from '../../shared/types/types.ts'; - -// TODO - encapsulate useEffect +import useMovie from '../../entities/movie/hooks/useMovie.ts'; +import { NOT_EXIST } from '../../shared/const/const.ts'; function MovieDetails() { - const { readUrl } = useUrl(); - const [movie, setMovie] = useState(null); - - useEffect(() => { - async function fetchMovies() { - const id = readUrl(MOVIE_PARAM); - - if (id) { - const movieData = await getMovie(id); - setMovie(movieData); - } - } - - void fetchMovies(); - }, [readUrl]); + const movie = useMovie(); if (movie === null) return null; @@ -47,14 +27,14 @@ function MovieDetails() { const poster = Poster === NOT_EXIST ? ReactLogo : Poster; return ( -
                    +
                    {`The -
                    +

                    {Title}

                    {time} | {Year} From c09c5914a9d8a28fc9bef76e0651062de9f2e0e9 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 2 Nov 2023 18:36:55 +0200 Subject: [PATCH 109/144] refactor: encapsulate radial hover in separate hook --- src/entities/movie/ui/Movie.tsx | 24 ++++++++---------------- src/shared/hooks/useRadialHover.ts | 20 ++++++++++++++++++++ src/shared/ui/Modal.tsx | 22 +++++++--------------- 3 files changed, 35 insertions(+), 31 deletions(-) create mode 100644 src/shared/hooks/useRadialHover.ts diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 6041098..d454b9b 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,9 +1,9 @@ -import { MouseEvent, useCallback, useRef } from 'react'; +import { useCallback, useRef } from 'react'; import ReactLogo from '../../../assets/reactJS-logo.png'; import { MOVIE_PARAM, NOT_EXIST } from '../../../shared/const/const.ts'; +import useRadialHover from '../../../shared/hooks/useRadialHover.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; -import createRadialHover from '../../../shared/lib/helpers/animateRadialHover.ts'; import { Movie as MovieData } from '../../../shared/types/types.ts'; interface IMovieProps { @@ -12,29 +12,21 @@ interface IMovieProps { } // TODO - divide the component into smaller ones -// TODO - encapsulate radial hover into separate hook function Movie({ data, delay }: IMovieProps) { - const movieRef = useRef(null); const containerRef = useRef(null); const { setUrl } = useUrl(); + const { + handleMouseOut, + handleMouseMove, + containerRef: movieRef, + } = useRadialHover(); const { Poster, Title, Year, imdbID } = data; - const isPosterExist = Poster !== NOT_EXIST; - const poster = isPosterExist ? Poster : ReactLogo; + const poster = Poster === NOT_EXIST ? ReactLogo : Poster; const animationDelay = `0.${String(delay)}s`; - const [radialHover, cleanUp] = createRadialHover(); - - const handleMouseMove = (e: MouseEvent) => { - if (movieRef.current) radialHover(movieRef.current, e); - }; - - const handleMouseOut = () => { - if (movieRef.current) cleanUp(movieRef.current); - }; - const handleMovieClick = useCallback(() => { setUrl(MOVIE_PARAM, imdbID); }, [imdbID, setUrl]); diff --git a/src/shared/hooks/useRadialHover.ts b/src/shared/hooks/useRadialHover.ts new file mode 100644 index 0000000..ecab46d --- /dev/null +++ b/src/shared/hooks/useRadialHover.ts @@ -0,0 +1,20 @@ +import { MouseEvent, useRef } from 'react'; + +import createRadialHover from '../lib/helpers/animateRadialHover.ts'; + +function useRadialHover() { + const containerRef = useRef(null); + const [radialHover, cleanUp] = createRadialHover(); + + const handleMouseMove = (e: MouseEvent) => { + if (containerRef.current) radialHover(containerRef.current, e); + }; + + const handleMouseOut = () => { + if (containerRef.current) cleanUp(containerRef.current); + }; + + return { handleMouseMove, handleMouseOut, containerRef }; +} + +export default useRadialHover; diff --git a/src/shared/ui/Modal.tsx b/src/shared/ui/Modal.tsx index 439e8a4..fbaafcc 100644 --- a/src/shared/ui/Modal.tsx +++ b/src/shared/ui/Modal.tsx @@ -1,6 +1,4 @@ -import { MouseEvent, useRef } from 'react'; - -import createRadialHover from '../lib/helpers/animateRadialHover.ts'; +import useRadialHover from '../hooks/useRadialHover.ts'; import { IChildren } from '../types/interfaces.ts'; interface IModalProps extends IChildren { @@ -8,20 +6,14 @@ interface IModalProps extends IChildren { } function Modal({ className = '', children }: IModalProps) { - const modalRef = useRef(null); - - const [radialHover, cleanUp] = createRadialHover(); - - const handleMouseMove = (e: MouseEvent) => { - if (modalRef.current) radialHover(modalRef.current, e); - }; - - const handleMouseOut = () => { - if (modalRef.current) cleanUp(modalRef.current); - }; + const { + handleMouseOut, + handleMouseMove, + containerRef: modalRef, + } = useRadialHover(); return ( -
                    +
                    Date: Thu, 2 Nov 2023 18:41:32 +0200 Subject: [PATCH 110/144] refactor: change use scroll hook api --- src/pages/MainLayout.tsx | 5 +---- src/shared/hooks/useScroll.ts | 20 +++++++++----------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/pages/MainLayout.tsx b/src/pages/MainLayout.tsx index 9b5397f..1d8ac16 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/MainLayout.tsx @@ -11,10 +11,7 @@ import Main from '../widgets/Main/Main.tsx'; import MovieDetails from '../widgets/MovieDetails/MovieDetails.tsx'; function MainLayout() { - const [containerRef, scrollRef] = useScroll< - HTMLDivElement, - HTMLUListElement - >(); + const { containerRef, scrollRef } = useScroll(); return ( <> diff --git a/src/shared/hooks/useScroll.ts b/src/shared/hooks/useScroll.ts index 0fd8da8..c9b5b26 100644 --- a/src/shared/hooks/useScroll.ts +++ b/src/shared/hooks/useScroll.ts @@ -1,17 +1,15 @@ -import { MutableRefObject, RefObject, useEffect, useRef } from 'react'; +import { RefObject, useEffect, useRef } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; -// TODO - change return value from array to object - function useScroll< TContainer extends HTMLElement, - TScrollbar extends HTMLElement, + TScrollbar extends HTMLElement | void = void, >() { - const containerRef = useRef(); + const containerRef = useRef(null); const scrollRef = useRef(); const observerRef = useRef(); - const scrollbarRef = useRef(); + const scrollbarRef = useRef(null); useEffect(() => { if (containerRef.current) { @@ -40,11 +38,11 @@ function useScroll< }; }, []); - return [containerRef, scrollRef, scrollbarRef] as [ - MutableRefObject, - RefObject, - MutableRefObject, - ]; + return { containerRef, scrollRef, scrollbarRef } as { + containerRef: RefObject; + scrollRef: RefObject; + scrollbarRef: RefObject; + }; } export default useScroll; From b4d53d7764611fe9823837c3b481149c4b1559cb Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 2 Nov 2023 18:47:36 +0200 Subject: [PATCH 111/144] refactor: change to not show pagination on empty list --- src/features/Pagination/Pagination.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/features/Pagination/Pagination.tsx b/src/features/Pagination/Pagination.tsx index 5abd09b..e481ddb 100644 --- a/src/features/Pagination/Pagination.tsx +++ b/src/features/Pagination/Pagination.tsx @@ -9,11 +9,12 @@ import useSearch from '../Search/hooks/useSearch.ts'; function Pagination() { const { setUrl, readUrl } = useUrl(); - const { fetchMovies, query, isLoading } = useSearch(); + const { fetchMovies, query, isLoading, totalResults } = useSearch(); const currPage = Number(readUrl(PAGE_PARAM)); const isPrevDisabled = currPage === 1; const isPage = Boolean(currPage); + const noPages = totalResults <= 10; const handleNextPage = useCallback(() => { const newPage = currPage + 1; @@ -32,6 +33,8 @@ function Pagination() { fetchMovies(query, newPage); }, [isPage, currPage, setUrl, fetchMovies, query]); + if (noPages) return null; + return (
                    -
                    diff --git a/src/features/Pagination/hooks/usePagination.ts b/src/features/Pagination/hooks/usePagination.ts new file mode 100644 index 0000000..819f166 --- /dev/null +++ b/src/features/Pagination/hooks/usePagination.ts @@ -0,0 +1,43 @@ +import { useCallback } from 'react'; + +import { DEFAULT_PAGE, PAGE_PARAM } from '../../../shared/const/const.ts'; +import useUrl from '../../../shared/hooks/useUrl.ts'; +import useSearch from '../../Search/hooks/useSearch.ts'; + +function usePagination() { + const { setUrl, readUrl } = useUrl(); + const { fetchMovies, query, isLoading, totalResults } = useSearch(); + + const currPage = Number(readUrl(PAGE_PARAM)); + const isPrevDisabled = currPage === 1 || isLoading; + const isNextDisabled = isLoading; + const isPage = Boolean(currPage); + const noPages = totalResults <= 10; + + const handleNextPage = useCallback(() => { + const newPage = currPage + 1; + + setUrl(PAGE_PARAM, isPage ? String(newPage) : String(DEFAULT_PAGE)); + + fetchMovies(query, newPage); + }, [isPage, setUrl, fetchMovies, query, currPage]); + + const handlePrevPage = useCallback(() => { + if (!isPage || currPage === 1) return; + + const newPage = currPage - 1; + + setUrl(PAGE_PARAM, String(newPage)); + fetchMovies(query, newPage); + }, [isPage, currPage, setUrl, fetchMovies, query]); + + return { + handleNextPage, + handlePrevPage, + isPrevDisabled, + isNextDisabled, + noPages, + }; +} + +export default usePagination; From 622fe0bb5945dea9e36956e62828972008d25f48 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Nov 2023 11:38:10 +0200 Subject: [PATCH 116/144] refactor: rename layout component --- src/app/App.tsx | 4 ++-- src/pages/{MainLayout.tsx => AppLayout.tsx} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/pages/{MainLayout.tsx => AppLayout.tsx} (95%) diff --git a/src/app/App.tsx b/src/app/App.tsx index 15230ab..0f7cd7e 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,7 +1,7 @@ import { HashRouter } from 'react-router-dom'; import SearchProvider from '../features/Search/context/SearchProvider.tsx'; -import MainLayout from '../pages/MainLayout.tsx'; +import AppLayout from '../pages/AppLayout.tsx'; import ErrorBoundary from '../shared/ui/ErrorBoundary.tsx'; import FallbackUi from '../shared/ui/FallbackUi.tsx'; @@ -10,7 +10,7 @@ function App() { }> - + diff --git a/src/pages/MainLayout.tsx b/src/pages/AppLayout.tsx similarity index 95% rename from src/pages/MainLayout.tsx rename to src/pages/AppLayout.tsx index e046541..0d18e47 100644 --- a/src/pages/MainLayout.tsx +++ b/src/pages/AppLayout.tsx @@ -10,7 +10,7 @@ import TotalResults from '../widgets/Header/ui/TotalResults.tsx'; import Main from '../widgets/Main/Main.tsx'; import MovieDetails from '../widgets/MovieDetails/MovieDetails.tsx'; -function MainLayout() { +function AppLayout() { const { containerRef, scrollRef } = useScroll(); return ( @@ -35,4 +35,4 @@ function MainLayout() { ); } -export default MainLayout; +export default AppLayout; From 440d02fb46b3286688cbc6b46facb31899f46d44 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Nov 2023 14:11:39 +0200 Subject: [PATCH 117/144] refactor: change router to outlet --- src/app/App.tsx | 15 ++---- src/app/router.tsx | 24 +++++++++ src/entities/movie/hooks/useMovie.ts | 23 --------- src/entities/movie/loader.ts | 15 ++++++ src/entities/movie/ui/Movie.tsx | 62 ++++++++++------------- src/pages/AppLayout.tsx | 10 ++-- src/shared/ui/LinkWithQuery.tsx | 22 ++++++++ src/widgets/MovieDetails/MovieDetails.tsx | 11 ++-- 8 files changed, 107 insertions(+), 75 deletions(-) create mode 100644 src/app/router.tsx delete mode 100644 src/entities/movie/hooks/useMovie.ts create mode 100644 src/entities/movie/loader.ts create mode 100644 src/shared/ui/LinkWithQuery.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index 0f7cd7e..62024a0 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,19 +1,14 @@ -import { HashRouter } from 'react-router-dom'; +import { RouterProvider } from 'react-router-dom'; -import SearchProvider from '../features/Search/context/SearchProvider.tsx'; -import AppLayout from '../pages/AppLayout.tsx'; +import router from './router.tsx'; import ErrorBoundary from '../shared/ui/ErrorBoundary.tsx'; import FallbackUi from '../shared/ui/FallbackUi.tsx'; function App() { return ( - - }> - - - - - + }> + + ); } diff --git a/src/app/router.tsx b/src/app/router.tsx new file mode 100644 index 0000000..bddd3a1 --- /dev/null +++ b/src/app/router.tsx @@ -0,0 +1,24 @@ +import { createBrowserRouter } from 'react-router-dom'; + +import loader from '../entities/movie/loader.ts'; +import AppLayout from '../pages/AppLayout.tsx'; +import MovieDetails from '../widgets/MovieDetails/MovieDetails.tsx'; + +const router = createBrowserRouter([ + { + element: , + children: [ + { + element: null, + path: '/', + }, + { + element: , + path: ':movieId', + loader, + }, + ], + }, +]); + +export default router; diff --git a/src/entities/movie/hooks/useMovie.ts b/src/entities/movie/hooks/useMovie.ts deleted file mode 100644 index c8bf4a0..0000000 --- a/src/entities/movie/hooks/useMovie.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect } from 'react'; - -import useSearch from '../../../features/Search/hooks/useSearch.ts'; - -function useMovie() { - const { fetchMovie, movieDetails } = useSearch(); - - useEffect(() => { - fetchMovie(); - }, [fetchMovie]); - - useEffect(() => { - const title = movieDetails?.Title; - - if (title) { - document.title = `Cinemania | ${title}`; - } - }, [movieDetails?.Title]); - - return movieDetails; -} - -export default useMovie; diff --git a/src/entities/movie/loader.ts b/src/entities/movie/loader.ts new file mode 100644 index 0000000..1b94641 --- /dev/null +++ b/src/entities/movie/loader.ts @@ -0,0 +1,15 @@ +import { Params } from 'react-router-dom'; + +import { getMovie } from './api/apiMovie.ts'; + +interface ILoaderParams { + params: Params; +} + +async function loader({ params }: ILoaderParams) { + const { movieId } = params; + if (movieId) return getMovie(movieId); + return null; +} + +export default loader; diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 19ab90e..49604c4 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,8 +1,8 @@ import ReactLogo from '../../../assets/reactJS-logo.png'; -import { MOVIE_PARAM, NOT_EXIST } from '../../../shared/const/const.ts'; +import { NOT_EXIST } from '../../../shared/const/const.ts'; import useRadialHover from '../../../shared/hooks/useRadialHover.ts'; -import useUrl from '../../../shared/hooks/useUrl.ts'; import { Movie as MovieData } from '../../../shared/types/types.ts'; +import LinkWithQuery from '../../../shared/ui/LinkWithQuery.tsx'; interface IMovieProps { data: MovieData; @@ -10,7 +10,6 @@ interface IMovieProps { } function Movie({ data, delay }: IMovieProps) { - const { setUrl } = useUrl(); const { handleMouseOut, handleMouseMove, containerRef } = useRadialHover(); @@ -19,38 +18,33 @@ function Movie({ data, delay }: IMovieProps) { const poster = Poster === NOT_EXIST ? ReactLogo : Poster; const animationDelay = `0.${String(delay)}s`; - function handleMovieClick() { - setUrl(MOVIE_PARAM, imdbID); - } - return ( - // TODO - fix errors - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions -
                  • -
                    - {`The -
                    -

                    {Title}

                    -
                    - {Year} -
                    -
                    -
                    -
                  • + +
                  • +
                    + {`The +
                    +

                    {Title}

                    +
                    + {Year} +
                    +
                    +
                    +
                  • +
                    ); } export default Movie; diff --git a/src/pages/AppLayout.tsx b/src/pages/AppLayout.tsx index 0d18e47..2cb0b27 100644 --- a/src/pages/AppLayout.tsx +++ b/src/pages/AppLayout.tsx @@ -1,6 +1,9 @@ +import { Outlet } from 'react-router-dom'; + import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/MovieList/MovieList.tsx'; import Pagination from '../features/Pagination/Pagination.tsx'; +import SearchProvider from '../features/Search/context/SearchProvider.tsx'; import Search from '../features/Search/Search.tsx'; import useScroll from '../shared/hooks/useScroll.ts'; import Spinner from '../shared/ui/Spinner.tsx'; @@ -8,13 +11,12 @@ import Header from '../widgets/Header/Header.tsx'; import Logo from '../widgets/Header/ui/Logo.tsx'; import TotalResults from '../widgets/Header/ui/TotalResults.tsx'; import Main from '../widgets/Main/Main.tsx'; -import MovieDetails from '../widgets/MovieDetails/MovieDetails.tsx'; function AppLayout() { const { containerRef, scrollRef } = useScroll(); return ( - <> +
                    @@ -26,12 +28,12 @@ function AppLayout() {
                    - +
                    - +
                    ); } diff --git a/src/shared/ui/LinkWithQuery.tsx b/src/shared/ui/LinkWithQuery.tsx new file mode 100644 index 0000000..2ebb60e --- /dev/null +++ b/src/shared/ui/LinkWithQuery.tsx @@ -0,0 +1,22 @@ +/* eslint-disable react/jsx-props-no-spreading */ + +import { ReactNode } from 'react'; + +import { Link, useLocation } from 'react-router-dom'; + +interface ILinkWithQueryProps { + children: ReactNode; + to: string; +} + +function LinkWithQuery({ children, to, ...props }: ILinkWithQueryProps) { + const { search } = useLocation(); + + return ( + + {children} + + ); +} + +export default LinkWithQuery; diff --git a/src/widgets/MovieDetails/MovieDetails.tsx b/src/widgets/MovieDetails/MovieDetails.tsx index 611749e..df89bd9 100644 --- a/src/widgets/MovieDetails/MovieDetails.tsx +++ b/src/widgets/MovieDetails/MovieDetails.tsx @@ -1,9 +1,11 @@ +import { useLoaderData } from 'react-router-dom'; + import ReactLogo from '../../assets/reactJS-logo.png'; -import useMovie from '../../entities/movie/hooks/useMovie.ts'; import { NOT_EXIST } from '../../shared/const/const.ts'; +import { ApiMovieResponse } from '../../shared/types/types.ts'; function MovieDetails() { - const movie = useMovie(); + const movie = useLoaderData() as ApiMovieResponse; if (movie === null) return null; @@ -20,8 +22,9 @@ function MovieDetails() { Actors, } = movie; - const hrs = Math.floor(Number(Runtime.slice(0, -4)) / 60); - const min = Math.floor(Number(Runtime.slice(0, -4)) % 60); + const timeSeconds = Number(Runtime.slice(0, -4)); + const hrs = Math.floor(timeSeconds / 60); + const min = Math.floor(timeSeconds % 60); const time = hrs !== 0 ? `${hrs}h ${min}m` : `${min}m`; const description = `${Plot.slice(0, 150)}...`; const poster = Poster === NOT_EXIST ? ReactLogo : Poster; From 428a95fb0865f111bad4b367e22da7ef07dfc0c7 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Nov 2023 15:18:49 +0200 Subject: [PATCH 118/144] refactor: remove unused code --- .../Search/context/SearchProvider.tsx | 37 +------------------ src/features/Search/types/types.ts | 13 +------ 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx index ec415ba..1a4858b 100644 --- a/src/features/Search/context/SearchProvider.tsx +++ b/src/features/Search/context/SearchProvider.tsx @@ -6,19 +6,15 @@ import { useReducer, } from 'react'; -import { - getMovie, - getMovieList, -} from '../../../entities/movie/api/apiMovie.ts'; +import { getMovieList } from '../../../entities/movie/api/apiMovie.ts'; import { DEFAULT_PAGE, LOCAL_STORAGE_SEARCH_QUERY, - MOVIE_PARAM, PAGE_PARAM, } from '../../../shared/const/const.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; import { IChildren } from '../../../shared/types/interfaces.ts'; -import { ApiMovieResponse, MovieList } from '../../../shared/types/types.ts'; +import { MovieList } from '../../../shared/types/types.ts'; import { NO_MOVIES, NO_RESULTS } from '../const/const.ts'; import { Action, @@ -55,9 +51,6 @@ function reducer(state: IInitialState, action: Action): IInitialState { case SearchActions.LOADING: return { ...state, isLoading: true }; - case SearchActions.MOVIE_DETAILS_UPDATED: - return { ...state, movieDetails: action.payload, isLoading: false }; - default: throw new Error('The action does not exist!'); } @@ -85,16 +78,6 @@ function SearchProvider({ children }: IChildren) { [], ); - const updateMovieDetails = useCallback( - (newMovie: ApiMovieResponse | null) => { - dispatch({ - type: SearchActions.MOVIE_DETAILS_UPDATED, - payload: newMovie, - }); - }, - [], - ); - const fetchMovies = useCallback( async (searchQuery: string, page?: number) => { localStorage.setItem(LOCAL_STORAGE_SEARCH_QUERY, searchQuery); @@ -111,20 +94,6 @@ function SearchProvider({ children }: IChildren) { [updateMovies, updateQuery], ); - const fetchMovie = useCallback(async () => { - try { - dispatch({ type: SearchActions.LOADING }); - const id = readUrl(MOVIE_PARAM); - - if (id) { - const movie = await getMovie(id); - updateMovieDetails(movie); - } - } catch (e) { - updateMovieDetails(null); - } - }, [readUrl, updateMovieDetails]); - useEffect(() => { const storedQuery = localStorage.getItem(LOCAL_STORAGE_SEARCH_QUERY); const page = Number(readUrl(PAGE_PARAM)) || DEFAULT_PAGE; @@ -144,11 +113,9 @@ function SearchProvider({ children }: IChildren) { isLoading, updateQuery, fetchMovies, - fetchMovie, movieDetails, }), [ - fetchMovie, fetchMovies, isLoading, movieDetails, diff --git a/src/features/Search/types/types.ts b/src/features/Search/types/types.ts index a024d4f..a135e55 100644 --- a/src/features/Search/types/types.ts +++ b/src/features/Search/types/types.ts @@ -4,7 +4,6 @@ export enum SearchActions { QUERY_UPDATED = 'search/queryUpdated', MOVIES_LOADED = 'search/moviesLoaded', LOADING = 'search/loading', - MOVIE_DETAILS_UPDATED = 'searc/movieDetailsUpdated', } export interface IInitialState { @@ -18,7 +17,6 @@ export interface IInitialState { export interface ISearchContext extends IInitialState { updateQuery: (newQuery: string) => void; fetchMovies: (searchQuery: string, page?: number) => void; - fetchMovie: () => void; } export interface IQueryAction { @@ -34,17 +32,8 @@ export interface IMoviesAction { }; } -export interface IMoviesDetailsAction { - type: SearchActions.MOVIE_DETAILS_UPDATED; - payload: ApiMovieResponse | null; -} - export interface ILoadingAction { type: SearchActions.LOADING; } -export type Action = - | IQueryAction - | IMoviesAction - | ILoadingAction - | IMoviesDetailsAction; +export type Action = IQueryAction | IMoviesAction | ILoadingAction; From 50453137037d1c0afa0b7deb3bf62b967fef7520 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Nov 2023 15:32:50 +0200 Subject: [PATCH 119/144] fix: loading state --- src/features/MovieList/MovieList.tsx | 2 ++ src/pages/AppLayout.tsx | 8 ++++++-- src/shared/const/const.ts | 2 +- src/shared/ui/Spinner.tsx | 6 ------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/features/MovieList/MovieList.tsx b/src/features/MovieList/MovieList.tsx index 1707551..7cfb2c4 100644 --- a/src/features/MovieList/MovieList.tsx +++ b/src/features/MovieList/MovieList.tsx @@ -5,6 +5,7 @@ import LocomotiveScroll from 'locomotive-scroll'; import NotFound from './ui/NotFound.tsx'; import Movie from '../../entities/movie/ui/Movie.tsx'; import useScrollTop from '../../shared/hooks/useScrollTop.ts'; +import Spinner from '../../shared/ui/Spinner.tsx'; import useSearch from '../Search/hooks/useSearch.ts'; interface IMovieListProps { @@ -18,6 +19,7 @@ function MovieList({ scroll }: IMovieListProps) { const isNoMovies = !movies?.length; if (isNoMovies && !isLoading) return ; + if (isLoading) return ; if (isNoMovies) return null; return ( diff --git a/src/pages/AppLayout.tsx b/src/pages/AppLayout.tsx index 2cb0b27..460e209 100644 --- a/src/pages/AppLayout.tsx +++ b/src/pages/AppLayout.tsx @@ -1,10 +1,11 @@ -import { Outlet } from 'react-router-dom'; +import { Outlet, useNavigation } from 'react-router-dom'; import GradientBackground from './ui/GradientBackground.tsx'; import MovieList from '../features/MovieList/MovieList.tsx'; import Pagination from '../features/Pagination/Pagination.tsx'; import SearchProvider from '../features/Search/context/SearchProvider.tsx'; import Search from '../features/Search/Search.tsx'; +import { LOADING_STATE } from '../shared/const/const.ts'; import useScroll from '../shared/hooks/useScroll.ts'; import Spinner from '../shared/ui/Spinner.tsx'; import Header from '../widgets/Header/Header.tsx'; @@ -14,12 +15,15 @@ import Main from '../widgets/Main/Main.tsx'; function AppLayout() { const { containerRef, scrollRef } = useScroll(); + const navigation = useNavigation(); + + const isLoading = navigation.state === LOADING_STATE; return ( -
                    + {isLoading && }
                    diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts index b518189..674fcc0 100644 --- a/src/shared/const/const.ts +++ b/src/shared/const/const.ts @@ -4,6 +4,6 @@ export const QUERY_FALLBACK = 'all'; export const NOT_EXIST = 'N/A'; export const LOCAL_STORAGE_SEARCH_QUERY = 'search-query'; export const PAGE_PARAM = 'page'; -export const MOVIE_PARAM = 'movie'; export const DEFAULT_PAGE = 1; export const APP_TITLE = 'Cinemania | Dive into Movie Wonderland'; +export const LOADING_STATE = 'loading'; diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Spinner.tsx index eb7f587..486c0e8 100644 --- a/src/shared/ui/Spinner.tsx +++ b/src/shared/ui/Spinner.tsx @@ -1,12 +1,6 @@ import { createPortal } from 'react-dom'; -import useSearch from '../../features/Search/hooks/useSearch.ts'; - function Spinner() { - const { isLoading } = useSearch(); - - if (!isLoading) return null; - return createPortal(
                    From 2865ced73821f49035409dffaac90b5b8c4bb09d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Nov 2023 15:34:05 +0200 Subject: [PATCH 120/144] refactor: rename loader component --- src/features/MovieList/MovieList.tsx | 4 ++-- src/pages/AppLayout.tsx | 4 ++-- src/shared/ui/{Spinner.tsx => Loader.tsx} | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/shared/ui/{Spinner.tsx => Loader.tsx} (87%) diff --git a/src/features/MovieList/MovieList.tsx b/src/features/MovieList/MovieList.tsx index 7cfb2c4..df4c94c 100644 --- a/src/features/MovieList/MovieList.tsx +++ b/src/features/MovieList/MovieList.tsx @@ -5,7 +5,7 @@ import LocomotiveScroll from 'locomotive-scroll'; import NotFound from './ui/NotFound.tsx'; import Movie from '../../entities/movie/ui/Movie.tsx'; import useScrollTop from '../../shared/hooks/useScrollTop.ts'; -import Spinner from '../../shared/ui/Spinner.tsx'; +import Loader from '../../shared/ui/Loader.tsx'; import useSearch from '../Search/hooks/useSearch.ts'; interface IMovieListProps { @@ -19,7 +19,7 @@ function MovieList({ scroll }: IMovieListProps) { const isNoMovies = !movies?.length; if (isNoMovies && !isLoading) return ; - if (isLoading) return ; + if (isLoading) return ; if (isNoMovies) return null; return ( diff --git a/src/pages/AppLayout.tsx b/src/pages/AppLayout.tsx index 460e209..44f75e0 100644 --- a/src/pages/AppLayout.tsx +++ b/src/pages/AppLayout.tsx @@ -7,7 +7,7 @@ import SearchProvider from '../features/Search/context/SearchProvider.tsx'; import Search from '../features/Search/Search.tsx'; import { LOADING_STATE } from '../shared/const/const.ts'; import useScroll from '../shared/hooks/useScroll.ts'; -import Spinner from '../shared/ui/Spinner.tsx'; +import Loader from '../shared/ui/Loader.tsx'; import Header from '../widgets/Header/Header.tsx'; import Logo from '../widgets/Header/ui/Logo.tsx'; import TotalResults from '../widgets/Header/ui/TotalResults.tsx'; @@ -23,7 +23,7 @@ function AppLayout() {
                    - {isLoading && } + {isLoading && }
                    diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Loader.tsx similarity index 87% rename from src/shared/ui/Spinner.tsx rename to src/shared/ui/Loader.tsx index 486c0e8..6321059 100644 --- a/src/shared/ui/Spinner.tsx +++ b/src/shared/ui/Loader.tsx @@ -1,6 +1,6 @@ import { createPortal } from 'react-dom'; -function Spinner() { +function Loader() { return createPortal(
                    @@ -9,4 +9,4 @@ function Spinner() { ); } -export default Spinner; +export default Loader; From be2e46faa82538930e64f6c7d6c0800e8a059eed Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Nov 2023 15:59:18 +0200 Subject: [PATCH 121/144] refactor: remove unused code --- src/app/router.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app/router.tsx b/src/app/router.tsx index bddd3a1..19b5c4d 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -7,11 +7,8 @@ import MovieDetails from '../widgets/MovieDetails/MovieDetails.tsx'; const router = createBrowserRouter([ { element: , + path: '/', children: [ - { - element: null, - path: '/', - }, { element: , path: ':movieId', From 71fd4daed3d0b66f5f7a36b5d48daa03b13b2e1d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Nov 2023 16:03:33 +0200 Subject: [PATCH 122/144] feat: implement title change on movie click --- src/widgets/MovieDetails/MovieDetails.tsx | 8 +++----- src/widgets/MovieDetails/hooks/useMovie.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 src/widgets/MovieDetails/hooks/useMovie.ts diff --git a/src/widgets/MovieDetails/MovieDetails.tsx b/src/widgets/MovieDetails/MovieDetails.tsx index df89bd9..7e7bab7 100644 --- a/src/widgets/MovieDetails/MovieDetails.tsx +++ b/src/widgets/MovieDetails/MovieDetails.tsx @@ -1,13 +1,11 @@ -import { useLoaderData } from 'react-router-dom'; - +import useMovie from './hooks/useMovie.ts'; import ReactLogo from '../../assets/reactJS-logo.png'; import { NOT_EXIST } from '../../shared/const/const.ts'; -import { ApiMovieResponse } from '../../shared/types/types.ts'; function MovieDetails() { - const movie = useLoaderData() as ApiMovieResponse; + const movie = useMovie(); - if (movie === null) return null; + if (!movie) return null; const { Poster, diff --git a/src/widgets/MovieDetails/hooks/useMovie.ts b/src/widgets/MovieDetails/hooks/useMovie.ts new file mode 100644 index 0000000..2b64e08 --- /dev/null +++ b/src/widgets/MovieDetails/hooks/useMovie.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; + +import { useLoaderData } from 'react-router-dom'; + +import { ApiMovieResponse } from '../../../shared/types/types.ts'; + +function useMovie() { + const movie = useLoaderData() as ApiMovieResponse; + + useEffect(() => { + const newTitle = movie.Title; + + if (newTitle) document.title = `Cinemania | ${newTitle}`; + }, [movie.Title]); + + return movie; +} + +export default useMovie; From 51ed822caf3458dea1d91864bb1f309c15d922c9 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Nov 2023 18:24:42 +0200 Subject: [PATCH 123/144] feat: add nice tooltip --- src/entities/movie/ui/Movie.tsx | 16 ++++++++--- src/features/MovieList/MovieList.tsx | 12 ++++++++- src/shared/hooks/useTooltip.ts | 40 ++++++++++++++++++++++++++++ src/shared/ui/Tooltip.tsx | 21 +++++++++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 src/shared/hooks/useTooltip.ts create mode 100644 src/shared/ui/Tooltip.tsx diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 49604c4..7b718c5 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,3 +1,5 @@ +import { MouseEvent } from 'react'; + import ReactLogo from '../../../assets/reactJS-logo.png'; import { NOT_EXIST } from '../../../shared/const/const.ts'; import useRadialHover from '../../../shared/hooks/useRadialHover.ts'; @@ -7,9 +9,11 @@ import LinkWithQuery from '../../../shared/ui/LinkWithQuery.tsx'; interface IMovieProps { data: MovieData; delay: number; + onMouseIn: (e: MouseEvent) => void; + onMouseOut: () => void; } -function Movie({ data, delay }: IMovieProps) { +function Movie({ data, delay, onMouseIn, onMouseOut }: IMovieProps) { const { handleMouseOut, handleMouseMove, containerRef } = useRadialHover(); @@ -27,8 +31,14 @@ function Movie({ data, delay }: IMovieProps) { className="w-64 animate-springish cursor-pointer overflow-hidden rounded-5xl bg-neutral-950 text-gray-100 transition-all duration-200">
                    { + handleMouseMove(e); + onMouseIn(e); + }} + onMouseOut={() => { + handleMouseOut(); + onMouseOut(); + }} onBlur={handleMouseOut} className="h-full space-y-4 rounded-4xl p-2"> + Click for details {movies.map((movie, i) => ( - + ))}
                  ); diff --git a/src/shared/hooks/useTooltip.ts b/src/shared/hooks/useTooltip.ts new file mode 100644 index 0000000..14a07a6 --- /dev/null +++ b/src/shared/hooks/useTooltip.ts @@ -0,0 +1,40 @@ +import { MouseEvent, RefObject, useEffect, useRef } from 'react'; + +import LocomotiveScroll from 'locomotive-scroll'; + +const HIDDEN = ['invisible', 'opacity-0', '!duration-300', 'scale-50']; + +function useTooltip(scroll: RefObject) { + const tooltipRef = useRef(null); + + useEffect(() => { + if (scroll.current) + scroll.current.on('scroll', () => { + if (tooltipRef.current) tooltipRef.current.classList.add(...HIDDEN); + }); + }, [scroll]); + + function handleMouseIn(e: MouseEvent) { + const screenCoord = document.body.getBoundingClientRect(); + + if (!screenCoord) return; + + const posX = e.clientX - screenCoord.x - 130; + const posY = e.clientY - screenCoord.y - 130; + + if (tooltipRef.current) { + tooltipRef.current.classList.remove(...HIDDEN); + tooltipRef.current.style.cssText = ` + translate: ${posX}px ${posY}px; + `; + } + } + + function handleMouseOut() { + if (tooltipRef.current) tooltipRef.current.classList.add(...HIDDEN); + } + + return { tooltipRef, handleMouseIn, handleMouseOut }; +} + +export default useTooltip; diff --git a/src/shared/ui/Tooltip.tsx b/src/shared/ui/Tooltip.tsx new file mode 100644 index 0000000..6f93e3d --- /dev/null +++ b/src/shared/ui/Tooltip.tsx @@ -0,0 +1,21 @@ +import { ReactNode, RefObject } from 'react'; + +import { createPortal } from 'react-dom'; + +interface ITooltipProps { + innerRef: RefObject; + children: ReactNode; +} + +function Tooltip({ innerRef, children }: ITooltipProps) { + return createPortal( +
                  + {children} +
                  , + document.body, + ); +} + +export default Tooltip; From 2af1b2a5a3be0a260378b9ccbc11050dd9d060bc Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Nov 2023 18:34:53 +0200 Subject: [PATCH 124/144] fix: layout issues --- src/assets/chevron-left.svg | 3 +++ src/features/MovieList/MovieList.tsx | 2 +- src/widgets/Main/Main.tsx | 4 +--- src/widgets/MovieDetails/MovieDetails.tsx | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 src/assets/chevron-left.svg diff --git a/src/assets/chevron-left.svg b/src/assets/chevron-left.svg new file mode 100644 index 0000000..25c1ef0 --- /dev/null +++ b/src/assets/chevron-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/features/MovieList/MovieList.tsx b/src/features/MovieList/MovieList.tsx index 8ddf010..2f66a49 100644 --- a/src/features/MovieList/MovieList.tsx +++ b/src/features/MovieList/MovieList.tsx @@ -28,7 +28,7 @@ function MovieList({ scroll }: IMovieListProps) { return (
                    + className="m-auto mb-8 flex max-w-6xl flex-1 animate-fade-in flex-wrap items-center justify-between gap-6 last:m-auto sm:gap-10"> Click for details {movies.map((movie, i) => ( {children} - ); + return
                    {children}
                    ; } export default Main; diff --git a/src/widgets/MovieDetails/MovieDetails.tsx b/src/widgets/MovieDetails/MovieDetails.tsx index 7e7bab7..7989a7d 100644 --- a/src/widgets/MovieDetails/MovieDetails.tsx +++ b/src/widgets/MovieDetails/MovieDetails.tsx @@ -28,7 +28,7 @@ function MovieDetails() { const poster = Poster === NOT_EXIST ? ReactLogo : Poster; return ( -
                    +
                    Date: Fri, 3 Nov 2023 19:16:07 +0200 Subject: [PATCH 125/144] feat: add back button --- src/widgets/MovieDetails/MovieDetails.tsx | 6 ++++-- src/widgets/MovieDetails/ui/BackButton.tsx | 24 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/widgets/MovieDetails/ui/BackButton.tsx diff --git a/src/widgets/MovieDetails/MovieDetails.tsx b/src/widgets/MovieDetails/MovieDetails.tsx index 7989a7d..189d91c 100644 --- a/src/widgets/MovieDetails/MovieDetails.tsx +++ b/src/widgets/MovieDetails/MovieDetails.tsx @@ -1,4 +1,5 @@ import useMovie from './hooks/useMovie.ts'; +import BackButton from './ui/BackButton.tsx'; import ReactLogo from '../../assets/reactJS-logo.png'; import { NOT_EXIST } from '../../shared/const/const.ts'; @@ -35,14 +36,15 @@ function MovieDetails() { src={poster} alt={`The poster of ${Title} film`} /> -
                    +
                    +

                    {Title}

                    {time} | {Year} {Genre} - ⭐{imdbRating}/10 | 🎟️{imdbVotes} + ⭐{imdbRating}/10 | 🍿{imdbVotes}

                    {description}

                    diff --git a/src/widgets/MovieDetails/ui/BackButton.tsx b/src/widgets/MovieDetails/ui/BackButton.tsx new file mode 100644 index 0000000..814e7ea --- /dev/null +++ b/src/widgets/MovieDetails/ui/BackButton.tsx @@ -0,0 +1,24 @@ +import chevronLeft from '../../../assets/chevron-left.svg'; +import LinkWithQuery from '../../../shared/ui/LinkWithQuery.tsx'; + +function BackButton() { + return ( + +

                    +
                    + + +
                    + + ); +} + +export default BackButton; From 922ff157f949cb29a9994db4599930f0dfacb5c1 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 3 Nov 2023 19:32:17 +0200 Subject: [PATCH 126/144] feat: minor app improvements --- src/shared/ui/Tooltip.tsx | 2 +- src/widgets/MovieDetails/MovieDetails.tsx | 2 +- src/widgets/MovieDetails/ui/BackButton.tsx | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/shared/ui/Tooltip.tsx b/src/shared/ui/Tooltip.tsx index 6f93e3d..7149639 100644 --- a/src/shared/ui/Tooltip.tsx +++ b/src/shared/ui/Tooltip.tsx @@ -11,7 +11,7 @@ function Tooltip({ innerRef, children }: ITooltipProps) { return createPortal(
                    + className="pointer-events-none invisible absolute left-0 top-0 flex h-28 w-28 scale-50 items-center justify-center rounded-full bg-lime-500 p-6 text-center text-sm font-bold text-zinc-100 opacity-0 transition-all duration-[1200ms] ease-[cubic-bezier(.13,.66,0,.95)]"> {children}
                    , document.body, diff --git a/src/widgets/MovieDetails/MovieDetails.tsx b/src/widgets/MovieDetails/MovieDetails.tsx index 189d91c..5585179 100644 --- a/src/widgets/MovieDetails/MovieDetails.tsx +++ b/src/widgets/MovieDetails/MovieDetails.tsx @@ -30,7 +30,7 @@ function MovieDetails() { return (
                    -
                    +
                    -
                    +
                    From 496a44fbff24bc37cd2237f26ca7579eaf87e638 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 4 Nov 2023 13:08:08 +0200 Subject: [PATCH 127/144] feat: improve tooltip --- src/entities/movie/ui/Movie.tsx | 6 ++-- src/features/MovieList/MovieList.tsx | 6 ++-- src/shared/hooks/useTooltip.ts | 44 ++++++++++++++++++---------- src/shared/ui/Tooltip.tsx | 2 +- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 7b718c5..d333095 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -9,11 +9,11 @@ import LinkWithQuery from '../../../shared/ui/LinkWithQuery.tsx'; interface IMovieProps { data: MovieData; delay: number; - onMouseIn: (e: MouseEvent) => void; + onMouseMove: (e: MouseEvent) => void; onMouseOut: () => void; } -function Movie({ data, delay, onMouseIn, onMouseOut }: IMovieProps) { +function Movie({ data, delay, onMouseMove, onMouseOut }: IMovieProps) { const { handleMouseOut, handleMouseMove, containerRef } = useRadialHover(); @@ -33,7 +33,7 @@ function Movie({ data, delay, onMouseIn, onMouseOut }: IMovieProps) { ref={containerRef} onMouseMove={(e) => { handleMouseMove(e); - onMouseIn(e); + onMouseMove(e); }} onMouseOut={() => { handleMouseOut(); diff --git a/src/features/MovieList/MovieList.tsx b/src/features/MovieList/MovieList.tsx index 2f66a49..1a61f2e 100644 --- a/src/features/MovieList/MovieList.tsx +++ b/src/features/MovieList/MovieList.tsx @@ -16,8 +16,8 @@ interface IMovieListProps { function MovieList({ scroll }: IMovieListProps) { const { movies, isLoading } = useSearch(); + const { tooltipRef, hideTooltip, showTooltip } = useTooltip(scroll); useScrollTop(scroll, movies); - const { tooltipRef, handleMouseIn, handleMouseOut } = useTooltip(scroll); const isNoMovies = !movies?.length; @@ -32,8 +32,8 @@ function MovieList({ scroll }: IMovieListProps) { Click for details {movies.map((movie, i) => (