diff --git a/.env b/.env new file mode 100644 index 000000000..879c18218 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +REACT_APP_API_BASE_URL = "https://panda-market-api.vercel.app" \ No newline at end of file diff --git a/.github/workflows/delete-merged-branch-config.yml b/.github/workflows/delete-merged-branch-config.yml index d54933615..4389c2701 100644 --- a/.github/workflows/delete-merged-branch-config.yml +++ b/.github/workflows/delete-merged-branch-config.yml @@ -1,9 +1,7 @@ name: delete branch on close pr - on: pull_request: types: [closed] - jobs: delete-branch: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index f5ff48f26..6e4f29624 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,11 @@ # Edit at https://www.toptal.com/developers/gitignore?templates=react,nextjs,sass ### NextJS ### +<<<<<<< HEAD +======= +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +>>>>>>> cde515c40685c7ea849fcafc91354005f21c13fd # dependencies /node_modules /.pnp @@ -59,4 +64,16 @@ sketch *.sass.map *.scss.map -# End of https://www.toptal.com/developers/gitignore/api/react,nextjs,sass \ No newline at end of file +<<<<<<< HEAD +# End of https://www.toptal.com/developers/gitignore/api/react,nextjs,sass +======= +# End of https://www.toptal.com/developers/gitignore/api/react,nextjs,sass +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +>>>>>>> cde515c40685c7ea849fcafc91354005f21c13fd diff --git a/README.md b/README.md index 58beeaccd..b87cb0044 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ In the project directory, you can run: ### `npm start` Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. -The page will reload when you make changes.\ -You may also see any lint errors in the console. +The page will reload if you make edits.\ +You will also see any lint errors in the console. ### `npm test` @@ -31,40 +31,16 @@ See the section about [deployment](https://facebook.github.io/create-react-app/d ### `npm run eject` -**Note: this is a one-way operation. Once you `eject`, you can't go back!** +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. ## Learn More You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) - -### Analyzing the Bundle Size - -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) - -### Making a Progressive Web App - -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) - -### Advanced Configuration - -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) - -### Deployment - -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) - -### `npm run build` fails to minify - -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/package-lock.json b/package-lock.json index c162972da..2a0e256f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,17 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "classnames": "^2.5.1", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.119", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "dotenv": "^16.4.5", "node-sass": "^7.0.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", + "typescript": "^4.9.5", "web-vitals": "^2.1.4", "yup": "^1.4.0" } @@ -2686,25 +2690,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/expect-utils": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.4.tgz", - "integrity": "sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils/node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/fake-timers": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", @@ -4097,239 +4082,12 @@ } }, "node_modules/@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/jest/node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" - }, - "node_modules/@types/jest/node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@types/jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@types/jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@types/jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@types/jest/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/expect": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.4.tgz", - "integrity": "sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==", - "dependencies": { - "@jest/expect-utils": "^29.6.4", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.6.4", - "jest-message-util": "^29.6.3", - "jest-util": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@types/jest/node_modules/jest-diff": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.4.tgz", - "integrity": "sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-matcher-utils": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz", - "integrity": "sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.6.4", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-message-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.3.tgz", - "integrity": "sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.6.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.3.tgz", - "integrity": "sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", - "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@types/jest/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/@types/jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "27.5.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "jest-matcher-utils": "^27.0.0", + "pretty-format": "^27.0.0" } }, "node_modules/@types/json-schema": { @@ -4353,9 +4111,9 @@ "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==" }, "node_modules/@types/node": { - "version": "20.5.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.9.tgz", - "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==" + "version": "16.18.119", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.119.tgz", + "integrity": "sha512-ia7V9a2FnhUFfetng4/sRPBMTwHZUkPFY736rb1cg9AgG7MZdR97q7/nLR9om+sq5f1la9C857E0l/nrI0RiFQ==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", @@ -4393,19 +4151,18 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "node_modules/@types/react": { - "version": "18.2.21", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", - "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", - "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dependencies": { "@types/react": "*" } @@ -4423,11 +4180,6 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" - }, "node_modules/@types/semver": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz", @@ -6206,11 +5958,6 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "node_modules/clean-css": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", @@ -8931,19 +8678,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -15601,9 +15335,9 @@ } }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -15750,15 +15484,15 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-error-overlay": { @@ -16669,9 +16403,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { "loose-envify": "^1.1.0" } @@ -18253,7 +17987,6 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 956b9b051..8f1146175 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,20 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "classnames": "^2.5.1", + "@types/node": "^16.18.119", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-scripts": "5.0.1", + "typescript": "^4.9.5", + "web-vitals": "^2.1.4", "dotenv": "^16.4.5", "node-sass": "^7.0.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", "react-router-dom": "^6.26.2", - "react-scripts": "5.0.1", - "web-vitals": "^2.1.4", - "yup": "^1.4.0" + "yup": "^1.4.0", + "@types/jest": "^27.5.2" + }, "scripts": { "start": "react-scripts start", diff --git a/src/components/App/App.css b/src/App/App.css similarity index 100% rename from src/components/App/App.css rename to src/App/App.css diff --git a/src/components/App/App.js b/src/App/App.js similarity index 61% rename from src/components/App/App.js rename to src/App/App.js index aff28a6f9..86495b5c1 100644 --- a/src/components/App/App.js +++ b/src/App/App.js @@ -1,10 +1,10 @@ import "./App.css"; -import Navigation from "../Navigation/Navigation"; +import Navigation from "../components/Navigation/Navigation"; import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom"; -import AddItemForm from "../AddItemForm/AddItemForm"; -import ItemListPage from "../../pages/ItemListPage/ItemListPage"; -import { DeviceTypeProvider } from "../../contexts/DeviceTypeContext"; -import ItemDetailPage from "../../pages/ItemDetailPage/ItemDetailPage"; +import AddItemPage from "../pages/AddItemPage/AddItemPage"; +import ItemListPage from "../pages/ItemListPage/ItemListPage"; +import { DeviceTypeProvider } from "../contexts/DeviceTypeContext"; +import ItemDetailPage from "../pages/ItemDetailPage/ItemDetailPage"; function App() { return ( @@ -16,7 +16,7 @@ function App() { } /> } /> - } /> + } /> diff --git a/src/api.js b/src/api.js deleted file mode 100644 index 8f3db3a5f..000000000 --- a/src/api.js +++ /dev/null @@ -1,49 +0,0 @@ -// import dotenv from "dotenv"; -// dotenv.config(); -const BASE_URL = "https://panda-market-api.vercel.app"; - -/** - * 상품 목록 조회 API - * @param {number} page 페이지 번호 (기본 : 1) - * @param {number} pageSize 페이지당 상품 수 (기본 : 12) - * @param {string} orderBy recent 최신순(기본) / favorite 좋아요순 - * @param {string} keyword 검색 키워드 - * @returns {object} 상품 목록 객체 - */ -export async function getProducts( - page = 1, - pageSize = 12, - orderBy = "recent", - keyword = undefined -) { - if (!BASE_URL) throw new Error("요청을 보낼 수 없습니다."); - const url = new URL(BASE_URL + "/products"); - url.searchParams.append("page", page); - url.searchParams.append("pageSize", pageSize); - url.searchParams.append("orderBy", orderBy); - keyword && url.searchParams.append("keyword", keyword); - - const res = await fetch(url.href); - return res.json(); -} - -/** - * 상품 상세 조회 API - * @param {string} id - * @returns {object} 상품 상세 정보 객체 - */ -export async function getProductById(id) { - if (!BASE_URL) throw new Error("요청을 보낼 수 없습니다."); - const url = new URL(BASE_URL + "/products/" + id); - const res = await fetch(url.href); - return res.json(); -} - -export async function getCommentById(type, id, limit, cursor) { - if (!BASE_URL) throw new Error("요청을 보낼 수 없습니다."); - const url = new URL(BASE_URL + `/${type}/${id}/comments`); - url.searchParams.append("limit", limit); - cursor && url.searchParams.append("cursor", cursor); - const res = await fetch(url.href); - return res.json(); -} diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 000000000..e8761219d --- /dev/null +++ b/src/api.ts @@ -0,0 +1,42 @@ +import { CommentList } from "./types/Comment"; +import { ProductExtended, ProductList } from "./types/Product"; + +const BASE_URL = process.env.REACT_APP_API_BASE_URL; + +export async function getProductList( + page: number = 1, + pageSize: number = 12, + orderBy: string = "recent", + keyword: string | undefined = undefined +): Promise { + if (!BASE_URL) throw new Error("요청을 보낼 수 없습니다."); + const url = new URL(BASE_URL + "/products"); + url.searchParams.append("page", String(page)); + url.searchParams.append("pageSize", String(pageSize)); + url.searchParams.append("orderBy", orderBy); + keyword && url.searchParams.append("keyword", keyword); + + const res = await fetch(url.href); + return res.json(); +} + +export async function getProductById(id: string): Promise { + if (!BASE_URL) throw new Error("요청을 보낼 수 없습니다."); + const url = new URL(BASE_URL + "/products/" + id); + const res = await fetch(url.href); + return res.json(); +} + +export async function getCommentListByProductId( + type: string, + id: string, + limit: number, + cursor?: number | undefined +): Promise { + if (!BASE_URL) throw new Error("요청을 보낼 수 없습니다."); + const url = new URL(BASE_URL + `/${type}/${id}/comments`); + url.searchParams.append("limit", String(limit)); + cursor && url.searchParams.append("cursor", String(cursor)); + const res = await fetch(url.href); + return res.json(); +} diff --git a/src/components/BestItemList/BestItemList.js b/src/components/BestItemList/BestItemList.tsx similarity index 68% rename from src/components/BestItemList/BestItemList.js rename to src/components/BestItemList/BestItemList.tsx index 4a3282e05..ba5dc04d8 100644 --- a/src/components/BestItemList/BestItemList.js +++ b/src/components/BestItemList/BestItemList.tsx @@ -1,22 +1,23 @@ import { useEffect, useState } from "react"; -import { getProducts } from "../../api"; -import Item from "../Item/Item"; +import { getProductList } from "../../api"; +import ListItem from "../ListItem/ListItem"; import "./BestItemList.css"; import { useDeviceType } from "../../contexts/DeviceTypeContext"; +import { Product } from "../../types/Product"; const PAGE_SIZE = { desktop: 4, tablet: 2, mobile: 1, -}; +} as const; function ItemList() { - const [items, setItems] = useState([]); + const [items, setItems] = useState([]); const deviceType = useDeviceType(); useEffect(() => { const fetchData = async () => { - const result = await getProducts(1, PAGE_SIZE[deviceType], "favorite"); + const result = await getProductList(1, PAGE_SIZE[deviceType], "favorite"); setItems(result.list); }; fetchData(); @@ -38,12 +39,12 @@ function Header() { ); } -function Content({ items }) { +function Content({ items }: { items: Product[] }) { return (
    {items.map((item) => (
  • - +
  • ))}
