diff --git a/.gitignore b/.gitignore index 4d29575d..5fa505c0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /node_modules /.pnp .pnp.js +.env # testing /coverage diff --git a/package-lock.json b/package-lock.json index a1e590ee..6c48b1fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,14 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.9", + "lodash.debounce": "^4.0.8", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-js-pagination": "^3.0.3", + "react-router-dom": "^6.28.1", "react-scripts": "5.0.1", + "styled-components": "^6.1.14", "web-vitals": "^2.1.4" } }, @@ -2270,6 +2275,24 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3241,6 +3264,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", + "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4412,6 +4443,11 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.9", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", @@ -5285,6 +5321,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -5614,6 +5673,17 @@ "node": ">=8" } }, + "node_modules/block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha512-OorbnJVPII4DuUKbjARAe8u8EfqOmkEEaSFIyoQ7OjTHn6kafxWl0wLgoZ2rXaYd7MyLcDaU4TmhfxtwgcccMQ==", + "dependencies": { + "inherits": "~2.0.0" + }, + "engines": { + "node": "0.4 || >=0.5.8" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -5826,6 +5896,14 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -5954,6 +6032,11 @@ "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", @@ -6261,6 +6344,14 @@ "postcss": "^8.4" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -6442,6 +6533,16 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -6635,9 +6736,9 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" }, "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==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -8294,9 +8395,9 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", @@ -8553,6 +8654,33 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -12424,9 +12552,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -12820,6 +12948,11 @@ "node": ">=6" } }, + "node_modules/paginator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/paginator/-/paginator-1.0.0.tgz", + "integrity": "sha512-j2Y5AtF/NrXOEU9VVOQBGHnj81NveRQ/cDzySywqsWrAj+cxivMpMCkYJOds3ulQiDU4rQBWc0WoyyXMXOmuMA==" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -13085,9 +13218,9 @@ } }, "node_modules/postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -13103,9 +13236,9 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -14375,6 +14508,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -14663,6 +14801,32 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-js-pagination": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/react-js-pagination/-/react-js-pagination-3.0.3.tgz", + "integrity": "sha512-podyA6Rd0uxc8uQakXWXxnonoOPI6NnFOROXfc6qPKNYm44s+Bgpn0JkyflcfbHf/GFKahnL8JN8rxBHZiBskg==", + "dependencies": { + "classnames": "^2.2.5", + "fstream": "1.0.12", + "paginator": "^1.0.0", + "prop-types": "15.x.x - 16.x.x", + "react": "15.x.x - 16.x.x", + "tar": "2.2.2" + } + }, + "node_modules/react-js-pagination/node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14671,6 +14835,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.1.tgz", + "integrity": "sha512-2omQTA3rkMljmrvvo6WtewGdVh45SpL9hGiCI9uUrwGGfNFDIvGK4gYJsKlJoNVi6AQZcopSCballL+QGOm7fA==", + "dependencies": { + "@remix-run/router": "1.21.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.1.tgz", + "integrity": "sha512-YraE27C/RdjcZwl5UCqF/ffXnZDxpJdk9Q6jw38SZHjXs7NNdpViq2l2c7fO7+4uWaEfcwfGCv3RSg4e1By/fQ==", + "dependencies": { + "@remix-run/router": "1.21.0", + "react-router": "6.28.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -15485,6 +15679,11 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15567,9 +15766,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -15980,6 +16179,33 @@ "webpack": "^5.0.0" } }, + "node_modules/styled-components": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.14.tgz", + "integrity": "sha512-KtfwhU5jw7UoxdM0g6XU9VZQFV4do+KrM8idiVCH5h4v49W+3p3yMe0icYwJgZQZepa5DbH04Qv8P0/RdcLcgg==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -15995,6 +16221,11 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", @@ -16230,6 +16461,17 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.", + "dependencies": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", diff --git a/package.json b/package.json index 7ff0d6b5..7c5a459a 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,14 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.9", + "lodash.debounce": "^4.0.8", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-js-pagination": "^3.0.3", + "react-router-dom": "^6.28.1", "react-scripts": "5.0.1", + "styled-components": "^6.1.14", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index a11777cc..00000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/font/ROKAF Slab Serif Bold.ttf b/public/font/ROKAF Slab Serif Bold.ttf new file mode 100644 index 00000000..6421545f Binary files /dev/null and b/public/font/ROKAF Slab Serif Bold.ttf differ diff --git a/public/index.html b/public/index.html index aa069f27..0b32c076 100644 --- a/public/index.html +++ b/public/index.html @@ -1,43 +1,12 @@ - + - - - - - - - - React App + + Panda Market -
- diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a3..00000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a65..00000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 080d6c77..00000000 --- a/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index e9e57dc4..00000000 --- a/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e053..00000000 --- a/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/App.js b/src/App.js deleted file mode 100644 index 37845757..00000000 --- a/src/App.js +++ /dev/null @@ -1,25 +0,0 @@ -import logo from './logo.svg'; -import './App.css'; - -function App() { - return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
- ); -} - -export default App; diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 00000000..79e65ce9 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,11 @@ +import Router from "./components/Router"; + +function App() { + return ( + <> + + + ); +} + +export default App; diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 1f03afee..00000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/api/products.js b/src/api/products.js new file mode 100644 index 00000000..9700cd4e --- /dev/null +++ b/src/api/products.js @@ -0,0 +1,12 @@ +import axios from "axios"; + +const BASE_URL = process.env.REACT_APP_BASE_URL; + +export async function getProducts(params) { + const { page, pageSize, orderBy, keyword } = params; + const response = await axios.get(`${BASE_URL}/products`, { + params: { page, pageSize, orderBy, keyword }, + }); + + return response.data; +} diff --git a/src/assets/icons/arrowDown.svg b/src/assets/icons/arrowDown.svg new file mode 100644 index 00000000..8308690f --- /dev/null +++ b/src/assets/icons/arrowDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/arrowLeft.svg b/src/assets/icons/arrowLeft.svg new file mode 100644 index 00000000..040e81c2 --- /dev/null +++ b/src/assets/icons/arrowLeft.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/arrowRight.svg b/src/assets/icons/arrowRight.svg new file mode 100644 index 00000000..368742c9 --- /dev/null +++ b/src/assets/icons/arrowRight.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/arrowUp.svg b/src/assets/icons/arrowUp.svg new file mode 100644 index 00000000..c48c9aad --- /dev/null +++ b/src/assets/icons/arrowUp.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/dropdown.svg b/src/assets/icons/dropdown.svg new file mode 100644 index 00000000..657b44f9 --- /dev/null +++ b/src/assets/icons/dropdown.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icons/heart.svg b/src/assets/icons/heart.svg new file mode 100644 index 00000000..f7065ea2 --- /dev/null +++ b/src/assets/icons/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/image.svg b/src/assets/icons/image.svg new file mode 100644 index 00000000..59b454a2 --- /dev/null +++ b/src/assets/icons/image.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/panda.svg b/src/assets/icons/panda.svg new file mode 100644 index 00000000..e6848692 --- /dev/null +++ b/src/assets/icons/panda.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg new file mode 100644 index 00000000..52241e6d --- /dev/null +++ b/src/assets/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/user.svg b/src/assets/icons/user.svg new file mode 100644 index 00000000..0b555d81 --- /dev/null +++ b/src/assets/icons/user.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Items/AllItems/AllItems.jsx b/src/components/Items/AllItems/AllItems.jsx new file mode 100644 index 00000000..26fa56bf --- /dev/null +++ b/src/components/Items/AllItems/AllItems.jsx @@ -0,0 +1,39 @@ +import * as S from "./AllItems.styles"; +import ItemCard from "../ItemCard/ItemCard"; +import Dropdown from "../../common/Dropdown/Dropdown"; +import { Link } from "react-router-dom"; +import Search from "../../Search/Search"; +import NoneItem from "../../NoneItem/NoneItem"; + +const list = ["최신순", "좋아요순"]; + +export default function AllItems({ items, sortOption, onChange, setKeyword }) { + return ( + + + + 전체 상품 + + 상품 등록하기 + + + + + + 상품 등록하기 + + + + + {items.length !== 0 ? ( + + {items.map((items, idx) => ( + + ))} + + ) : ( + + )} + + ); +} diff --git a/src/components/Items/AllItems/AllItems.styles.jsx b/src/components/Items/AllItems/AllItems.styles.jsx new file mode 100644 index 00000000..30effde3 --- /dev/null +++ b/src/components/Items/AllItems/AllItems.styles.jsx @@ -0,0 +1,104 @@ +import styled from "styled-components"; + +export const AllContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + width: 1200px; + + @media (max-width: 767px) { + width: 343px; + } + + @media (min-width: 768px) and (max-width: 1199px) { + width: 710px; + } +`; + +export const AllHeader = styled.div` + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + + @media (max-width: 767px) { + flex-direction: column; + gap: 8px; + } +`; + +export const Div = styled.div` + width: 100%; + display: flex; + justify-content: space-between; +`; + +export const Title = styled.h2` + font-size: 20px; + font-weight: 700; + line-height: 32px; + color: var(--gray900); + margin: 0; + + @media (max-width: 767px) { + display: flex; + justify-content: center; + align-items: center; + margin: 0; + } +`; + +export const Filter = styled.div` + display: flex; + gap: 12px; + + @media (max-width: 767px) { + width: 100%; + justify-content: space-between; + } +`; + +export const AddBtn = styled.button` + width: 133px; + height: 42px; + background-color: var(--primary); + color: var(--gray100); + border-radius: 8px; + border: none; + text-align: center; + font-size: 16px; + font-weight: 600; + line-height: 26px; + cursor: pointer; + + @media (max-width: 767px) { + display: none; + } +`; + +export const AddBtnForMedia = styled(AddBtn)` + display: none; + + @media (max-width: 767px) { + display: block; + } +`; + +export const ItemCardContainer = styled.div` + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-template-rows: 1fr 1fr; + gap: 24px; + margin: auto; + + @media (max-width: 767px) { + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + } + + @media (min-width: 768px) and (max-width: 1199px) { + grid-template-columns: repeat(3, 1fr); + grid-template-rows: 1fr 1fr; + } +`; diff --git a/src/components/Items/BestItems/BestItems.jsx b/src/components/Items/BestItems/BestItems.jsx new file mode 100644 index 00000000..f5db3640 --- /dev/null +++ b/src/components/Items/BestItems/BestItems.jsx @@ -0,0 +1,25 @@ +import * as S from "./BestItems.styles"; +import ItemCard from "../ItemCard/ItemCard"; +import { useEffect } from "react"; + +export default function BestItems({ bestItems, updateBestItems }) { + useEffect(() => { + updateBestItems(); + window.addEventListener("resize", updateBestItems); + + return () => { + window.removeEventListener("resize", updateBestItems); + }; + }, [updateBestItems]); + + return ( + + 베스트 상품 + + {bestItems.map((items, idx) => ( + + ))} + + + ); +} diff --git a/src/components/Items/BestItems/BestItems.styles.jsx b/src/components/Items/BestItems/BestItems.styles.jsx new file mode 100644 index 00000000..dbd8b45f --- /dev/null +++ b/src/components/Items/BestItems/BestItems.styles.jsx @@ -0,0 +1,23 @@ +import styled from "styled-components"; + +export const BestContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; +`; + +export const Title = styled.h2` + font-size: 20px; + font-weight: 700; + line-height: 32px; + text-align: left; + color: var(--gray900); + margin: 0; + margin-bottom: 16px; +`; + +export const ItemCardContainer = styled.div` + display: flex; + gap: 24px; + margin: auto; +`; diff --git a/src/components/Items/ItemCard/ItemCard.jsx b/src/components/Items/ItemCard/ItemCard.jsx new file mode 100644 index 00000000..bfa18f92 --- /dev/null +++ b/src/components/Items/ItemCard/ItemCard.jsx @@ -0,0 +1,20 @@ +import * as S from "./ItemCard.styles"; +import heart from "../../../assets/icons/heart.svg"; +import NoneImage from "../../NoneImage/NoneImage"; + +// images 값이 없으면 NoneImage 컴포넌트가 보이도록 구현했는데 images 배열 안에 값이 있지만 사진이 안 불러와지는 경우에는 어떻게 처리해야 하는지 고민입니다. +export default function ItemCard({ list = "best", images, name, price, favoriteCount }) { + return ( + + {images[0] ? : } + + {name} + {price.toLocaleString()}원 + + + {favoriteCount} + + + + ); +} diff --git a/src/components/Items/ItemCard/ItemCard.styles.jsx b/src/components/Items/ItemCard/ItemCard.styles.jsx new file mode 100644 index 00000000..75f3e5f8 --- /dev/null +++ b/src/components/Items/ItemCard/ItemCard.styles.jsx @@ -0,0 +1,99 @@ +import styled from "styled-components"; + +export const BEST_IMG = { + PC: "282px", + Tablet: "343px", + Moblie: "343px", +}; + +export const ALL_IMG = { + PC: "221px", + Tablet: "221px", + Moblie: "168px", +}; + +export const getImgSize = (list, screen) => { + const item = list === "best" ? BEST_IMG : ALL_IMG; + return item[screen]; +}; + +export const ItemImg = styled.img` + width: ${({ list }) => getImgSize(list, "PC")}; + height: ${({ list }) => getImgSize(list, "PC")}; + border-radius: 16px; + object-fit: cover; + + @media (max-width: 767px) { + width: ${({ list }) => getImgSize(list, "Moblie")}; + height: ${({ list }) => getImgSize(list, "Moblie")}; + } + + @media (min-width: 768px) and (max-width: 1199px) { + width: ${({ list }) => getImgSize(list, "Tablet")}; + height: ${({ list }) => getImgSize(list, "Tablet")}; + } + + @media (min-width: 1200px) { + width: ${({ list }) => getImgSize(list, "PC")}; + height: ${({ list }) => getImgSize(list, "PC")}; + } +`; + +export const ItemContainer = styled.div` + width: ${({ list }) => getImgSize(list, "PC")}; + display: flex; + flex-direction: column; + gap: 16px; + + @media (max-width: 767px) { + width: ${({ list }) => getImgSize(list, "Moblie")}; + } + + @media (min-width: 768px) and (max-width: 1199px) { + width: ${({ list }) => getImgSize(list, "Tablet")}; + } + + @media (min-width: 1200px) { + width: ${({ list }) => getImgSize(list, "PC")}; + } +`; + +export const ContentContainer = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +`; + +export const Title = styled.div` + font-size: 14px; + font-weight: 500; + line-height: 24px; + text-align: left; + color: var(--gray800); +`; + +export const Price = styled.div` + font-size: 16px; + font-weight: 700; + line-height: 26px; + text-align: left; + color: var(--gray800); +`; + +export const HeartContainer = styled.div` + display: flex; + gap: 4px; +`; + +export const Heart = styled.img` + width: 16px; + height: 16px; +`; + +export const HeartCount = styled.div` + font-size: 12px; + font-weight: 500; + line-height: 18px; + text-align: left; + color: var(--gray600); +`; diff --git a/src/components/NoneImage/NoneImage.jsx b/src/components/NoneImage/NoneImage.jsx new file mode 100644 index 00000000..1234238a --- /dev/null +++ b/src/components/NoneImage/NoneImage.jsx @@ -0,0 +1,10 @@ +import * as S from "./NoneImage.styles"; +import noneImg from "../../assets/icons/image.svg"; + +export default function NoneImage({ list }) { + return ( + + + + ); +} diff --git a/src/components/NoneImage/NoneImage.styles.jsx b/src/components/NoneImage/NoneImage.styles.jsx new file mode 100644 index 00000000..6c4ee14b --- /dev/null +++ b/src/components/NoneImage/NoneImage.styles.jsx @@ -0,0 +1,32 @@ +import styled from "styled-components"; +import { getImgSize } from "../Items/ItemCard/ItemCard.styles"; + +export const NoneImgContainer = styled.div` + width: ${({ list }) => getImgSize(list, "PC")}; + height: ${({ list }) => getImgSize(list, "PC")}; + border-radius: 16px; + background-color: var(--gray200); + display: flex; + justify-content: center; + align-items: center; + + @media (max-width: 767px) { + width: ${({ list }) => getImgSize(list, "Moblie")}; + height: ${({ list }) => getImgSize(list, "Moblie")}; + } + + @media (min-width: 768px) and (max-width: 1199px) { + width: ${({ list }) => getImgSize(list, "Tablet")}; + height: ${({ list }) => getImgSize(list, "Tablet")}; + } + + @media (min-width: 1200px) { + width: ${({ list }) => getImgSize(list, "PC")}; + height: ${({ list }) => getImgSize(list, "PC")}; + } +`; + +export const NoneImg = styled.img` + width: 60px; + height: 60px; +`; diff --git a/src/components/NoneItem/NoneItem.jsx b/src/components/NoneItem/NoneItem.jsx new file mode 100644 index 00000000..de86223d --- /dev/null +++ b/src/components/NoneItem/NoneItem.jsx @@ -0,0 +1,9 @@ +import * as S from "./NoneItem.styles"; + +export default function NoneItem({ list }) { + return ( + + 검색한 상품이 없습니다 + + ); +} diff --git a/src/components/NoneItem/NoneItem.styles.jsx b/src/components/NoneItem/NoneItem.styles.jsx new file mode 100644 index 00000000..720878a4 --- /dev/null +++ b/src/components/NoneItem/NoneItem.styles.jsx @@ -0,0 +1,17 @@ +import { styled } from "styled-components"; + +export const NoneItemContainer = styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + padding: 250px 0; +`; + +export const Text = styled.div` + font-size: 20px; + font-weight: 700; + line-height: 32px; + color: var(--gray900); +`; diff --git a/src/components/Paging/Paging.jsx b/src/components/Paging/Paging.jsx new file mode 100644 index 00000000..95de4453 --- /dev/null +++ b/src/components/Paging/Paging.jsx @@ -0,0 +1,20 @@ +import * as S from "./Paging.styles"; +import Pagination from "react-js-pagination"; +import { ReactComponent as Left } from "../../assets/icons/arrowLeft.svg"; +import { ReactComponent as Right } from "../../assets/icons/arrowRight.svg"; + +export default function Paging({ currentPage, pageSize, totalItemsCount, setPage }) { + return ( + + } + nextPageText={} + onChange={(currentPage) => setPage(currentPage)} + /> + + ); +} diff --git a/src/components/Paging/Paging.styles.jsx b/src/components/Paging/Paging.styles.jsx new file mode 100644 index 00000000..af75a280 --- /dev/null +++ b/src/components/Paging/Paging.styles.jsx @@ -0,0 +1,50 @@ +import { styled } from "styled-components"; + +export const PagingContainer = styled.div` + width: 100%; + display: flex; + margin-bottom: 40px; + + .pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + padding: 0; + margin: auto; + + li { + width: 40px; + height: 40px; + border: 1px solid var(--gray200); + border-radius: 40px; + font-size: 16px; + font-weight: 600; + line-height: 26px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + user-select: none; + + &.active { + background-color: #2f80ed; + } + &:first-child, + &:last-child { + display: none; + } + } + + li.active a { + color: var(--gray50); + } + + a { + display: flex; + justify-content: center; + align-items: center; + color: var(--gray500); + } + } +`; diff --git a/src/components/Router.jsx b/src/components/Router.jsx new file mode 100644 index 00000000..e526ca45 --- /dev/null +++ b/src/components/Router.jsx @@ -0,0 +1,19 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Layout from "../styles/Layout"; +import ItemPage from "./pages/ItemPage/ItemPage"; +import FreeBoardPage from "./pages/FreeBoardPage/FreeBoardPage"; +import AddItemPage from "./pages/AddItemPage/AddItemPage"; + +export default function Router() { + return ( + + + }> + } /> + } /> + } /> + + + + ); +} diff --git a/src/components/Search/Search.jsx b/src/components/Search/Search.jsx new file mode 100644 index 00000000..b6cf58a7 --- /dev/null +++ b/src/components/Search/Search.jsx @@ -0,0 +1,20 @@ +import * as S from "./Search.styles"; +import searchImg from "../../assets/icons/search.svg"; +import { useMemo } from "react"; +import { debounce } from "lodash"; + +export default function Search({ onSearch }) { + const debouncedOnSearch = useMemo(() => debounce(onSearch, 500), [onSearch]); + + const handleSearchChange = (e) => { + const search = e.target.value; + debouncedOnSearch(search); + }; + + return ( + + + + + ); +} diff --git a/src/components/Search/Search.styles.jsx b/src/components/Search/Search.styles.jsx new file mode 100644 index 00000000..90c44393 --- /dev/null +++ b/src/components/Search/Search.styles.jsx @@ -0,0 +1,41 @@ +import { styled } from "styled-components"; + +export const SearchContainer = styled.div` + width: 325px; + height: 42px; + padding: 9px 18px; + background-color: var(--gray100); + border-radius: 12px; + display: flex; + gap: 4px; + + @media (max-width: 767px) { + width: 282px; + } + + @media (min-width: 768px) and (max-width: 1199px) { + width: 242px; + } +`; + +export const SearchImg = styled.img` + width: 24px; + height: 24px; +`; + +export const Input = styled.input` + width: 100%; + border: none; + background-color: transparent; + font-size: 16px; + font-weight: 400; + line-height: 26px; + + &:focus { + outline: none; + } + + &::placeholder { + color: var(--gray400); + } +`; diff --git a/src/components/common/Dropdown/Dropdown.jsx b/src/components/common/Dropdown/Dropdown.jsx new file mode 100644 index 00000000..e64aba65 --- /dev/null +++ b/src/components/common/Dropdown/Dropdown.jsx @@ -0,0 +1,34 @@ +import * as S from "./Dropdown.styles"; +import { useState } from "react"; + +export default function Dropdown({ sortOption = "최신순", onChange, list = [] }) { + const [isOpen, setIsOpen] = useState(false); + + const handleOpenClick = () => { + setIsOpen(!isOpen); + }; + + return ( + + + {sortOption} + + + {isOpen && ( + + {list.map((item, idx) => ( + { + onChange(item); + setIsOpen(false); + }} + > + {item} + + ))} + + )} + + ); +} diff --git a/src/components/common/Dropdown/Dropdown.styles.jsx b/src/components/common/Dropdown/Dropdown.styles.jsx new file mode 100644 index 00000000..cb919d54 --- /dev/null +++ b/src/components/common/Dropdown/Dropdown.styles.jsx @@ -0,0 +1,89 @@ +import { styled } from "styled-components"; +import down from "../../../assets/icons/arrowDown.svg"; +import up from "../../../assets/icons/arrowUp.svg"; +import dropdown from "../../../assets/icons/dropdown.svg"; + +export const DropdownContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 20px; + width: 130px; + height: 42px; + position: relative; + + @media screen and (max-width: 767px) { + width: 42px; + } +`; + +export const Present = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 42px; + padding: 12px 20px; + border-radius: 12px; + border: 1px solid #e5e7eb; + background-color: white; + cursor: pointer; + + @media screen and (max-width: 767px) { + justify-content: flex-end; + padding: 9px; + } +`; + +export const PresentValue = styled.div` + font-size: 16px; + font-weight: 400; + line-height: 26px; + color: var(--gray800); + + @media screen and (max-width: 767px) { + display: none; + } +`; + +// UI component에서 style 작업을 하고 싶지 않아 content를 이용해서 icon 변경했는데 content를 쓰는 게 맞는지 모르겠습니다. +export const Arrow = styled.img` + width: 24px; + height: 24px; + user-select: none; + content: url(${({ $isOpen }) => ($isOpen ? up : down)}); + + @media screen and (max-width: 767px) { + content: url(${dropdown}); + } +`; + +export const List = styled.div` + width: 130px; + background-color: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + position: absolute; + top: 55px; + color: var(--gray800); +`; + +export const ListItem = styled.div` + height: 42px; + display: flex; + justify-content: center; + align-items: center; + border-bottom: 1px solid #e5e7eb; + font-size: 16px; + font-weight: 400; + line-height: 26px; + cursor: pointer; + + &:hover { + color: var(--primary); + } + + &:last-child { + border-bottom: none; + } +`; diff --git a/src/components/common/Header/Header.jsx b/src/components/common/Header/Header.jsx new file mode 100644 index 00000000..e8bd96be --- /dev/null +++ b/src/components/common/Header/Header.jsx @@ -0,0 +1,36 @@ +import { NavLink } from "react-router-dom"; +import * as S from "./Header.styles"; +import logo from "../../../assets/icons/panda.svg"; +import user from "../../../assets/icons/user.svg"; + +const activeLink = ({ isActive }) => { + return { + color: isActive ? "var(--primary)" : "var(--gray600)", + }; +}; + +export default function Header() { + return ( + + + + + 판다마켓 + + + + + 자유게시판 + + + + + 중고마켓 + + + + + + + ); +} diff --git a/src/components/common/Header/Header.styles.jsx b/src/components/common/Header/Header.styles.jsx new file mode 100644 index 00000000..429ac353 --- /dev/null +++ b/src/components/common/Header/Header.styles.jsx @@ -0,0 +1,64 @@ +import styled from "styled-components"; + +export const HeaderContainer = styled.div` + background: var(--white); + display: flex; + justify-content: space-between; + align-items: center; + position: sticky; + top: 0; + border-bottom: 1px solid #dfdfdf; + width: 100%; + height: 70px; + z-index: 1; + @media (min-width: 1200px) { + padding: 9px 200px; + } + @media screen and (min-width: 768px) and (max-width: 1199px) { + padding: 9px 24px; + } + @media screen and (max-width: 767px) { + padding: 9px 16px; + } +`; + +export const Nav = styled.div` + display: flex; +`; + +export const LogoContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-right: 16px; + cursor: pointer; +`; + +export const Logo = styled.img` + width: 40px; + height: 40px; +`; + +export const Title = styled.div` + font-family: ROKAF Sans; + font-size: 25px; + font-weight: 700; + color: var(--primary); +`; + +export const NavList = styled.div` + display: flex; + gap: 10px; +`; + +export const NavItems = styled.div` + font-size: 18px; + font-weight: 700; + line-height: 26px; + text-align: center; + padding: 15px 21px; + color: var(--gray600); + cursor: pointer; +`; + +export const User = styled(Logo)``; diff --git a/src/components/pages/AddItemPage/AddItemPage.jsx b/src/components/pages/AddItemPage/AddItemPage.jsx new file mode 100644 index 00000000..116f504d --- /dev/null +++ b/src/components/pages/AddItemPage/AddItemPage.jsx @@ -0,0 +1,3 @@ +export default function AddItemPage() { + return <>; +} diff --git a/src/components/pages/FreeBoardPage/FreeBoardPage.jsx b/src/components/pages/FreeBoardPage/FreeBoardPage.jsx new file mode 100644 index 00000000..26c71516 --- /dev/null +++ b/src/components/pages/FreeBoardPage/FreeBoardPage.jsx @@ -0,0 +1,3 @@ +export default function FreeBoardPage() { + return <>; +} diff --git a/src/components/pages/ItemPage/ItemPage.jsx b/src/components/pages/ItemPage/ItemPage.jsx new file mode 100644 index 00000000..04e17fe1 --- /dev/null +++ b/src/components/pages/ItemPage/ItemPage.jsx @@ -0,0 +1,90 @@ +import * as S from "./ItemPage.styles"; +import BestItems from "../../Items/BestItems/BestItems"; +import AllItems from "../../Items/AllItems/AllItems"; +import { useEffect, useState } from "react"; +import { getProducts } from "../../../api/products"; +import Paging from "../../Paging/Paging"; + +export default function ItemPage() { + const [items, setItems] = useState([]); + const [bestItems, setBestItems] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [sortOption, setSortOption] = useState("최신순"); + const [keyword, setKeyword] = useState(""); + const [totalItems, setTotalItems] = useState(0); + const [showItems, setShowItems] = useState(4); + + const orderByValue = sortOption === "최신순" ? "recent" : "favorite"; + + const updateItems = () => { + if (window.innerWidth <= 767) { + setPageSize(4); + } else if (window.innerWidth >= 768 && window.innerWidth <= 1199) { + setPageSize(6); + } else { + setPageSize(10); + } + }; + + const updateBestItems = () => { + if (window.innerWidth <= 767) { + setShowItems(1); + } else if (window.innerWidth >= 768 && window.innerWidth <= 1199) { + setShowItems(2); + } else { + setShowItems(4); + } + }; + + useEffect(() => { + getProducts({ + page: 1, + pageSize: showItems, + orderBy: "favorite", + keyword: "", + }).then((result) => { + if (!result) return; + const sortedBestItems = [...result.list].slice(0, 4); + setBestItems(sortedBestItems); + }); + }, [showItems]); + + useEffect(() => { + getProducts({ + page: currentPage, + pageSize: pageSize, + orderBy: orderByValue, + keyword: keyword, + }).then((result) => { + if (!result) return; + setItems(result.list); + setTotalItems(result.totalCount); + }); + }, [currentPage, pageSize, orderByValue, keyword]); + + useEffect(() => { + updateItems(); + window.addEventListener("resize", updateItems); + + return () => { + window.removeEventListener("resize", updateItems); + }; + }, []); + + const handleChangeClick = (sortOption) => { + setSortOption(sortOption); + }; + + const responsiveItems = bestItems.slice(0, showItems); + + return ( + + + + {items.length !== 0 && ( + + )} + + ); +} diff --git a/src/components/pages/ItemPage/ItemPage.styles.jsx b/src/components/pages/ItemPage/ItemPage.styles.jsx new file mode 100644 index 00000000..54937919 --- /dev/null +++ b/src/components/pages/ItemPage/ItemPage.styles.jsx @@ -0,0 +1,11 @@ +import styled from "styled-components"; + +export const Container = styled.div` + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 24px auto; + gap: 40px; +`; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index ec2585e8..00000000 --- a/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/src/index.js b/src/index.js index d563c0fb..d4c777aa 100644 --- a/src/index.js +++ b/src/index.js @@ -1,17 +1,11 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import GlobalStyle from "./styles/GlobalStyle"; -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render( - +ReactDOM.createRoot(document.getElementById("root")).render( + <> + - + ); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 9dfc1c05..00000000 --- a/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js deleted file mode 100644 index 5253d3ad..00000000 --- a/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/src/setupTests.js b/src/setupTests.js deleted file mode 100644 index 8f2609b7..00000000 --- a/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/src/styles/GlobalStyle.jsx b/src/styles/GlobalStyle.jsx new file mode 100644 index 00000000..50bdafcf --- /dev/null +++ b/src/styles/GlobalStyle.jsx @@ -0,0 +1,43 @@ +import { createGlobalStyle } from "styled-components"; + +const GlobalStyle = createGlobalStyle` + * { + box-sizing: border-box; + text-decoration: none; + list-style: none; +} + +body { + margin: 0; +} + +:root { + --primary: #3692ff; + --primary-hover: #1967d6; + --dark-text: #374151; + --bright-text: #f3f4f6; + --white: #ffffff; + --main-background: #fcfcfc; + --footer-background: #111827; + --landing-background: #cfe5ff; + --easy-login: #e6f2ff; + --gray50: #f9fafb; + --gray100: #f3f4f6; + --gray200: #e5e7eb; + --gray400: #9ca3af; + --gray500: #6b7280; + --gray600: #4b5563; + --gray700: #374151; + --gray800: #1f2937; + --gray900: #111827; + --error-red: #f74747; + --font: "Pretendard", sans-serif; +} + +@font-face { + font-family: ROKAF Sans; + src: url("font/ROKAF\ Slab\ Serif\ Bold.ttf"); +} +`; + +export default GlobalStyle; diff --git a/src/styles/Layout.jsx b/src/styles/Layout.jsx new file mode 100644 index 00000000..3b81393b --- /dev/null +++ b/src/styles/Layout.jsx @@ -0,0 +1,11 @@ +import { Outlet } from "react-router-dom"; +import Header from "../components/common/Header/Header"; + +export default function Layout() { + return ( + <> +
+ + + ); +}