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 (
-
- );
-}
-
-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 (
+ <>
+
+
+ >
+ );
+}