diff --git a/.env b/.env new file mode 100644 index 00000000..ceb8e422 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +VITE_API_URL=https://www.omdbapi.com/ +VITE_API_KEY=dbb72d83 diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml new file mode 100644 index 00000000..c72f6e4b --- /dev/null +++ b/.github/actions/ci-setup/action.yml @@ -0,0 +1,21 @@ +name: "Setup Continuous Integration" +description: "Cache Dependencies" +runs: + using: "composite" + steps: + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + + - name: Cache NPM Dependencies + uses: actions/cache@v3 + id: cache-primes + with: + path: node_modules + key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} + + - name: Install Dependencies + run: npm install + shell: bash diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..5f406f42 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,29 @@ +name: Deploy + +on: [push] + +permissions: + contents: write + +env: + NODE_VERSION: 18.16.0 + +jobs: + deploy: + name: Build And Deploy + runs-on: ubuntu-latest + + steps: + - name: Checkout️ + uses: actions/checkout@v3 + + - name: Setup Continuous integration + uses: ./.github/actions/ci-setup + + - name: Build + run: npm run build + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: dist diff --git a/.gitignore b/.gitignore index 3e12931a..8c5de10f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ dist-ssr *.njsproj *.sln *.sw? +#*.env diff --git a/index.html b/index.html index 78c9dfe5..db6fadc9 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ + href="pop-corn.svg" /> diff --git a/package-lock.json b/package-lock.json index 7eff7a76..d377b3bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,25 +8,29 @@ "name": "cinemania", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^1.9.7", "animejs": "^3.2.1", "clsx": "^2.0.0", "locomotive-scroll": "^4.1.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^8.1.3", "react-router-dom": "^6.18.0", + "redux": "^4.2.1", "tailwind-merge": "^2.0.0" }, "devDependencies": { "@edge-runtime/vm": "^3.1.7", "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.1.4", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.1", "@types/animejs": "^3.1.11", "@types/deep-equal": "^1.0.4", "@types/locomotive-scroll": "^4.1.2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", + "@types/react-redux": "^7.1.30", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", @@ -124,6 +128,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -500,6 +505,7 @@ "resolved": "https://registry.npmjs.org/@edge-runtime/vm/-/vm-3.1.7.tgz", "integrity": "sha512-hUMFbDQ/nZN+1TLMi6iMO1QFz9RSV8yGG8S42WFPFma1d7VSNE0eMdJUmwjmtav22/iQkzHMmu6oTSfAvRGS8g==", "dev": true, + "peer": true, "dependencies": { "@edge-runtime/primitives": "4.0.5" }, @@ -1147,6 +1153,29 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", + "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", @@ -1400,9 +1429,9 @@ } }, "node_modules/@testing-library/react": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", - "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.2.tgz", + "integrity": "sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -1430,17 +1459,6 @@ "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/@types/animejs": { "version": "3.1.11", "resolved": "https://registry.npmjs.org/@types/animejs/-/animejs-3.1.11.tgz", @@ -1521,6 +1539,15 @@ "integrity": "sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==", "dev": true }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1556,6 +1583,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1563,14 +1591,13 @@ "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 + "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, "node_modules/@types/react": { "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, + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1581,16 +1608,28 @@ "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, + "devOptional": true, + "peer": true, "dependencies": { "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.30", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.30.tgz", + "integrity": "sha512-i2kqM6YaUwFKduamV6QM/uHbb0eCP8f8ZQ/0yWf+BsAVVsZPRYJ9eeGWZ3uxLfWwwA0SrPRMTPTqsPFkY3HZdA==", + "dev": true, + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "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 + "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==" }, "node_modules/@types/semver": { "version": "7.5.4", @@ -1604,11 +1643,17 @@ "integrity": "sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "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, + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", "@typescript-eslint/scope-manager": "6.9.0", @@ -1644,6 +1689,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.0.tgz", "integrity": "sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.9.0", "@typescript-eslint/types": "6.9.0", @@ -1938,19 +1984,12 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/acorn": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1976,20 +2015,6 @@ "node": ">=0.4.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2241,14 +2266,6 @@ "has-symbols": "^1.0.3" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/autoprefixer": { "version": "10.4.16", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", @@ -2434,6 +2451,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001541", "electron-to-chromium": "^1.4.535", @@ -2819,20 +2837,6 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -2901,25 +2905,10 @@ "node": ">=4" } }, - "node_modules/cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "rrweb-cssom": "^0.6.0" - }, - "engines": { - "node": ">=14" - } - }, "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 + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -2927,22 +2916,6 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, - "node_modules/data-urls": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", - "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2960,14 +2933,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/deep-eql": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", @@ -3157,17 +3122,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3228,20 +3182,6 @@ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3273,20 +3213,6 @@ "node": ">=10.13.0" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/es-abstract": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", @@ -3482,6 +3408,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3684,6 +3611,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -3745,6 +3673,7 @@ "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, + "peer": true, "dependencies": { "@babel/runtime": "^7.20.7", "aria-query": "^5.1.3", @@ -3813,6 +3742,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", @@ -3843,6 +3773,7 @@ "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, + "peer": true, "engines": { "node": ">=10" }, @@ -4302,22 +4233,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -4578,22 +4493,6 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, - "node_modules/happy-dom": { - "version": "12.10.3", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-12.10.3.tgz", - "integrity": "sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "css.escape": "^1.5.1", - "entities": "^4.5.0", - "iconv-lite": "^0.6.3", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0" - } - }, "node_modules/has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -4690,18 +4589,12 @@ "integrity": "sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw==", "dev": true }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dev": true, - "optional": true, - "peer": true, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" + "react-is": "^16.7.0" } }, "node_modules/html-escaper": { @@ -4710,37 +4603,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -4765,20 +4627,6 @@ "url": "https://github.com/sponsors/typicode" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4808,6 +4656,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -5379,14 +5236,6 @@ "node": ">=8" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -5685,50 +5534,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsdom": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", - "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "abab": "^2.0.6", - "cssstyle": "^3.0.0", - "data-urls": "^4.0.0", - "decimal.js": "^10.4.3", - "domexception": "^4.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.4", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.1", - "ws": "^8.13.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -6179,31 +5984,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -6529,14 +6309,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6943,20 +6715,6 @@ "node": ">=6" } }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7098,6 +6856,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -7224,6 +6983,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7361,14 +7121,6 @@ "react-is": "^16.13.1" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -7378,14 +7130,6 @@ "node": ">=6" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7410,6 +7154,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7421,6 +7166,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -7432,8 +7178,51 @@ "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 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-redux": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-refresh": { "version": "0.14.0", @@ -7522,6 +7311,23 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -7573,13 +7379,10 @@ "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "optional": true, - "peer": true + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" }, "node_modules/resolve": { "version": "1.22.8", @@ -7709,14 +7512,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/run-applescript": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", @@ -7938,20 +7733,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -8445,14 +8226,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/synckit": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", @@ -8648,37 +8421,6 @@ "node": ">=8.0" } }, - "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -8830,6 +8572,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8865,17 +8608,6 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -8924,16 +8656,12 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/util-deprecate": { @@ -8972,6 +8700,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -9050,6 +8779,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", "dev": true, + "peer": true, "dependencies": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", @@ -9122,20 +8852,6 @@ } } }, - "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -9145,57 +8861,6 @@ "defaults": "^1.0.3" } }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", - "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9365,48 +9030,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index b75eff4a..220fd1ca 100644 --- a/package.json +++ b/package.json @@ -12,30 +12,35 @@ "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", "prepare": "husky install", "precommit": "npx lint-staged && npm run type-check", - "test": "vitest --coverage", + "test": "vitest", + "test:coverage": "vitest --coverage", "test:staged": "vitest related --run", "test:committed": "vitest --run" }, "dependencies": { + "@reduxjs/toolkit": "^1.9.7", "animejs": "^3.2.1", "clsx": "^2.0.0", "locomotive-scroll": "^4.1.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^8.1.3", "react-router-dom": "^6.18.0", + "redux": "^4.2.1", "tailwind-merge": "^2.0.0" }, "devDependencies": { "@edge-runtime/vm": "^3.1.7", "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.1.4", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.1", "@types/animejs": "^3.1.11", "@types/deep-equal": "^1.0.4", "@types/locomotive-scroll": "^4.1.2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", + "@types/react-redux": "^7.1.30", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", diff --git a/public/pop-corn.svg b/public/pop-corn.svg new file mode 100644 index 00000000..7e11d53b --- /dev/null +++ b/public/pop-corn.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/app/model/slice.test.tsx b/src/app/model/slice.test.tsx new file mode 100644 index 00000000..917340e0 --- /dev/null +++ b/src/app/model/slice.test.tsx @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; + +import { + appReducer, + dataFetchedDetailsPage, + dataFetchedMainPage, + initialState, + moviesPerPageUpdated, +} from './slice.ts'; + +describe('appSlice', () => { + it("should change app's slice dataFetchedDetailsPage state", () => { + const state = appReducer(initialState, dataFetchedDetailsPage(true)); + + expect(state).toEqual({ + ...initialState, + isFetchingDetailsPage: true, + }); + }); + + it("should change app's slice dataFetchedMainPage state", () => { + const state = appReducer(initialState, dataFetchedMainPage(true)); + + expect(state).toEqual({ + ...initialState, + isFetchingMainPage: true, + }); + }); + + it("should change app's slice moviesPerPage state", () => { + const state = appReducer(initialState, moviesPerPageUpdated(99)); + + expect(state).toEqual({ + ...initialState, + moviesPerPage: 99, + }); + }); +}); diff --git a/src/app/model/slice.ts b/src/app/model/slice.ts new file mode 100644 index 00000000..892ffcc7 --- /dev/null +++ b/src/app/model/slice.ts @@ -0,0 +1,41 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { MOVIES_PER_PAGE } from '../../shared/const/const.ts'; + +interface IInitialState { + isFetchingMainPage: boolean; + isFetchingDetailsPage: boolean; + moviesPerPage: number; +} + +export const initialState: IInitialState = { + isFetchingMainPage: false, + isFetchingDetailsPage: false, + moviesPerPage: MOVIES_PER_PAGE, +}; + +export const appSlice = createSlice({ + name: 'app', + initialState, + reducers: { + dataFetchedMainPage: (state, action: PayloadAction) => { + state.isFetchingMainPage = action.payload; + }, + + dataFetchedDetailsPage: (state, action: PayloadAction) => { + state.isFetchingDetailsPage = action.payload; + }, + + moviesPerPageUpdated: (state, action: PayloadAction) => { + state.moviesPerPage = action.payload; + }, + }, +}); + +export const { + dataFetchedMainPage, + dataFetchedDetailsPage, + moviesPerPageUpdated, +} = appSlice.actions; + +export const appReducer = appSlice.reducer; diff --git a/src/app/router.tsx b/src/app/router.tsx index 59eb954b..1aedbce6 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -1,6 +1,5 @@ import { createHashRouter, RouteObject } from 'react-router-dom'; -import loader from '../entities/movie/loader.ts'; import AppLayout from '../pages/AppLayout/AppLayout.tsx'; import NotFound from '../pages/NotFound/NotFound.tsx'; @@ -13,7 +12,6 @@ export const ROUTES: RouteObject[] = [ { path: ':movieId', lazy: () => import('../widgets/MovieDetails/MovieDetails.tsx'), - loader, }, ], }, diff --git a/src/app/store/store.ts b/src/app/store/store.ts new file mode 100644 index 00000000..44493cc8 --- /dev/null +++ b/src/app/store/store.ts @@ -0,0 +1,24 @@ +import { combineReducers, configureStore } from '@reduxjs/toolkit'; + +import { movieApi } from '../../entities/movie/api/movieApi.ts'; +import { searchReducer } from '../../features/Search/model/slice.ts'; +import { appReducer } from '../model/slice.ts'; + +export type AppStore = ReturnType; +export type RootState = ReturnType; +export type AppDispatch = AppStore['dispatch']; +export type PreloadState = Partial; + +const rootReducer = combineReducers({ + [movieApi.reducerPath]: movieApi.reducer, + searchReducer, + appReducer, +}); + +export const setupStore = (preloadedState?: PreloadState) => + configureStore({ + preloadedState, + reducer: rootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(movieApi.middleware), + }); diff --git a/src/entities/movie/Movie.test.tsx b/src/entities/movie/Movie.test.tsx index 87a2dd83..9e10bcf5 100644 --- a/src/entities/movie/Movie.test.tsx +++ b/src/entities/movie/Movie.test.tsx @@ -2,16 +2,15 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it } from 'vitest'; -import * as apiMovie from './api/apiMovie.ts'; +import * as movieApi from './api/movieApi.ts'; import Movie from './ui/Movie.tsx'; -import * as useSearch from '../../features/Search/hooks/useSearch.ts'; -import createMockSearchContext from '../../test/helpers/createMockSearchContext.ts'; +import * as useGetMovieList from '../../shared/hooks/useGetMovieList.ts'; import renderWithRouter from '../../test/helpers/RenderWithRouter.tsx'; -import { mockMovieItem } from '../../test/mocks/data.ts'; +import renderWithRouterProvider from '../../test/helpers/renderWithRouterProvider.tsx'; +import { mockMovieItem, mockMovies } from '../../test/mocks/data.ts'; -const mockMovie = mockMovieItem; -const mockedUseSearch = vi.spyOn(useSearch, 'default'); -const mockedApiMovie = vi.spyOn(apiMovie, 'getMovie'); +const mockedUseGetMovieList = vi.spyOn(useGetMovieList, 'default'); +const mockedUseGetMovieQuery = vi.spyOn(movieApi, 'useGetMovieQuery'); describe('Movie', () => { afterEach(() => { @@ -23,7 +22,7 @@ describe('Movie', () => { , ); @@ -36,15 +35,19 @@ describe('Movie', () => { expect(title).toBeInTheDocument(); expect(year).toBeInTheDocument(); - expect(poster).toHaveAttribute('src', mockMovie.Poster); - expect(title).toHaveTextContent(mockMovie.Title); - expect(year).toHaveTextContent(mockMovie.Year); + expect(poster).toHaveAttribute('src', mockMovieItem.Poster); + expect(title).toHaveTextContent(mockMovieItem.Title); + expect(year).toHaveTextContent(mockMovieItem.Year); }); it('should open a detailed card component when clicking on a card', async () => { - mockedUseSearch.mockReturnValue(createMockSearchContext()); + mockedUseGetMovieList.mockReturnValue({ + movieList: mockMovies, + totalResults: mockMovies.length, + isInitialLoading: false, + }); - renderWithRouter(); + renderWithRouterProvider(); expect(screen.queryByTestId('details-section')).toBeNull(); @@ -56,13 +59,11 @@ describe('Movie', () => { }); it('should triggers an additional API call to fetch detailed information when clicking on the card', async () => { - mockedUseSearch.mockReturnValue(createMockSearchContext()); - - renderWithRouter(); + renderWithRouterProvider(); const [movie] = screen.getAllByTestId('movie-item'); await userEvent.click(movie); - expect(mockedApiMovie).toBeCalledTimes(1); + expect(mockedUseGetMovieQuery).toHaveBeenCalled(); }); }); diff --git a/src/entities/movie/api/apiMovie.ts b/src/entities/movie/api/apiMovie-v1.ts similarity index 70% rename from src/entities/movie/api/apiMovie.ts rename to src/entities/movie/api/apiMovie-v1.ts index e4208e7e..2e2dc0e8 100644 --- a/src/entities/movie/api/apiMovie.ts +++ b/src/entities/movie/api/apiMovie-v1.ts @@ -1,8 +1,4 @@ -import { - API_URL, - DEFAULT_PAGE, - QUERY_FALLBACK, -} from '../../../shared/const/const.ts'; +import { DEFAULT_PAGE, QUERY_FALLBACK } from '../../../shared/const/const.ts'; import { ApiErrorResponse, ApiMovieListResponse, @@ -14,7 +10,9 @@ export async function getMovieList( page: number = DEFAULT_PAGE, ): Promise { const response = await fetch( - `${API_URL}&s=${query || QUERY_FALLBACK}&page=${page}`, + `${import.meta.env.VITE_API_URL}?apikey=${import.meta.env.VITE_API_KEY}&s=${ + query || QUERY_FALLBACK + }&page=${page}`, ); if (!response.ok) throw new Error('Something went wrong fetching movies!'); @@ -29,7 +27,11 @@ export async function getMovieList( } export async function getMovie(id: string) { - const response = await fetch(`${API_URL}&i=${id}`); + const response = await fetch( + `${import.meta.env.VITE_API_URL}?apikey=${ + import.meta.env.VITE_API_KEY + }&i=${id}`, + ); if (!response.ok) throw new Error('Something went wrong fetching movies!'); diff --git a/src/entities/movie/api/movieApi.test.tsx b/src/entities/movie/api/movieApi.test.tsx new file mode 100644 index 00000000..03f9aa5d --- /dev/null +++ b/src/entities/movie/api/movieApi.test.tsx @@ -0,0 +1,61 @@ +import { ReactNode } from 'react'; + +import { renderHook, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { describe, it } from 'vitest'; + +import { useGetMovieListQuery, useGetMovieQuery } from './movieApi.ts'; +import { setupStore } from '../../../app/store/store.ts'; +import { + DEFAULT_PAGE, + MOVIES_PER_PAGE, + QUERY_FALLBACK, +} from '../../../shared/const/const.ts'; +import { mockMovieDetails, mockMovies } from '../../../test/mocks/data.ts'; + +function wrapper({ children }: { children: ReactNode }) { + const store = setupStore(); + return {children}; +} + +describe('movieApi', () => { + it('should fetch movie list data', async () => { + const { result } = renderHook( + () => + useGetMovieListQuery({ + moviesPerPage: MOVIES_PER_PAGE, + query: QUERY_FALLBACK, + page: String(DEFAULT_PAGE), + }), + { wrapper }, + ); + + expect(result.current).toMatchObject({ + data: undefined, + isFetching: true, + }); + + await waitFor(() => { + expect(result.current).toMatchObject({ + data: mockMovies, + isFetching: false, + }); + }); + }); + + it('should fetch movie details', async () => { + const { result } = renderHook(() => useGetMovieQuery('test'), { wrapper }); + + expect(result.current).toMatchObject({ + data: undefined, + isFetching: true, + }); + + await waitFor(() => { + expect(result.current).toMatchObject({ + data: mockMovieDetails, + isFetching: false, + }); + }); + }); +}); diff --git a/src/entities/movie/api/movieApi.ts b/src/entities/movie/api/movieApi.ts new file mode 100644 index 00000000..a3cc9394 --- /dev/null +++ b/src/entities/movie/api/movieApi.ts @@ -0,0 +1,31 @@ +import { IInitialState } from '../../../features/Search/types/types.ts'; +import rootApi from '../../../shared/api/rootApi.ts'; +import { QUERY_FALLBACK } from '../../../shared/const/const.ts'; +import { + ApiErrorResponse, + ApiMovieListResponse, + ApiMovieResponse, +} from '../../../shared/types/types.ts'; + +interface IMovieListParams { + query: IInitialState['query']; + page: string; + moviesPerPage: number; +} + +export const movieApi = rootApi.injectEndpoints({ + endpoints: (build) => ({ + getMovieList: build.query({ + query: ({ query, page, moviesPerPage }) => + `?apikey=${import.meta.env.VITE_API_KEY}&s=${ + query || QUERY_FALLBACK || moviesPerPage + }&page=${page}`, + }), + + getMovie: build.query({ + query: (id) => `?apikey=${import.meta.env.VITE_API_KEY}&i=${id}`, + }), + }), +}); + +export const { useGetMovieListQuery, useGetMovieQuery } = movieApi; diff --git a/src/entities/movie/loader.ts b/src/entities/movie/loader.ts index 1b94641d..59075ee8 100644 --- a/src/entities/movie/loader.ts +++ b/src/entities/movie/loader.ts @@ -1,6 +1,6 @@ import { Params } from 'react-router-dom'; -import { getMovie } from './api/apiMovie.ts'; +import { getMovie } from './api/apiMovie-v1.ts'; interface ILoaderParams { params: Params; diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 404b6660..12ed1287 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,4 +1,4 @@ -import { memo, MouseEvent } from 'react'; +import { memo, MouseEvent, SyntheticEvent } from 'react'; import { useLocation } from 'react-router-dom'; @@ -31,6 +31,11 @@ const Movie = memo(function Movie({ const animationDelay = `0.${String(delay)}s`; const isDetailsClose = pathname.slice(1) === ''; + const handleError = (e: SyntheticEvent) => { + const target = e.target as HTMLImageElement; + target.src = ReactLogo; + } + return (
{ - handleMouseMove(e); - onMouseMove(e); - }} + onMouseMove={handleMouseMove} + onMouseEnter={onMouseMove} onMouseLeave={() => { handleMouseOut(); onMouseOut(); @@ -56,6 +59,7 @@ const Movie = memo(function Movie({ onBlur={handleMouseOut} className="h-full space-y-4 rounded-4xl p-2"> ; describe('MovieList', () => { beforeAll(() => { scroll = { current: new LocomotiveScroll() }; + vi.clearAllMocks(); }); afterAll(() => { @@ -23,9 +24,13 @@ describe('MovieList', () => { }); it('should display an empty list message', () => { - mockedUseSearch.mockReturnValue(createMockSearchContext(null)); + mockedUseGetMovieList.mockReturnValue({ + movieList: undefined, + totalResults: 0, + isInitialLoading: false, + }); - renderWithRouter( + renderWithRouterProvider( ( @@ -46,9 +51,13 @@ describe('MovieList', () => { }); it('should renders the specified number of cards', () => { - mockedUseSearch.mockReturnValue(createMockSearchContext()); + mockedUseGetMovieList.mockReturnValue({ + movieList: mockMovies, + totalResults: mockMovies.length, + isInitialLoading: false, + }); - renderWithRouter( + renderWithRouterProvider( ( diff --git a/src/features/MovieList/MovieList.tsx b/src/features/MovieList/MovieList.tsx index f61f39fe..d8f104bc 100644 --- a/src/features/MovieList/MovieList.tsx +++ b/src/features/MovieList/MovieList.tsx @@ -3,8 +3,8 @@ import { PropsWithChildren, ReactNode, RefObject } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; import useListClick from './hooks/useListClick.ts'; -import useMovieList from './hooks/useMovieList.ts'; -import NotFound from './ui/NotFound.tsx'; +import MovieNotFound from './ui/MovieNotFound.tsx'; +import useGetMovieList from '../../shared/hooks/useGetMovieList.ts'; import { Movie } from '../../shared/types/types.ts'; interface IMovieListProps extends PropsWithChildren { @@ -13,10 +13,10 @@ interface IMovieListProps extends PropsWithChildren { } function MovieList({ scroll, render, children }: IMovieListProps) { - const { renderMovies, noMovies } = useMovieList(); const { listRef, handleClick } = useListClick(scroll); + const { movieList } = useGetMovieList(); - if (noMovies) return ; + if (!movieList) return ; return (
    {children} - {renderMovies?.map(render)} + {movieList?.map(render)}
); } diff --git a/src/features/MovieList/hooks/useMovieList.ts b/src/features/MovieList/hooks/useMovieList.ts deleted file mode 100644 index a26d108e..00000000 --- a/src/features/MovieList/hooks/useMovieList.ts +++ /dev/null @@ -1,20 +0,0 @@ -import useUrl from '../../../shared/hooks/useUrl.ts'; -import { urlParams } from '../../../shared/types/enums.ts'; -import useSearch from '../../Search/hooks/useSearch.ts'; - -function useMovieList() { - const { movies, isLoading } = useSearch(); - const { readUrl } = useUrl(); - - const moviesPerPage = Number(readUrl(urlParams.MOVIES_PER_PAGE)); - const noMovies = !movies?.length && !isLoading; - const renderMovies = movies?.slice(0, moviesPerPage); - - return { - isLoading, - renderMovies, - noMovies, - }; -} - -export default useMovieList; diff --git a/src/features/MovieList/ui/MovieListHeader.tsx b/src/features/MovieList/ui/MovieListHeader.tsx index d3189959..061e5a38 100644 --- a/src/features/MovieList/ui/MovieListHeader.tsx +++ b/src/features/MovieList/ui/MovieListHeader.tsx @@ -2,10 +2,11 @@ import { PropsWithChildren, RefObject, useCallback } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; -import { DEFAULT_PAGE } from '../../../shared/const/const.ts'; +import { moviesPerPageUpdated } from '../../../app/model/slice.ts'; +import useAppDispatch from '../../../shared/hooks/useAppDispatch.ts'; +import useAppSelector from '../../../shared/hooks/useAppSelector.ts'; import useScrollTop from '../../../shared/hooks/useScrollTop.ts'; -import useUrl from '../../../shared/hooks/useUrl.ts'; -import { urlParams } from '../../../shared/types/enums.ts'; +import selectMoviesPerPage from '../../../shared/lib/selectors/selectMoviesPerPage.ts'; import { ItemsPerPage } from '../../../shared/types/types.ts'; import Tabs from '../../../shared/ui/Tabs.tsx'; @@ -14,22 +15,18 @@ interface IMovieListHeader extends PropsWithChildren { } function MovieListHeader({ children, scroll }: IMovieListHeader) { - const { setUrl, readUrl } = useUrl(); - - const moviesPerPage = Number(readUrl(urlParams.MOVIES_PER_PAGE)); + const dispatch = useAppDispatch(); + const moviesPerPage = useAppSelector(selectMoviesPerPage); useScrollTop(moviesPerPage, scroll); const handleMoviesPerPage = useCallback( - (value: number) => { - if (value === moviesPerPage) return; + (newMoviesPerPage: number) => { + if (newMoviesPerPage === moviesPerPage) return; - setUrl({ - 'movies-per-page': String(value), - page: String(DEFAULT_PAGE), - }); + dispatch(moviesPerPageUpdated(newMoviesPerPage)); }, - [moviesPerPage, setUrl], + [dispatch, moviesPerPage], ); return ( diff --git a/src/features/MovieList/ui/NotFound.tsx b/src/features/MovieList/ui/MovieNotFound.tsx similarity index 84% rename from src/features/MovieList/ui/NotFound.tsx rename to src/features/MovieList/ui/MovieNotFound.tsx index 79166dbe..4894f95d 100644 --- a/src/features/MovieList/ui/NotFound.tsx +++ b/src/features/MovieList/ui/MovieNotFound.tsx @@ -1,6 +1,6 @@ import Modal from '../../../shared/ui/Modal.tsx'; -function NotFound() { +function MovieNotFound() { return ( @@ -12,4 +12,4 @@ function NotFound() { ); } -export default NotFound; +export default MovieNotFound; diff --git a/src/features/MovieList/ui/PageNum.tsx b/src/features/MovieList/ui/PageNum.tsx index f5c67239..e1885762 100644 --- a/src/features/MovieList/ui/PageNum.tsx +++ b/src/features/MovieList/ui/PageNum.tsx @@ -1,27 +1,29 @@ import { useEffect, useRef } from 'react'; -import useAnime from '../../../shared/hooks/useAnime.tsx'; +import useAnime from '../../../shared/hooks/useAnime.ts'; +import useAppSelector from '../../../shared/hooks/useAppSelector.ts'; +import useGetMovieList from '../../../shared/hooks/useGetMovieList.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; +import selectMoviesPerPage from '../../../shared/lib/selectors/selectMoviesPerPage.ts'; import { urlParams } from '../../../shared/types/enums.ts'; -import useSearch from '../../Search/hooks/useSearch.ts'; function PageNum() { const { readUrl } = useUrl(); - const { totalResults } = useSearch(); - const moviesPerPage = Number(readUrl(urlParams.MOVIES_PER_PAGE)); - const prevCurrPage = useRef(0); + const { totalResults } = useGetMovieList(); + const moviesPerPage = useAppSelector(selectMoviesPerPage); const prevMaxPage = useRef(0); const currPage = Number(readUrl(urlParams.PAGE)); const maxPage = Math.ceil(totalResults / moviesPerPage); - const currPageRef = useAnime({ - textContent: [prevCurrPage.current, currPage], + const { elementRef: currPageRef } = useAnime({ + textContent: [0, currPage], round: 1, easing: 'easeInOutExpo', + delay: 500, }); - const maxPageRef = useAnime( + const { elementRef: maxPageRef } = useAnime( { textContent: [prevMaxPage.current, maxPage], round: 1, @@ -30,7 +32,7 @@ function PageNum() { [maxPage], ); - const containerRef = useAnime({ + const { elementRef: containerRef } = useAnime({ scale: [0, 1], opacity: [0, 1], easing: 'easeInOutElastic(1, .34)', @@ -39,9 +41,8 @@ function PageNum() { }); useEffect(() => { - prevCurrPage.current = currPage; prevMaxPage.current = maxPage; - }, [currPage, maxPage]); + }, [maxPage]); return ( ; describe('Pagination', () => { @@ -27,11 +30,13 @@ describe('Pagination', () => { }); it('should update URL query parameter when page changes', async () => { - render( - - - , - ); + mockedUseGetMovieList.mockReturnValue({ + movieList: mockMovies, + totalResults, + isInitialLoading: false, + }); + + renderWithRouterProvider(); const [button] = screen.getAllByTestId('pagination'); diff --git a/src/features/Pagination/hooks/usePagination.ts b/src/features/Pagination/hooks/usePagination.ts index 1c0c05d7..1133ce83 100644 --- a/src/features/Pagination/hooks/usePagination.ts +++ b/src/features/Pagination/hooks/usePagination.ts @@ -2,23 +2,26 @@ import { RefObject, useCallback } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; -import { - DEFAULT_MOVIES_PER_PAGE, - DEFAULT_PAGE, -} from '../../../shared/const/const.ts'; +import { DEFAULT_PAGE, MOVIES_PER_PAGE } from '../../../shared/const/const.ts'; +import useAppSelector from '../../../shared/hooks/useAppSelector.ts'; +import useGetMovieList from '../../../shared/hooks/useGetMovieList.ts'; import useScrollTop from '../../../shared/hooks/useScrollTop.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; +import selectIsFetchingDetails from '../../../shared/lib/selectors/selectIsFetchingDetails.ts'; +import selectIsFetchingMain from '../../../shared/lib/selectors/selectIsFetchingMain.ts'; import { urlParams } from '../../../shared/types/enums.ts'; -import useSearch from '../../Search/hooks/useSearch.ts'; function usePagination(scroll: RefObject) { const { setUrl, readUrl } = useUrl(); - const { isLoading, totalResults } = useSearch(); + const isFetchingMain = useAppSelector(selectIsFetchingMain); + const isFetchingDetails = useAppSelector(selectIsFetchingDetails); + const { totalResults } = useGetMovieList(); + const isFetching = isFetchingDetails || isFetchingMain; const currPage = Number(readUrl(urlParams.PAGE)); - const isPrevDisabled = currPage === DEFAULT_PAGE || isLoading; - const isNextDisabled = isLoading; - const noPages = totalResults <= DEFAULT_MOVIES_PER_PAGE; + const isPrevDisabled = currPage === DEFAULT_PAGE || isFetching; + const isNextDisabled = isFetching; + const noPages = totalResults <= MOVIES_PER_PAGE; useScrollTop(currPage, scroll, undefined, currPage); diff --git a/src/features/Search/Search.test.tsx b/src/features/Search/Search.test.tsx index da13dc9c..53d99dd3 100644 --- a/src/features/Search/Search.test.tsx +++ b/src/features/Search/Search.test.tsx @@ -1,14 +1,13 @@ import { RefObject } from 'react'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import LocomotiveScroll from 'locomotive-scroll'; -import { MemoryRouter } from 'react-router-dom'; import { afterAll, afterEach, beforeAll, expect } from 'vitest'; import Search from './Search.tsx'; import { SEARCH_TEST_VALUE } from '../../test/const/const.ts'; -import renderWithRouter from '../../test/helpers/RenderWithRouter.tsx'; +import renderWithRouterProvider from '../../test/helpers/renderWithRouterProvider.tsx'; let scroll: RefObject; @@ -29,11 +28,7 @@ describe('Search', () => { }); it('should save the entered value to the local storage when clicking on the search button', async () => { - render( - - - , - ); + renderWithRouterProvider(); await userEvent.click(screen.getByRole('button')); @@ -41,7 +36,7 @@ describe('Search', () => { }); it('should retrieves the value from the local storage upon mounting', async () => { - renderWithRouter(); + renderWithRouterProvider(); await userEvent.type(screen.getByTestId('search-input'), SEARCH_TEST_VALUE); await userEvent.click(screen.getByRole('button')); diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index b7e67ade..f9dd8bbc 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -3,13 +3,14 @@ import { RefObject, useCallback, useRef } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; import { ENTER_KEY, ESCAPE_KEY } from './const/const.ts'; -import useSearch from './hooks/useSearch.ts'; +import { queryUpdated } from './model/slice.ts'; import searchIcon from '../../assets/search.svg'; import { DEFAULT_PAGE, LOCAL_STORAGE_SEARCH_QUERY, SCROLL_TOP_DURATION, } from '../../shared/const/const.ts'; +import useAppDispatch from '../../shared/hooks/useAppDispatch.ts'; import useKey from '../../shared/hooks/useKey.ts'; import useLocalStorageState from '../../shared/hooks/useLocalStorageState.ts'; import useUrl from '../../shared/hooks/useUrl.ts'; @@ -26,21 +27,14 @@ function Search({ scroll }: IMovieListProps) { LOCAL_STORAGE_SEARCH_QUERY, ); const inputRef = useRef(null); - const { fetchMovies, query: currQuery, updateQuery } = useSearch(); const { setUrl } = useUrl(); + const dispatch = useAppDispatch(); - const handleSearch = useCallback( - async (newQuery: string) => { - if (newQuery === currQuery) return; - - setUrl(urlParams.PAGE, DEFAULT_PAGE); - scroll?.current?.scrollTo('top', { duration: SCROLL_TOP_DURATION }); - - fetchMovies(newQuery.trim()); - updateQuery(query); - }, - [currQuery, fetchMovies, query, scroll, setUrl, updateQuery], - ); + const handleSearch = useCallback(() => { + setUrl(urlParams.PAGE, DEFAULT_PAGE); + scroll?.current?.scrollTo('top', { duration: SCROLL_TOP_DURATION }); + dispatch(queryUpdated(query)); + }, [dispatch, query, scroll, setUrl]); function handleEnter() { const isInputFocus = document.activeElement === inputRef.current; @@ -53,7 +47,7 @@ function Search({ scroll }: IMovieListProps) { } if (isInputFocus) { - void handleSearch(query); + handleSearch(); inputRef.current?.blur(); } } @@ -81,7 +75,7 @@ function Search({ scroll }: IMovieListProps) { onChange={(e) => setQuery(e.target.value)} />