From 8b8cd7e4f54e397eba8e7ef2c18507460523a83b Mon Sep 17 00:00:00 2001 From: felipebrsk Date: Tue, 4 Feb 2025 14:05:37 -0300 Subject: [PATCH 1/5] fix: fixing integrations with API - adding lazy load images --- package-lock.json | 368 +++++++++++++- package.json | 3 + src/App.tsx | 1 + src/components/Comments/Comments.tsx | 450 ++++++------------ src/components/Comments/modules/Actions.tsx | 82 ++++ src/components/Comments/modules/Content.tsx | 41 ++ src/components/Comments/modules/Input.tsx | 38 ++ src/components/Comments/modules/index.ts | 3 + .../Forms/Review/ReviewForm.test.tsx | 7 +- src/components/Forms/Review/ReviewForm.tsx | 4 +- src/components/GameCard/GameCard.test.tsx | 12 +- src/components/GameCard/GameCard.tsx | 142 +++--- src/components/Header/Header.tsx | 14 +- .../Header/modules/HeaderCarousel.tsx | 3 +- src/components/HeartButton/HeartButton.tsx | 2 +- src/components/index.ts | 1 + src/layouts/DefaultLayout.tsx | 2 +- src/pages/Blog/Details.tsx | 20 +- src/pages/Game/modules/GameChat.tsx | 2 +- src/pages/Game/modules/MainDetails.tsx | 58 +-- src/pages/Profile/Sections/Transactions.tsx | 4 +- src/services/api.ts | 12 +- src/types/generics.ts | 2 +- src/types/reviews.ts | 4 +- 24 files changed, 836 insertions(+), 439 deletions(-) create mode 100644 src/components/Comments/modules/Actions.tsx create mode 100644 src/components/Comments/modules/Content.tsx create mode 100644 src/components/Comments/modules/Input.tsx create mode 100644 src/components/Comments/modules/index.ts diff --git a/package-lock.json b/package-lock.json index 52a694b..37612c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@reduxjs/toolkit": "^2.2.7", "@vitejs/plugin-react": "^4.3.1", "date-fns": "^3.6.0", + "immer": "^10.1.1", "nprogress": "^0.2.0", "react": "^18.3.1", "react-calendar": "^5.0.0", @@ -27,6 +28,7 @@ "react-hook-form": "^7.53.0", "react-hot-toast": "^2.4.1", "react-icons": "^5.3.0", + "react-lazy-load-image-component": "^1.6.3", "react-redux": "^9.1.2", "react-responsive-carousel": "^3.2.23", "react-router-dom": "^6.26.2", @@ -59,6 +61,7 @@ "@types/minimatch": "^5.1.2", "@types/nprogress": "^0.2.3", "@types/react-dom": "^18.3.0", + "@types/react-lazy-load-image-component": "^1.6.4", "@typescript-eslint/eslint-plugin": "^8.5.0", "@typescript-eslint/parser": "^8.5.0", "autoprefixer": "^10.4.20", @@ -7251,6 +7254,17 @@ "@types/react": "*" } }, + "node_modules/@types/react-lazy-load-image-component": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@types/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.4.tgz", + "integrity": "sha512-8pFPeDPF4yVG4lU1/ixZidJEEDZmEOYOTYDvmIu2IAabyuv97Q7n/93nMCocHvQ7vD1czKGiW+op55D9m3MkdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "csstype": "^3.0.2" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.11", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", @@ -17107,7 +17121,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, "license": "MIT" }, "node_modules/lodash.escaperegexp": { @@ -17173,6 +17186,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -22066,6 +22085,198 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "extraneous": true, + "os": [ + "android" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "os": [ + "android" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "os": [ + "darwin" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "extraneous": true, + "os": [ + "darwin" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "extraneous": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "extraneous": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "extraneous": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "extraneous": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "extraneous": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "extraneous": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "extraneous": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "os": [ + "win32" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "extraneous": true, + "os": [ + "win32" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "extraneous": true, + "os": [ + "win32" + ] + }, "node_modules/netlify-cli/node_modules/@sindresorhus/is": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", @@ -22185,12 +22396,54 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "node_modules/netlify-cli/node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "extraneous": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/netlify-cli/node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "extraneous": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/netlify-cli/node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/netlify-cli/node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "extraneous": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/netlify-cli/node_modules/@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "extraneous": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "node_modules/netlify-cli/node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -22230,6 +22483,12 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/netlify-cli/node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "extraneous": true + }, "node_modules/netlify-cli/node_modules/@types/node": { "version": "22.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", @@ -22245,12 +22504,34 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/netlify-cli/node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "extraneous": true + }, + "node_modules/netlify-cli/node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "extraneous": true + }, "node_modules/netlify-cli/node_modules/@types/retry": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", "dev": true }, + "node_modules/netlify-cli/node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "extraneous": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/netlify-cli/node_modules/@types/yargs-parser": { "version": "20.2.1", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", @@ -22631,6 +22912,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/netlify-cli/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "extraneous": 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/netlify-cli/node_modules/ajv-formats": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", @@ -25929,6 +26226,12 @@ "node": ">=8.6.0" } }, + "node_modules/netlify-cli/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==", + "extraneous": true + }, "node_modules/netlify-cli/node_modules/fast-json-stringify": { "version": "5.15.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.15.1.tgz", @@ -27630,6 +27933,15 @@ "ipx": "bin/ipx.mjs" } }, + "node_modules/netlify-cli/node_modules/ipx/node_modules/@netlify/blobs": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-6.5.0.tgz", + "integrity": "sha512-wRFlNnL/Qv3WNLZd3OT/YYqF1zb6iPSo8T31sl9ccL1ahBxW1fBqKgF4b1XL7Z+6mRIkatvcsVPkWBcO+oJMNA==", + "extraneous": true, + "engines": { + "node": "^14.16.0 || >=16.0.0" + } + }, "node_modules/netlify-cli/node_modules/ipx/node_modules/lru-cache": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", @@ -28185,6 +28497,12 @@ "fast-deep-equal": "^3.1.3" } }, + "node_modules/netlify-cli/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==", + "extraneous": true + }, "node_modules/netlify-cli/node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -31378,6 +31696,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/netlify-cli/node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "extraneous": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, "node_modules/netlify-cli/node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -38250,6 +38603,19 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-lazy-load-image-component": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.3.tgz", + "integrity": "sha512-kdQYUDbuISF3T9El0sBLNoWrmPohqlytcG4ognLtHYjY8bZAsJ0/Ez+VaV+0QlVyUY3K6dDXkuQAz3GpvdjBkw==", + "license": "MIT", + "dependencies": { + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1" + }, + "peerDependencies": { + "react": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x || ^19.x.x" + } + }, "node_modules/react-redux": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", diff --git a/package.json b/package.json index 68c1a71..5d7be4b 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@reduxjs/toolkit": "^2.2.7", "@vitejs/plugin-react": "^4.3.1", "date-fns": "^3.6.0", + "immer": "^10.1.1", "nprogress": "^0.2.0", "react": "^18.3.1", "react-calendar": "^5.0.0", @@ -45,6 +46,7 @@ "react-hook-form": "^7.53.0", "react-hot-toast": "^2.4.1", "react-icons": "^5.3.0", + "react-lazy-load-image-component": "^1.6.3", "react-redux": "^9.1.2", "react-responsive-carousel": "^3.2.23", "react-router-dom": "^6.26.2", @@ -77,6 +79,7 @@ "@types/minimatch": "^5.1.2", "@types/nprogress": "^0.2.3", "@types/react-dom": "^18.3.0", + "@types/react-lazy-load-image-component": "^1.6.4", "@typescript-eslint/eslint-plugin": "^8.5.0", "@typescript-eslint/parser": "^8.5.0", "autoprefixer": "^10.4.20", diff --git a/src/App.tsx b/src/App.tsx index beacc30..37df376 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import './styles/global.css' import 'swiper/swiper-bundle.css' import 'react-responsive-carousel/lib/styles/carousel.min.css' +import 'react-lazy-load-image-component/src/effects/opacity.css' import { CssBaseline, ThemeProvider } from '@mui/material' import { HelmetProvider } from 'react-helmet-async' diff --git a/src/components/Comments/Comments.tsx b/src/components/Comments/Comments.tsx index 378363f..992f8ac 100644 --- a/src/components/Comments/Comments.tsx +++ b/src/components/Comments/Comments.tsx @@ -1,27 +1,18 @@ -import { - Avatar, - Box, - IconButton, - List, - ListItem, - ListItemText, - Tooltip, - Typography, -} from '@mui/material' -import { formatRelative } from 'date-fns' -import { useEffect, useState } from 'react' import toast from 'react-hot-toast' +import { useMemo, useState } from 'react' +import { enableMapSet, produce } from 'immer' import { useNavigate } from 'react-router-dom' +import { Avatar, Box, List, ListItem } from '@mui/material' -import { HeartButton, HeartsUp, Icon, Input } from '@/components' +import { Comment } from '@/types' +import { HeartsUp } from '@/components' import { useAccount, useSuccess } from '@/hooks' import { useCreateCommentMutation, useDeleteCommentMutation, } from '@/services/api' -import { Comment } from '@/types' -import ActionDialog from '../ActionDialog' +import { Actions, Content, Input } from './modules' interface CommentsProps { defaultComments: Comment[] @@ -29,6 +20,8 @@ interface CommentsProps { commentableType: string } +enableMapSet() + function Comments(props: CommentsProps) { const { defaultComments, commentableId, commentableType } = props const go = useNavigate() @@ -41,69 +34,82 @@ function Comments(props: CommentsProps) { const [replyTo, setReplyTo] = useState(null) const [heartPops, setHeartPops] = useState([]) const [likedComments, setLikedComments] = useState>( - new Set(), + () => { + const initialSet = new Set() + defaultComments.forEach((comment) => { + if (comment.is_hearted) { + initialSet.add(comment.id) + } + comment.replies.forEach((reply) => { + if (reply.is_hearted) { + initialSet.add(`${comment.id}-${reply.id}`) + } + }) + }) + return initialSet + }, ) + const updateComments = (recipe: (draft: Comment[]) => void) => { + setComments(produce(recipe)) + } + const handleDelete = async (id: number) => { - await triggerDel(id) + await triggerDel(id).unwrap() + + updateComments((draft) => { + const parentIndex = draft.findIndex((c) => + c.replies.some((r) => r.id === id), + ) - setComments(comments.filter(({ id: commentId }) => commentId !== id)) + if (parentIndex > -1) { + draft[parentIndex].replies = draft[parentIndex].replies.filter( + (r) => r.id !== id, + ) + } else { + return draft.filter((c) => c.id !== id) + } + }) } const handleHeartClick = (item: Comment, parentId?: number) => { const isReply = Boolean(parentId) const itemId = isReply ? `${parentId}-${item.id}` : item.id + const wasLiked = likedComments.has(itemId) + setLikedComments((prev) => { - const updatedLikes = new Set(prev) - const isLiked = updatedLikes.has(itemId) + const newSet = new Set(prev) + wasLiked ? newSet.delete(itemId) : newSet.add(itemId) + return newSet + }) - if (isLiked) { - updatedLikes.delete(itemId) - } else { - updatedLikes.add(itemId) - } + updateComments((draft) => { + if (isReply && parentId) { + const parent = draft.find((c) => c.id === parentId) + const reply = parent?.replies.find((r) => r.id === item.id) - setComments((prevComments) => - prevComments.map((comment) => - isReply && parentId && comment.id === parentId - ? { - ...comment, - replies: comment.replies.map((reply) => - reply.id === item.id - ? { - ...reply, - hearts_count: - reply.hearts_count + (isLiked ? -1 : 1), - is_hearted: !isLiked, - } - : reply, - ), - } - : !isReply && comment.id === item.id - ? { - ...comment, - hearts_count: comment.hearts_count + (isLiked ? -1 : 1), - is_hearted: !isLiked, - } - : comment, - ), - ) + if (reply) { + reply.hearts_count += wasLiked ? -1 : 1 + reply.is_hearted = !wasLiked + } + } else { + const comment = draft.find((c) => c.id === item.id) - return updatedLikes + if (comment) { + comment.hearts_count += wasLiked ? -1 : 1 + comment.is_hearted = !wasLiked + } + } }) } const handleSendMessage = async () => { if (!newMessage.trim()) { - toast.error('You can not submit an empty message.') - return + return toast.error('You can not submit an empty message.') } - if (!user) { - go('/login') - return - } + if (!user) return go('/login') const comment = await trigger({ comment: newMessage, @@ -111,7 +117,10 @@ function Comments(props: CommentsProps) { commentable_type: commentableType, }).unwrap() - setComments([...comments, comment]) + updateComments((draft) => { + draft.unshift(comment) + }) + setNewMessage('') } @@ -133,266 +142,116 @@ function Comments(props: CommentsProps) { commentable_type: commentableType, }).unwrap() - setComments( - comments.map((comment) => - comment.id === id - ? { - ...comment, - replies: [...comment.replies, reply], - } - : comment, - ), - ) - setReplyTo(null) - setReplyMessage('') - } - - useEffect(() => { - const initialLikedComments = new Set() + updateComments((draft) => { + const parentComment = draft.find((comment) => comment.id === id) - comments.forEach((comment) => { - if (comment.is_hearted) { - initialLikedComments.add(comment.id) + if (parentComment) { + parentComment.replies.unshift(reply) } - - comment.replies.forEach((reply) => { - if (reply.is_hearted) { - initialLikedComments.add(`${comment.id}-${reply.id}`) - } - }) }) - setLikedComments(initialLikedComments) - }, [comments]) + setReplyTo(null) + setReplyMessage('') + } useSuccess(isSuccess, data?.message) + const heartPopElements = useMemo( + () => + heartPops.map((delay, index) => ( + + )), + [heartPops], + ) + return ( - {heartPops.map((delay, index) => ( - - ))} + {heartPopElements} - + {comments.map((comment) => ( - - - - {Array.from(comment.by.name)[0]} - - +
+ + + {comment.by.name[0]} + + + + handleHeartClick(comment)} + onReply={() => + setReplyTo(replyTo === comment.id ? null : comment.id) + } + isHearted={likedComments.has(comment.id)} /> - - - - - setReplyTo( - replyTo === comment.id ? null : comment.id, - ) - } - className="text-theme-red-900 hover:text-theme-red-700 hover:scale-110 transition-transform duration-300"> - - - - - {comment.by.id === user?.id && ( - handleDelete(comment.id)} - trigger={ - - - - - - } - /> - )} - - - {comment.by.id === user?.id ? ( - - ) : ( - {}} - onHeartToggle={() => handleHeartClick(comment)} - size={28} - /> - )} - - {comment.hearts_count} - - - {replyTo === comment.id && ( - + setReplyMessage(e.target.value)} - onKeyDown={({ code }) => { - if (code === 'Enter' || code === 'NumpadEnter') { - handleReplyMessage(comment.id) - } - }} - customClass="min-h-[3.5rem] rounded-lg border border-gray-300 dark:border-gray-700 p-2 focus:ring-2 focus:ring-theme-red-900 transition duration-300 ease-in-out" - icon={ - handleReplyMessage(comment.id)} - className="text-theme-red-900 hover:text-theme-red-700 hover:scale-110 transition-transform duration-300"> - - - } + onChange={setReplyMessage} + onSend={() => handleReplyMessage(comment.id)} /> )} {comment.replies.length > 0 && ( - + {comment.replies.map((reply) => ( - - - - - {Array.from(reply.by.name)[0]} - - +
+ + + {reply.by.name[0]} + + + + + handleHeartClick(reply, comment.id) + } + onReply={() => + setReplyTo( + replyTo === reply.id ? null : reply.id, + ) + } /> - - - - setReplyTo( - replyTo === reply.id ? null : reply.id, - ) - } - className="text-theme-red-900 hover:text-theme-red-700 hover:scale-110 transition-transform duration-300"> - - - - {reply.by.id === user?.id && ( - handleDelete(reply.id)} - trigger={ - - - - - - } - /> - )} - {reply.by.id === user?.id ? ( - - ) : ( - {}} - onHeartToggle={() => - handleHeartClick(reply, comment.id) - } - size={28} - /> - )} - - {reply.hearts_count} - - -
- +
{replyTo === reply.id && ( - + setReplyMessage(e.target.value)} - onKeyDown={({ code }) => { - if ( - code === 'Enter' || - code === 'NumpadEnter' - ) { - handleReplyMessage(comment.id) - } - }} - customClass="min-h-[3.5rem] rounded-lg border border-gray-300 dark:border-gray-700 p-2 focus:ring-2 focus:ring-theme-red-900 transition duration-300 ease-in-out" - icon={ - - handleReplyMessage(comment.id) - } - className="text-theme-red-900 hover:text-theme-red-700 hover:scale-110 transition-transform duration-300"> - - - } + onChange={setReplyMessage} + onSend={() => handleReplyMessage(comment.id)} /> )} @@ -406,26 +265,9 @@ function Comments(props: CommentsProps) { setNewMessage(target.value)} - onKeyDown={({ code }) => { - if (code === 'Enter' || code === 'NumpadEnter') { - handleSendMessage() - } - }} - customClass="min-h-[3.5rem] rounded-lg border border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-theme-red-900 transition-all duration-300 ease-in-out" - icon={ - - - - } + onChange={setNewMessage} + onSend={handleSendMessage} /> diff --git a/src/components/Comments/modules/Actions.tsx b/src/components/Comments/modules/Actions.tsx new file mode 100644 index 0000000..bad92bf --- /dev/null +++ b/src/components/Comments/modules/Actions.tsx @@ -0,0 +1,82 @@ +import { Dispatch, memo, SetStateAction } from 'react' +import { Box, IconButton, Tooltip, Typography } from '@mui/material' + +import { Comment, User } from '@/types' +import { ActionDialog, HeartButton, Icon } from '@/components' + +const HEARTABLE_TYPE = 'App\\Models\\GCStatus\\Commentable' + +const Actions = memo( + ({ + comment, + currentUser, + onReply, + onDelete, + onHeartClick, + parentId, + setHeartPops, + isHearted, + }: { + comment: Comment + currentUser: User | null + onReply: (id: number) => void + onDelete: (id: number) => void + onHeartClick: (item: Comment, parentId?: number) => void + parentId?: number + setHeartPops: Dispatch> + isHearted: boolean + }) => { + const isAuthor = comment.by.id === currentUser?.id + const isReply = !!parentId + + return ( + + {!isAuthor ? ( + + onReply(comment.id)} + size="small" + className="hover:text-theme-red-700"> + + + + ) : ( + onDelete(comment.id)} + trigger={ + + + + + + } + /> + )} + + + {isAuthor ? ( + + ) : ( + onHeartClick(comment, parentId)} + size={20} + setHearts={() => {}} + /> + )} + + {comment.hearts_count} + + + + ) + }, +) + +export default Actions diff --git a/src/components/Comments/modules/Content.tsx b/src/components/Comments/modules/Content.tsx new file mode 100644 index 0000000..ee87308 --- /dev/null +++ b/src/components/Comments/modules/Content.tsx @@ -0,0 +1,41 @@ +import { memo, useMemo } from 'react' +import { formatRelative } from 'date-fns' +import { ListItemText } from '@mui/material' + +import { Comment, User } from '@/types' + +const Content = memo( + ({ + comment, + currentUser, + isReply, + }: { + comment: Comment + currentUser: User | null + isReply?: boolean + }) => { + const { comment: text, created_at, by } = comment + + const formattedDate = useMemo( + () => formatRelative(new Date(created_at), new Date()), + [created_at], + ) + + return ( + + ) + }, +) + +export default Content diff --git a/src/components/Comments/modules/Input.tsx b/src/components/Comments/modules/Input.tsx new file mode 100644 index 0000000..f29df8e --- /dev/null +++ b/src/components/Comments/modules/Input.tsx @@ -0,0 +1,38 @@ +import { Box, IconButton } from '@mui/material' + +import { Icon, Input } from '@/components' + +const CommentInput = ({ + value, + onChange, + onSend, +}: { + value: string + onChange: (value: string) => void + onSend: () => void +}) => ( + + onChange(e.target.value)} + onKeyDown={({ code }) => + ['Enter', 'NumpadEnter'].includes(code) && onSend() + } + customClass="min-h-[3.5rem] rounded-lg border border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-theme-red-900 transition-all duration-300 ease-in-out" + icon={ + + + + } + /> + +) + +export default CommentInput diff --git a/src/components/Comments/modules/index.ts b/src/components/Comments/modules/index.ts new file mode 100644 index 0000000..8737429 --- /dev/null +++ b/src/components/Comments/modules/index.ts @@ -0,0 +1,3 @@ +export { default as Input } from './Input' +export { default as Content } from './Content' +export { default as Actions } from './Actions' diff --git a/src/components/Forms/Review/ReviewForm.test.tsx b/src/components/Forms/Review/ReviewForm.test.tsx index c6469b6..7976c54 100644 --- a/src/components/Forms/Review/ReviewForm.test.tsx +++ b/src/components/Forms/Review/ReviewForm.test.tsx @@ -4,6 +4,7 @@ import toast from 'react-hot-toast' import { MOCK_GAME_DETAILS } from '@/mocks' import ReviewForm, { ReviewFormProps } from './ReviewForm' +import { ReviewStore } from '@/types' const mockGame = MOCK_GAME_DETAILS @@ -112,10 +113,10 @@ describe('ReviewForm Component', () => { expect(consoleSpy).toHaveBeenCalledWith({ review: 'This is a valid review with more than 15 characters.', consumed: true, - userId: 1, rate: 2.5, - gameId: mockGame.id, - }) + reviewable_id: mockGame.id, + reviewable_type: 'App\\Models\\GCStatus\\Game', + } as ReviewStore) }) }) diff --git a/src/components/Forms/Review/ReviewForm.tsx b/src/components/Forms/Review/ReviewForm.tsx index 5c19e9f..69982a7 100644 --- a/src/components/Forms/Review/ReviewForm.tsx +++ b/src/components/Forms/Review/ReviewForm.tsx @@ -53,9 +53,9 @@ function ReviewForm(props: ReviewFormProps) { const payload: ReviewStore = { ...data, consumed, - userId: 1, rate: rating, - gameId: game.id, + reviewable_id: game.id, + reviewable_type: 'App\\Models\\GCStatus\\Game', } console.log(payload) diff --git a/src/components/GameCard/GameCard.test.tsx b/src/components/GameCard/GameCard.test.tsx index edda80b..981b8b5 100644 --- a/src/components/GameCard/GameCard.test.tsx +++ b/src/components/GameCard/GameCard.test.tsx @@ -38,7 +38,7 @@ describe('GameCard Component', () => { }) it('renders correctly in list view', () => { - const { getByText, getByAltText } = renderGameCard({ + const { getByText, getByAltText, getAllByText } = renderGameCard({ game, view: 'list', }) @@ -47,11 +47,9 @@ describe('GameCard Component', () => { expect(getByAltText(game.title)).toBeInTheDocument() - expect(getByText(game.condition!)).toBeInTheDocument() + const genreElements = getAllByText(game.genres[0].name) - expect(getByText(game.hearts_count.toString())).toBeInTheDocument() - - expect(getByText(game.genres[0].name)).toBeInTheDocument() + expect(genreElements.length).toBeGreaterThan(0) }) it('renders correctly in grid view', () => { @@ -63,10 +61,10 @@ describe('GameCard Component', () => { }) it('renders genres correctly', () => { - const { getByText } = renderGameCard() + const { getAllByText } = renderGameCard() game.genres.forEach((genre) => { - expect(getByText(genre.name)).toBeInTheDocument() + expect(getAllByText(genre.name).length).toBeGreaterThan(0) }) }) diff --git a/src/components/GameCard/GameCard.tsx b/src/components/GameCard/GameCard.tsx index 8f4a213..6d9ef5e 100644 --- a/src/components/GameCard/GameCard.tsx +++ b/src/components/GameCard/GameCard.tsx @@ -8,13 +8,14 @@ import { Typography, } from '@mui/material' import { format } from 'date-fns' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { IoCheckmarkCircleOutline, IoCloseCircleOutline, IoEyeOutline, IoNotificationsOutline, } from 'react-icons/io5' +import { LazyLoadImage } from 'react-lazy-load-image-component' import { GameList } from '@/types' import { mapCrack } from '@/utils' @@ -26,28 +27,42 @@ export interface GameCardProps { view: 'list' | 'grid' } -function GameCard(props: GameCardProps) { - const { game, view } = props +function GameCard({ game, view }: GameCardProps) { const [heartPops, setHeartPops] = useState([]) const [hearts, setHearts] = useState(game.hearts_count) + const crackStatus = useMemo( + () => + game.crack + ? mapCrack[game.crack.status.name] + : 'Crack not available yet', + [game.crack], + ) + + const isUncracked = useMemo( + () => !game.crack || game.crack.status.name === 'uncracked', + [game.crack], + ) + + const heartPopElements = useMemo( + () => + heartPops.map((delay, index) => ( + + )), + [heartPops], + ) + return ( <> - {heartPops.map((delay, index) => ( - - ))} + {heartPopElements} @@ -58,18 +73,23 @@ function GameCard(props: GameCardProps) { )} + - {game.title} + + + + @@ -98,6 +118,7 @@ function GameCard(props: GameCardProps) {
+ @@ -111,14 +132,13 @@ function GameCard(props: GameCardProps) { + className={`flex flex-col justify-between ${view === 'list' ? 'sm:w-2/3' : ''} h-full gap-4`}> + + Release Date:{' '} + {format(new Date(game.release_date), 'yyyy-MM-dd')} + + - - Release Date:{' '} - {format(new Date(game.release_date), 'yyyy-MM-dd')} - {game.platforms.map((platform) => ( ( @@ -148,7 +168,7 @@ function GameCard(props: GameCardProps) { ))} - {game.crack ? ( + {game.crack && ( {['cracked', 'cracked-oneday'].includes( game.crack.status.name, @@ -167,7 +187,7 @@ function GameCard(props: GameCardProps) { variant="body2" className="font-bold dark:text-white text-gray-800"> @@ -197,46 +217,37 @@ function GameCard(props: GameCardProps) { )} - - - ) : ( - - ) - } - className={`font-bold px-3 py-1 rounded-md text-white ${ - game.crack.status.name === 'uncracked' - ? 'bg-theme-red-900 animate-pulse' - : 'bg-green-500' - }`} - style={{ - boxShadow: `0 4px 10px ${ - game.crack.status.name === 'uncracked' - ? 'rgba(255, 77, 77, 0.5)' - : 'rgba(76, 175, 80, 0.5)' - }`, - transition: 'transform 0.3s ease-in-out', - }} - /> - ) : ( + )} + + } - className="font-bold px-3 py-1 rounded-md text-white bg-theme-red-900 animate-pulse" - style={{ - boxShadow: '0 1px 4px rgba(255, 77, 77, 0.5)', - transition: 'transform 0.3s ease-in-out', + label={crackStatus} + icon={ + game.crack?.status.name === 'uncracked' ? ( + + ) : ( + + ) + } + className={`font-bold w-full px-3 py-1 rounded-md text-white ${ + isUncracked + ? 'bg-theme-red-900' + : 'bg-green-500 animate-pulse' + }`} + sx={{ + boxShadow: isUncracked + ? '0 4px 10px rgba(255, 77, 77, 0.5)' + : '0 4px 10px rgba(76, 175, 80, 0.5)', }} /> - )} +