diff --git a/src/components/CommentForm/CommentForm.js b/src/components/CommentForm/CommentForm.tsx similarity index 64% rename from src/components/CommentForm/CommentForm.js rename to src/components/CommentForm/CommentForm.tsx index 014fde19d..0242a037e 100644 --- a/src/components/CommentForm/CommentForm.js +++ b/src/components/CommentForm/CommentForm.tsx @@ -1,40 +1,64 @@ -import { useEffect, useRef, useState } from "react"; +import { + ChangeEvent, + FormEvent, + MouseEvent, + MutableRefObject, + useEffect, + useRef, + useState, +} from "react"; import ic_kebab from "../../assets/images/ic_kebab.svg"; import ic_profile from "../../assets/images/profile.svg"; import styles from "./CommentForm.module.css"; import useDebounce from "../../hooks/useDebounce"; -import { getCommentById } from "../../api"; +import { getCommentListByProductId } from "../../api"; import no_comment from "../../assets/images/no_comment.svg"; - -function CommentForm({ productId, className }) { +import { Comment, CommentList } from "../../types/Comment"; + +function CommentForm({ + productId, + className, +}: { + productId: string | undefined; + className: string; +}) { const [comment, setComment] = useState(""); - const [comments, setComments] = useState(); - const [selectedComment, setSelectedComment] = useState(null); - const [isEditing, setIsEditing] = useState(null); - const selectRef = useRef(null); - - const handleInputChange = useDebounce((e) => { - const next = e.target.value.trim(); - setComment(next); - }, 500); - - const handleFeatureClick = (e) => { - if (e.target.classList.contains("feature")) { - setSelectedComment(e.currentTarget.dataset.id); + const [comments, setComments] = useState(); + const [selectedComment, setSelectedComment] = useState(null); + const [isEditing, setIsEditing] = useState(null); + const selectRef = useRef(null); + + const handleInputChange = useDebounce( + (event: ChangeEvent) => { + const next = event.target.value.trim(); + setComment(next); + }, + 500 + ); + + const handleFeatureClick = (event: MouseEvent) => { + if (!(event.target instanceof HTMLElement)) return; + if (event.target.classList.contains("feature")) { + const id = event.currentTarget.dataset.id; + setSelectedComment(id || null); return; } setSelectedComment(null); }; - const handleSelectOption = (e) => { - if (e.target.dataset.option === "edit") { - setIsEditing(e.currentTarget.dataset.id); + const handleSelectOption = (event: MouseEvent) => { + if (!(event.target instanceof HTMLElement)) return; + if (event.target.dataset.option === "edit") { + const id = event.currentTarget.dataset.id; + setIsEditing(id || null); setSelectedComment(null); return; } - if (e.target.dataset.option === "delete") { - alert(`test : comment id ${e.currentTarget.dataset.id} 삭제 되었습니다`); + if (event.target.dataset.option === "delete") { + alert( + `test : comment id ${event.currentTarget.dataset.id} 삭제 되었습니다` + ); } }; @@ -43,9 +67,10 @@ function CommentForm({ productId, className }) { }; useEffect(() => { - const handleClickSelectOutside = (e) => { - if (e.target.classList.contains("feature")) return; - if (selectRef.current && !e.target.dataset?.option) { + const handleClickSelectOutside = (event: Event) => { + const target = event.target as HTMLElement; + if (target.classList.contains("feature")) return; + if (selectRef.current && !target.dataset?.option) { setSelectedComment(null); } }; @@ -56,7 +81,11 @@ function CommentForm({ productId, className }) { useEffect(() => { const fetchData = async () => { - const result = await getCommentById("products", productId, 10); + const result = await getCommentListByProductId( + "products", + productId as string, + 10 + ); setComments(result); }; fetchData(); @@ -106,7 +135,21 @@ function CommentForm({ productId, className }) { ); } -function CommentListItem({ data, selectRef, isSelected, onClick, onSelect }) { +interface CommentListItemInterface { + data: Comment; + selectRef: MutableRefObject; + isSelected: any; + onClick: any; + onSelect: any; +} + +function CommentListItem({ + data, + selectRef, + isSelected, + onClick, + onSelect, +}: CommentListItemInterface) { return (
{ - const next = e.target.value.trim(); + const handleInputChange = (event: ChangeEvent) => { + const next = event.target.value.trim(); setContent(next); }; - const handleEditSubmit = (e) => { - e.preventDefault(); + const handleEditSubmit = (event: FormEvent) => { + event.preventDefault(); alert(`test : Edit "${content}" from "${data.content.trim()}"`); onCancel(); }; diff --git a/src/components/FileInput/FileInput.js b/src/components/FileInput/FileInput.tsx similarity index 66% rename from src/components/FileInput/FileInput.js rename to src/components/FileInput/FileInput.tsx index dfa15bae1..0a40d70ce 100644 --- a/src/components/FileInput/FileInput.js +++ b/src/components/FileInput/FileInput.tsx @@ -2,35 +2,51 @@ import { useEffect, useState } from "react"; import "./FileInput.css"; import ic_upload from "../../assets/images/ic_plus.svg"; +interface Image { + id: string; + image: File; +} + +interface ImagePriview { + id: string; + src: string; +} + const UPLOAD_LIMIT = 3; -const PREVIEWS_DEFAULT = []; +const PREVIEWS_DEFAULT: ImagePriview[] = []; -function FileInput({ name, value, onChange, onDelete }) { - const [previews, setPreviews] = useState(PREVIEWS_DEFAULT); +function FileInput({ + name, + value, + onChange, + onDelete, +}: { + name: string; + value: Image[]; + onChange: Function; + onDelete: Function; +}) { + const [previews, setPreviews] = useState(PREVIEWS_DEFAULT); - /** - * 상품 이미지를 추가하기 위한 핸들러. - * 파일이 업로드될 시 호출된다. - * @param {Event} e 이벤트 객체 - */ - const handleChange = (e) => { - if (value.length < UPLOAD_LIMIT) { + const handleChange = (event: React.ChangeEvent) => { + if (!(event.target instanceof HTMLElement)) return; + if (value.length >= UPLOAD_LIMIT) { alert("상품 이미지는 최대 3개까지 업로드 할 수 있습니다."); return; } - const image = e.target.files[0]; + const image = event.target.files?.[0]; image && onChange(name, { id: Date.now().toString(), image: image }); }; - const handleDelete = (e) => { - const id = e.currentTarget.dataset.id; + const handleDelete = (event: React.MouseEvent) => { + const id = event.currentTarget.dataset.id; onDelete(name, id, "id"); }; /** * 미이보기 이미지의 주소를 저장한 배열을 초기화 한다. */ - const handlePreviewsClear = () => { + const handlePreviewsClear = (): void => { setPreviews((prev) => { prev.forEach((img) => { URL.revokeObjectURL(img.src); @@ -39,14 +55,14 @@ function FileInput({ name, value, onChange, onDelete }) { }); }; - useEffect(() => { + useEffect((): (() => void) | void => { if (!Array.isArray(value)) return; - const nextPreviews = []; + const nextPreviews: ImagePriview[] = []; value.forEach((el) => { nextPreviews.push({ id: el.id, src: URL.createObjectURL(el.image) }); }); setPreviews(nextPreviews); - return () => handlePreviewsClear; + return () => handlePreviewsClear(); }, [value]); return ( diff --git a/src/components/ItemDetail/ItemDetail.js b/src/components/ItemDetail/ItemDetail.tsx similarity index 81% rename from src/components/ItemDetail/ItemDetail.js rename to src/components/ItemDetail/ItemDetail.tsx index 27028f5c2..89ca6787a 100644 --- a/src/components/ItemDetail/ItemDetail.js +++ b/src/components/ItemDetail/ItemDetail.tsx @@ -2,8 +2,23 @@ import ic_profile from "../../assets/images/profile.svg"; import ic_favorite from "../../assets/images/ic_heart.svg"; import ic_kebab from "../../assets/images/ic_kebab.svg"; import styles from "./ItemDetail.module.css"; +import img_default from "../../assets/images/thumbnail-placeholder.png"; +import { ProductExtended } from "../../types/Product"; + +function ItemDetail({ + className, + item, +}: { + className: string; + item: ProductExtended; +}) { + const handleErrorLoadingImg = ( + event: React.SyntheticEvent + ) => { + const img = event.target as HTMLImageElement; + img.src = img_default; + }; -function ItemDetail({ className, item }) { return (
@@ -11,6 +26,7 @@ function ItemDetail({ className, item }) { className={styles["image"]} src={item?.images[0]} alt={item?.name} + onError={handleErrorLoadingImg} />
diff --git a/src/components/ItemList/ItemList.js b/src/components/ItemList/ItemList.tsx similarity index 63% rename from src/components/ItemList/ItemList.js rename to src/components/ItemList/ItemList.tsx index b0f910531..1727a36ee 100644 --- a/src/components/ItemList/ItemList.js +++ b/src/components/ItemList/ItemList.tsx @@ -1,40 +1,38 @@ import { useEffect, useState } from "react"; -import { getProducts } from "../../api"; -import Item from "../Item/Item"; +import { getProductList } from "../../api"; +import ListItem from "../ListItem/ListItem"; import "./ItemList.css"; import Pagination from "../Pagination/Pagination"; import arrowDown from "../../assets/images/ic_arrow_down.svg"; import ic_sort from "../../assets/images/ic_sort.svg"; import ic_search from "../../assets/images/ic_search.svg"; -import { useDeviceType } from "../../contexts/DeviceTypeContext"; +import { DeviceType, useDeviceType } from "../../contexts/DeviceTypeContext"; import { Link } from "react-router-dom"; +import useAsync from "../../hooks/useAsync"; +import { Product } from "../../types/Product"; const PAGE_SIZE = { desktop: 12, tablet: 6, mobile: 4, -}; +} as const; -/** - * 전체 상품 리스트 컴포넌트다. - * 키워드 검색, 상품 등록, 조건 검색 기능을 제공한다. - * @returns 리스트 컴포넌트 - */ function ItemList() { - const [items, setItems] = useState([]); - const [page, setPage] = useState(1); - const [order, setOrder] = useState("recent"); - const [total, setTotal] = useState(0); + const [items, setItems] = useState([]); + const [page, setPage] = useState(1); + const [order, setOrder] = useState("recent"); + const [total, setTotal] = useState(0); + const { execute: getProductsAsync } = useAsync(getProductList); const deviceType = useDeviceType(); useEffect(() => { const fetchData = async () => { - const result = await getProducts(page, PAGE_SIZE[deviceType], order); + const result = await getProductsAsync(page, PAGE_SIZE[deviceType], order); setItems(result.list); setTotal(result.totalCount); }; fetchData(); - }, [deviceType, page, order]); + }, [deviceType, page, order, getProductsAsync]); return (
@@ -50,13 +48,15 @@ function ItemList() { ); } -/** - * 전체 상품 페이지의 헤더 컴포넌트 - * @param {Function} setOrder 정렬 조건 세터 함수 - * @returns 헤더 컴포넌트 - * @description 여러 유틸 기능을 포함한 헤더. 키워드 검색, 상품 등록, 검색 조건 셀렉터를 포함하고 있다. - */ -function Header({ deviceType, order, setOrder }) { +function Header({ + deviceType, + order, + setOrder, +}: { + deviceType: DeviceType; + order: string; + setOrder: React.Dispatch>; +}) { if (deviceType !== "mobile") { return (
@@ -97,13 +97,29 @@ function Search() { ); } -function Select({ deviceType, order, setOrder }) { - const handleSelectClick = (e) => { - e.currentTarget.querySelector(".option-wrap").classList.toggle("show"); +function Select({ + deviceType, + order, + setOrder, +}: { + deviceType: DeviceType; + order: string; + setOrder: React.Dispatch>; +}) { + const handleSelectClick = (event: React.MouseEvent) => { + const select = event.currentTarget; + if (!(select instanceof HTMLElement)) return; + + const option = select.querySelector(".option-wrap"); + if (!(option instanceof HTMLElement)) return; + + option.classList.toggle("show"); }; - const handleSelectChange = (e) => { - const orderBy = e.target.dataset.order; + const handleSelectChange = (event: React.MouseEvent) => { + const option = event.target; + if (!(option instanceof HTMLElement)) return; + const orderBy = option.dataset.order; if (orderBy) { setOrder(orderBy); } @@ -131,17 +147,12 @@ function Select({ deviceType, order, setOrder }) { ); } -/** - * 상품 배열을 리스트로 보여주는 컴포넌트 - * @param {Object} items 상품 배열 객체 - * @returns 상품 리스트 컴포넌트 - */ -function Content({ items }) { +function Content({ items }: { items: Product[] }) { return (
    {items.map((item) => (
  • - +
  • ))}
diff --git a/src/components/Item/Item.css b/src/components/ListItem/ListItem.css similarity index 100% rename from src/components/Item/Item.css rename to src/components/ListItem/ListItem.css diff --git a/src/components/Item/Item.js b/src/components/ListItem/ListItem.tsx similarity index 54% rename from src/components/Item/Item.js rename to src/components/ListItem/ListItem.tsx index b0be9d027..08776d373 100644 --- a/src/components/Item/Item.js +++ b/src/components/ListItem/ListItem.tsx @@ -1,24 +1,34 @@ -import "./Item.css"; +import "./ListItem.css"; import ic_heart from "../../assets/images/ic_heart.svg"; import thumbDefault from "../../assets/images/thumbnail-placeholder.png"; import { Link } from "react-router-dom"; +import { Product } from "../../types/Product"; -/** - * 상품 요소 컴포넌트 - * @param {Object} item 상품 정보가 담긴 객체 - * @param {String} type 삼품 타입 null(기본) best(베스트 상품) - * @returns 상품 카드 요소 - * @todo item.images[0]가 이상한 url이면 요청에 실패해 엑박이 뜸 - * @todo classnames 모듈 적용하기 - */ -function Item({ item, type = null }) { +function ListItem({ + item, + type = null, +}: { + item: Product; + type?: string | null; +}) { const classNames = `Item ${type ? type : ""}`; - const thumbnail = item.images[0] ?? thumbDefault; + + const handleErrorLoadingImg = ( + event: React.SyntheticEvent + ): void => { + const img = event.target as HTMLImageElement; + img.src = thumbDefault; + }; return (
- {item.name} + {item.name}

{item.name}

{`${item.price}원`}
@@ -32,4 +42,4 @@ function Item({ item, type = null }) { ); } -export default Item; +export default ListItem; diff --git a/src/components/Pagination/Pagination.js b/src/components/Pagination/Pagination.tsx similarity index 64% rename from src/components/Pagination/Pagination.js rename to src/components/Pagination/Pagination.tsx index eddd0832d..5d3a8e76b 100644 --- a/src/components/Pagination/Pagination.js +++ b/src/components/Pagination/Pagination.tsx @@ -4,13 +4,7 @@ import arrowRight from "../../assets/images/arrow_right.svg"; import arrowLeftDouble from "../../assets/images/arrow_left_double.svg"; import arrowRightDouble from "../../assets/images/arrow_right_double.svg"; -/** - * 렌더링할 페이지 번호 배열을 반환하는 함수 - * @param {Number} page 현재 페이지 번호 - * @param {Number} lastPage 마지막 페이지 번호 - * @return {Object} 버튼 번호 배열 - */ -function getPageButtonRange(page, lastPage) { +function getPageButtonRange(page: number, lastPage: number) { // 표시할 최대 버튼 수 const RANGE_WIDTH = 5; const RANGE_WIDTH_HALF = 2; @@ -40,30 +34,32 @@ function getPageButtonRange(page, lastPage) { return range; } -/** - *페이지네이션 컴포넌트 - * @param {Number} page 현재 페이지 번호 - * @param {Function} setPage 페이지 세터 함수 - * @param {Number} pageSize 페이지당 상품 수 - * @param {Number} total 총 상품 수 - * @returns 페이지네이션 컴포넌트 반환 - */ -function Pagination({ page, setPage, pageSize, total }) { +function Pagination({ + page, + setPage, + pageSize, + total, +}: { + page: number; + setPage: React.Dispatch>; + pageSize: number; + total: number; +}) { // 마지막 페이지 번호 - const lastPage = Math.round(total / pageSize) + (total % pageSize > 0); + const lastPage = Math.ceil(total / pageSize); const prev = page - 1; const next = page + 1; // 표시할 버튼 번호 배열 const range = getPageButtonRange(page, lastPage); - /** - * 페이지네이션 버튼 클릭 이벤트핸들러 - * @param {Event} e 이벤트 객체 - * @description 클릭한 버튼의 value 속성에 해당하는 페이지로 이동 - */ - const handlePageClick = (e) => { - const data = e.target.dataset.page; - console.log(data); + const handlePageClick = (event: React.MouseEvent) => { + if (!(event.target instanceof HTMLElement)) { + alert("페이지 선택 중 오류가 발생했습니다!"); + return; + } + + const data = event.target.dataset.page; + const targetPage = Number(data); if (!targetPage) { console.log("paginattion err 1"); @@ -105,14 +101,7 @@ function Pagination({ page, setPage, pageSize, total }) { ); } -/** - * 페이지를 이동할 수 있는 버튼 컴포넌트 - * @param {Number} page 이 버튼에 해당하는 페이지 - * @param {Number} current 현재 사용자가 보고 있는 페이지 - * @returns 버튼 컴포넌트 - * @description 현재 페이지와 컴포넌트가 가리키는 페이지가 일치할 경우 강조된 버튼 반환 - */ -function PageButton({ page, current }) { +function PageButton({ page, current }: { page: number; current: number }) { const isCurrent = page === current ? "current" : ""; return (