diff --git a/.gitignore b/.gitignore
index 4d29575d..465c59f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,10 @@
# production
/build
+# backup
+/backups
+.bak
+
# misc
.DS_Store
.env.local
@@ -21,3 +25,4 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+.vercel
diff --git a/package-lock.json b/package-lock.json
index bffbb592..17ffb6dc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,20 +1,23 @@
{
- "name": "FE10-Sprint-Mission5",
+ "name": "fe10-sprint-mission7",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "FE10-Sprint-Mission5",
+ "name": "fe10-sprint-mission7",
"version": "0.1.0",
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "date-fns": "^3.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
+ "react-spinners": "^0.13.8",
+ "styled-components": "^6.1.8",
"web-vitals": "^2.1.4"
}
},
@@ -2508,6 +2511,27 @@
"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==",
+ "license": "MIT",
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
+ "license": "MIT"
+ },
"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",
@@ -2524,9 +2548,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.11.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
- "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
+ "version": "4.11.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
+ "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@@ -2601,22 +2625,22 @@
}
},
"node_modules/@eslint/js": {
- "version": "8.57.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
- "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
"license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
- "version": "0.11.14",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
- "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
"deprecated": "Use @eslint/config-array instead",
"license": "Apache-2.0",
"dependencies": {
- "@humanwhocodes/object-schema": "^2.0.2",
+ "@humanwhocodes/object-schema": "^2.0.3",
"debug": "^4.3.1",
"minimatch": "^3.0.5"
},
@@ -2662,9 +2686,9 @@
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -3683,9 +3707,9 @@
}
},
"node_modules/@remix-run/router": {
- "version": "1.19.1",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz",
- "integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==",
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
+ "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -4270,9 +4294,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
},
"node_modules/@types/express": {
@@ -4354,9 +4378,9 @@
}
},
"node_modules/@types/jest": {
- "version": "29.5.12",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz",
- "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==",
+ "version": "29.5.13",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz",
+ "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==",
"license": "MIT",
"dependencies": {
"expect": "^29.0.0",
@@ -4414,9 +4438,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "22.5.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
- "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
+ "version": "22.5.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz",
+ "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
@@ -4444,9 +4468,9 @@
"license": "MIT"
},
"node_modules/@types/prop-types": {
- "version": "15.7.12",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
- "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
+ "version": "15.7.13",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
+ "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"license": "MIT"
},
"node_modules/@types/q": {
@@ -4456,9 +4480,9 @@
"license": "MIT"
},
"node_modules/@types/qs": {
- "version": "6.9.15",
- "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
- "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==",
+ "version": "6.9.16",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
+ "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
"license": "MIT"
},
"node_modules/@types/range-parser": {
@@ -4468,9 +4492,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "18.3.5",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
- "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
+ "version": "18.3.8",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
+ "integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -4552,6 +4576,12 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"license": "MIT"
},
+ "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==",
+ "license": "MIT"
+ },
"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",
@@ -5606,13 +5636,13 @@
}
},
"node_modules/babel-loader": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz",
- "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==",
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz",
+ "integrity": "sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==",
"license": "MIT",
"dependencies": {
"find-cache-dir": "^3.3.1",
- "loader-utils": "^2.0.0",
+ "loader-utils": "^2.0.4",
"make-dir": "^3.1.0",
"schema-utils": "^2.6.5"
},
@@ -5873,9 +5903,9 @@
"license": "MIT"
},
"node_modules/body-parser": {
- "version": "1.20.2",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
- "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
@@ -5886,7 +5916,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
- "qs": "6.11.0",
+ "qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -6103,6 +6133,15 @@
"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==",
+ "license": "MIT",
+ "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",
@@ -6116,9 +6155,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001657",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001657.tgz",
- "integrity": "sha512-DPbJAlP8/BAXy3IgiWmZKItubb3TYGP0WscQQlVGIfT4s/YlFYVuJgyOsQNP7rJRChx/qdMeLJQJP0Sgg2yjNA==",
+ "version": "1.0.30001662",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz",
+ "integrity": "sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==",
"funding": [
{
"type": "opencollective",
@@ -6236,9 +6275,9 @@
}
},
"node_modules/cjs-module-lexer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.0.tgz",
- "integrity": "sha512-N1NGmowPlGBLsOZLPvm48StN04V4YvQRL0i6b7ctrVY3epjP/ct7hFLOItz6pDIvRjwpfPxi52a2UWV2ziir8g==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz",
+ "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==",
"license": "MIT"
},
"node_modules/clean-css": {
@@ -6652,6 +6691,15 @@
"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==",
+ "license": "ISC",
+ "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",
@@ -6801,6 +6849,17 @@
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
"license": "MIT"
},
+ "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==",
+ "license": "MIT",
+ "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",
@@ -7086,13 +7145,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/date-fns": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
+ "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
"node_modules/debug": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
- "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -7509,9 +7578,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.14",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.14.tgz",
- "integrity": "sha512-bEfPECb3fJ15eaDnu9LEJ2vPGD6W1vt7vZleSVyFhYuMIKm3vz/g9lt7IvEzgdwj58RjbPKUF2rXTCN/UW47tQ==",
+ "version": "1.5.27",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz",
+ "integrity": "sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==",
"license": "ISC"
},
"node_modules/emittery": {
@@ -7542,9 +7611,9 @@
}
},
"node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -7839,16 +7908,16 @@
}
},
"node_modules/eslint": {
- "version": "8.57.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
- "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.57.0",
- "@humanwhocodes/config-array": "^0.11.14",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
@@ -7942,9 +8011,9 @@
}
},
"node_modules/eslint-module-utils": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz",
- "integrity": "sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==",
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz",
+ "integrity": "sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==",
"license": "MIT",
"dependencies": {
"debug": "^3.2.7"
@@ -8111,9 +8180,9 @@
}
},
"node_modules/eslint-plugin-react": {
- "version": "7.35.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.2.tgz",
- "integrity": "sha512-Rbj2R9zwP2GYNcIak4xoAMV57hrBh3hTaR0k7hVjwCQgryE/pw5px4b13EYjduOI0hfXyZhwBxaGpOTbWSGzKQ==",
+ "version": "7.36.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz",
+ "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==",
"license": "MIT",
"dependencies": {
"array-includes": "^3.1.8",
@@ -8545,37 +8614,37 @@
}
},
"node_modules/express": {
- "version": "4.19.2",
- "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
- "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
+ "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
- "body-parser": "1.20.2",
+ "body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
- "encodeurl": "~1.0.2",
+ "encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
- "finalhandler": "1.2.0",
+ "finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
- "merge-descriptors": "1.0.1",
+ "merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.7",
+ "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
- "qs": "6.11.0",
+ "qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
- "send": "0.18.0",
- "serve-static": "1.15.0",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@@ -8785,13 +8854,13 @@
}
},
"node_modules/finalhandler": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
- "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
- "encodeurl": "~1.0.2",
+ "encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@@ -8868,9 +8937,9 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
- "version": "1.15.8",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.8.tgz",
- "integrity": "sha512-xgrmBhBToVKay1q2Tao5LI26B83UhrB/vM1avwVSDzt8rx3rO6AizBAaF46EgksTVr+rFTQaqZZ9MVBfUe4nig==",
+ "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",
@@ -12447,9 +12516,9 @@
}
},
"node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -12789,9 +12858,9 @@
}
},
"node_modules/launch-editor": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.2.tgz",
- "integrity": "sha512-eF5slEUZXmi6WvFzI3dYcv+hA24/iKnROf24HztcURJpSz9RBmBgz5cNCVOeguouf1llrwy6Yctl4C4HM+xI8g==",
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz",
+ "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==",
"license": "MIT",
"dependencies": {
"picocolors": "^1.0.0",
@@ -13015,10 +13084,13 @@
}
},
"node_modules/merge-descriptors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
- "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
- "license": "MIT"
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
},
"node_modules/merge-stream": {
"version": "2.0.0",
@@ -13177,9 +13249,9 @@
}
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/multicast-dns": {
@@ -13760,9 +13832,9 @@
"license": "ISC"
},
"node_modules/path-to-regexp": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
- "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
+ "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"license": "MIT"
},
"node_modules/path-type": {
@@ -13896,9 +13968,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.45",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
- "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
+ "version": "8.4.47",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+ "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [
{
"type": "opencollective",
@@ -13916,8 +13988,8 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.0.1",
- "source-map-js": "^1.2.0"
+ "picocolors": "^1.1.0",
+ "source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -15329,12 +15401,12 @@
}
},
"node_modules/qs": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
- "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
- "side-channel": "^1.0.4"
+ "side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
@@ -15607,12 +15679,12 @@
}
},
"node_modules/react-router": {
- "version": "6.26.1",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz",
- "integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==",
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz",
+ "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==",
"license": "MIT",
"dependencies": {
- "@remix-run/router": "1.19.1"
+ "@remix-run/router": "1.19.2"
},
"engines": {
"node": ">=14.0.0"
@@ -15622,13 +15694,13 @@
}
},
"node_modules/react-router-dom": {
- "version": "6.26.1",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz",
- "integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==",
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz",
+ "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==",
"license": "MIT",
"dependencies": {
- "@remix-run/router": "1.19.1",
- "react-router": "6.26.1"
+ "@remix-run/router": "1.19.2",
+ "react-router": "6.26.2"
},
"engines": {
"node": ">=14.0.0"
@@ -15711,6 +15783,16 @@
}
}
},
+ "node_modules/react-spinners": {
+ "version": "0.13.8",
+ "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz",
+ "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -15799,9 +15881,9 @@
"license": "MIT"
},
"node_modules/regenerate-unicode-properties": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz",
- "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==",
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz",
+ "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==",
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2"
@@ -16374,9 +16456,9 @@
}
},
"node_modules/send": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
- "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
@@ -16412,11 +16494,14 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
@@ -16506,15 +16591,15 @@
}
},
"node_modules/serve-static": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
- "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
- "encodeurl": "~1.0.2",
+ "encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
- "send": "0.18.0"
+ "send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
@@ -16558,6 +16643,12 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
+ "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==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -16654,9 +16745,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
- "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+ "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==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -17170,6 +17261,68 @@
"webpack": "^5.0.0"
}
},
+ "node_modules/styled-components": {
+ "version": "6.1.13",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz",
+ "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==",
+ "license": "MIT",
+ "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/styled-components/node_modules/postcss": {
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/styled-components/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+ "license": "0BSD"
+ },
"node_modules/stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -17186,6 +17339,12 @@
"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==",
+ "license": "MIT"
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -17469,9 +17628,9 @@
"license": "MIT"
},
"node_modules/tailwindcss": {
- "version": "3.4.10",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
- "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
+ "version": "3.4.12",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
+ "integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -17570,9 +17729,9 @@
}
},
"node_modules/terser": {
- "version": "5.31.6",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz",
- "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==",
+ "version": "5.33.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.33.0.tgz",
+ "integrity": "sha512-JuPVaB7s1gdFKPKTelwUyRq5Sid2A3Gko2S0PncwdBq7kN9Ti9HPWDQ06MPsEDGsZeVESjKEnyGy68quBk1w6g==",
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@@ -17971,9 +18130,9 @@
}
},
"node_modules/typescript": {
- "version": "5.5.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
- "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
+ "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
@@ -18012,9 +18171,9 @@
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
- "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
+ "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
"license": "MIT",
"engines": {
"node": ">=4"
@@ -18034,9 +18193,9 @@
}
},
"node_modules/unicode-match-property-value-ecmascript": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz",
- "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz",
+ "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==",
"license": "MIT",
"engines": {
"node": ">=4"
diff --git a/package.json b/package.json
index 435ab985..954cf99b 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,18 @@
{
- "name": "fe10-sprint-mission5",
+ "name": "fe10-sprint-mission7",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "date-fns": "^3.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
+ "react-spinners": "^0.13.8",
+ "styled-components": "^6.1.8",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/package.json.bak b/package.json.bak
deleted file mode 100644
index 5ce1b88e..00000000
--- a/package.json.bak
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "name": "panda-market-react",
- "version": "0.1.0",
- "private": true,
- "dependencies": {
- "@testing-library/jest-dom": "^5.17.0",
- "@testing-library/react": "^13.4.0",
- "@testing-library/user-event": "^13.5.0",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "react-router-dom": "^6.22.3",
- "react-scripts": "5.0.1",
- "web-vitals": "^2.1.4"
- },
- "scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
- "eject": "react-scripts eject"
- },
- "eslintConfig": {
- "extends": [
- "react-app",
- "react-app/jest"
- ]
- },
- "browserslist": {
- "production": [
- ">0.2%",
- "not dead",
- "not op_mini all"
- ],
- "development": [
- "last 1 chrome version",
- "last 1 firefox version",
- "last 1 safari version"
- ]
- }
-}
diff --git a/public/index.html b/public/index.html
index 8c7ab456..bff25942 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/src/App.js b/src/App.js
index 41cabf19..8ec55a70 100644
--- a/src/App.js
+++ b/src/App.js
@@ -5,16 +5,19 @@ import MarketPage from "./pages/MarketPage/MarketPage";
import AddItemPage from "./pages/AddItemPage/AddItemPage";
import CommunityFeedPage from "./pages/CommunityFeedPage/CommunityFeedPage";
import Header from "./components/Layout/Header";
+import ItemPage from "./pages/ItemPage/ItemPage";
function App() {
return (
+
} />
} />
} />
+ } />
} />
} />
diff --git a/src/api/itemApi.js b/src/api/itemApi.js
index d4346262..51f3793b 100644
--- a/src/api/itemApi.js
+++ b/src/api/itemApi.js
@@ -16,3 +16,44 @@ export async function getProducts(params = {}) {
throw error;
}
}
+
+export async function getProductDetail(productId) {
+ if (!productId) {
+ throw new Error("Invalid product ID");
+ }
+
+ try {
+ const response = await fetch(
+ `https://panda-market-api.vercel.app/products/${productId}`
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error: ${response.status}`);
+ }
+ const body = await response.json();
+ return body;
+ } catch (error) {
+ console.error("Failed to fetch product detail:", error);
+ throw error;
+ }
+}
+
+export async function getProductComments({ productId, params }) {
+ if (!productId) {
+ throw new Error("Invalid product ID");
+ }
+
+ try {
+ const query = new URLSearchParams(params).toString();
+ const response = await fetch(
+ `https://panda-market-api.vercel.app/products/${productId}/comments?${query}`
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error: ${response.status}`);
+ }
+ const body = await response.json();
+ return body;
+ } catch (error) {
+ console.error("Failed to fetch product comments:", error);
+ throw error;
+ }
+}
diff --git a/src/assets/images/icons/ic_back.svg b/src/assets/images/icons/ic_back.svg
new file mode 100644
index 00000000..f8d47f89
--- /dev/null
+++ b/src/assets/images/icons/ic_back.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/icons/ic_kebab.svg b/src/assets/images/icons/ic_kebab.svg
new file mode 100644
index 00000000..dd7ed7f5
--- /dev/null
+++ b/src/assets/images/icons/ic_kebab.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/icons/ic_plus.svg b/src/assets/images/icons/ic_plus.svg
new file mode 100644
index 00000000..5bb9abf5
--- /dev/null
+++ b/src/assets/images/icons/ic_plus.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/icons/ic_x.svg b/src/assets/images/icons/ic_x.svg
new file mode 100644
index 00000000..8ff53504
--- /dev/null
+++ b/src/assets/images/icons/ic_x.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/ui/empty-comments.svg b/src/assets/images/ui/empty-comments.svg
new file mode 100644
index 00000000..d9d2d590
--- /dev/null
+++ b/src/assets/images/ui/empty-comments.svg
@@ -0,0 +1,17 @@
+
diff --git a/src/assets/images/ui/ic_profile.svg b/src/assets/images/ui/ic_profile.svg
new file mode 100644
index 00000000..7445a45c
--- /dev/null
+++ b/src/assets/images/ui/ic_profile.svg
@@ -0,0 +1,24 @@
+
diff --git a/src/components/Layout/Header.css b/src/components/Layout/Header.css
index 1bc2b02c..e65c9199 100644
--- a/src/components/Layout/Header.css
+++ b/src/components/Layout/Header.css
@@ -13,7 +13,7 @@
gap: 8px;
font-weight: bold;
font-size: 16px;
- color: #4b5563;
+ color: var(--gray-600);
}
.globalHeader nav ul li a:hover {
diff --git a/src/components/Layout/Header.jsx b/src/components/Layout/Header.jsx
index cd249a86..a6d715fd 100644
--- a/src/components/Layout/Header.jsx
+++ b/src/components/Layout/Header.jsx
@@ -1,29 +1,38 @@
-import React from 'react'
-import Logo from '../../assets/images/logo/logo.svg'
-import { Link, NavLink } from 'react-router-dom'
-import './Header.css'
+import React from "react";
+import Logo from "../../assets/images/logo/logo.svg";
+import { Link, NavLink, useLocation } from "react-router-dom";
+import "./Header.css";
-function getLinkStyle ({ isActive }) {
- return { color: isActive ? 'var(--blue)' : undefined }
+function getLinkStyle({ isActive }) {
+ return { color: isActive ? "var(--blue)" : undefined };
}
-function Header () {
+function Header() {
+ const location = useLocation(); // 현재 경로 정보
+
return (
-
-
-
-
+
+
+
+
-
+
로그인
- )
+ );
}
-export default Header
+export default Header;
diff --git a/src/components/UI/DeleteButton.jsx b/src/components/UI/DeleteButton.jsx
new file mode 100644
index 00000000..8244426a
--- /dev/null
+++ b/src/components/UI/DeleteButton.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import styled from "styled-components";
+import { ReactComponent as CloseIcon } from "../../assets/images/icons/ic_x.svg";
+
+const Button = styled.button`
+ background-color: ${({ theme }) => theme.colors.gray[400]};
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.blue.primary};
+ }
+`;
+
+function DeleteButton({ onClick, label }) {
+ return (
+
+ );
+}
+
+export default DeleteButton;
diff --git a/src/components/UI/DropdownMenu.css b/src/components/UI/DropdownMenu.css
new file mode 100644
index 00000000..1eeae960
--- /dev/null
+++ b/src/components/UI/DropdownMenu.css
@@ -0,0 +1,33 @@
+.sortButtonWrapper {
+ position: relative;
+}
+
+.sortDropdownTriggerButton {
+ border: 1px solid var(--gray-200);
+ border-radius: 12px;
+ padding: 9px;
+ margin-left: 8px;
+}
+
+.dropdownMenu {
+ position: absolute;
+ top: 110%;
+ right: 0;
+ background: #fff;
+ border-radius: 8px;
+ border: 1px solid var(--gray-200);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ z-index: 99;
+}
+
+.dropdownItem {
+ padding: 12px 44px;
+ border-bottom: 1px solid var(--gray-200);
+ font-size: 16px;
+ color: var(--gray-800);
+ cursor: pointer;
+}
+
+.dropdownItem:last-child {
+ border-bottom: none;
+}
diff --git a/src/components/UI/DropdownMenu.jsx b/src/components/UI/DropdownMenu.jsx
new file mode 100644
index 00000000..9a883fec
--- /dev/null
+++ b/src/components/UI/DropdownMenu.jsx
@@ -0,0 +1,43 @@
+import React, { useState } from "react";
+import "./DropdownMenu.css";
+import { ReactComponent as SortIcon } from "../../assets/images/icons/ic_sort.svg";
+
+function DropdownMenu({ onSortSelection }) {
+ const [isDropdownVisible, setIsDropdownVisible] = useState(false);
+
+ const toggleDropdown = () => {
+ setIsDropdownVisible(!isDropdownVisible);
+ };
+
+ return (
+
+
+
+ {isDropdownVisible && (
+
+
{
+ onSortSelection("recent");
+ setIsDropdownVisible(false);
+ }}
+ >
+ 최신순
+
+
{
+ onSortSelection("favorite");
+ setIsDropdownVisible(false);
+ }}
+ >
+ 인기순
+
+
+ )}
+
+ );
+}
+export default DropdownMenu;
diff --git a/src/components/UI/Icon.jsx b/src/components/UI/Icon.jsx
new file mode 100644
index 00000000..4514ee10
--- /dev/null
+++ b/src/components/UI/Icon.jsx
@@ -0,0 +1,32 @@
+import React from 'react'
+import styled from 'styled-components'
+
+const IconWrapper = styled.div`
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+
+ svg {
+ fill: ${({ $fillColor }) => $fillColor || 'current'}; // 색 채움
+ width: ${({ $size }) => ($size ? `${$size}px` : 'auto')};
+ height: ${({ $size }) => ($size ? `${$size}px` : 'auto')};
+ }
+
+ svg path {
+ stroke: ${({ $fillColor, $outlineColor }) =>
+ $fillColor || $outlineColor || 'currentColor'};
+ }
+`
+
+const Icon = ({
+ iconComponent: IconComponent,
+ size,
+ fillColor,
+ outlineColor
+}) => (
+
+
+
+)
+
+export default Icon
diff --git a/src/components/UI/ImageUpload.jsx b/src/components/UI/ImageUpload.jsx
new file mode 100644
index 00000000..077ea9d8
--- /dev/null
+++ b/src/components/UI/ImageUpload.jsx
@@ -0,0 +1,122 @@
+import React, { useState } from "react";
+import { Label } from "./InputItem";
+import styled, { css } from "styled-components";
+import { ReactComponent as PlusIcon } from "../../assets/images/icons/ic_plus.svg";
+import DeleteButton from "./DeleteButton";
+
+const ImageUploadContainer = styled.div`
+ display: flex;
+ gap: 8px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ gap: 18px;
+ }
+
+ @media ${({ theme }) => theme.mediaQuery.desktop} {
+ gap: 24px;
+ }
+`;
+
+const squareStyles = css`
+ // 작은 화면에서는 max-width가 되기 전까지는 UploadButton과 ImagePreview가 각각 gap을 포함해 컨테이너 너비의 절반을 차지하도록 함
+ width: calc(50% - 4px);
+ max-width: 200px;
+ aspect-ratio: 1 / 1; // 정사각형 비율 유지
+ border-radius: 12px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ width: 162px;
+ }
+
+ @media ${({ theme }) => theme.mediaQuery.desktop} {
+ width: 282px;
+ }
+`;
+
+// file input과 연관 짓기 위해 버튼이 대신 label로 설정
+const UploadButton = styled.label`
+ background-color: ${({ theme }) => theme.colors.gray[100]};
+ color: ${({ theme }) => theme.colors.gray[400]};
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ font-size: 16px;
+ cursor: pointer; // 버튼이 아닌 label을 사용한 경우 별도로 추가해 주세요
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.gray[50]};
+ }
+
+ ${squareStyles}
+`;
+
+const ImagePreview = styled.div`
+ background-image: url(${({ src }) => src});
+ background-size: cover;
+ background-position: center;
+ position: relative; // DeleteButton 포지셔닝을 위해 추가
+
+ ${squareStyles}
+`;
+
+const DeleteButtonWrapper = styled.div`
+ position: absolute;
+ top: 12px;
+ right: 12px;
+`;
+
+// 브라우저 기본 '파일 선택' 버튼 대신 커스텀 버튼을 사용하기 위해 file input을 숨김 처리
+const HiddenFileInput = styled.input`
+ display: none;
+`;
+
+function ImageUpload({ title }) {
+ const [imagePreviewUrl, setImagePreviewUrl] = useState("");
+
+ const handleImageChange = (event) => {
+ const file = event.target.files[0];
+ if (file) {
+ // 미리보기 주소 값(Object URL) 생성
+ const imageUrl = URL.createObjectURL(file);
+ setImagePreviewUrl(imageUrl);
+ }
+ };
+
+ const handleDelete = () => {
+ setImagePreviewUrl(""); // 미리보기 URL 리셋
+ };
+
+ return (
+
+ {title &&
}
+
+
+ {/* HiddenFileInput의 id와 label의 htmlFor 값을 매칭해 주세요 */}
+
+
+ 이미지 등록
+
+
+
+
+ {/* 업로드된 이미지가 있으면 썸네일 렌더링 */}
+ {imagePreviewUrl && (
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default ImageUpload;
diff --git a/src/components/UI/InputItem.jsx b/src/components/UI/InputItem.jsx
new file mode 100644
index 00000000..6d410812
--- /dev/null
+++ b/src/components/UI/InputItem.jsx
@@ -0,0 +1,76 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+const inputStyle = css`
+ padding: 16px 24px;
+ background-color: ${({ theme }) => theme.colors.gray[100]};
+ color: ${({ theme }) => theme.colors.gray[800]};
+ border: none;
+ border-radius: 12px;
+ font-size: 16px;
+ line-height: 24px;
+ width: 100%;
+
+ &::placeholder {
+ color: ${({ theme }) => theme.colors.gray[400]};
+ }
+
+ &:focus {
+ outline-color: ${({ theme }) => theme.colors.blue.primary};
+ }
+`;
+
+export const Label = styled.label`
+ display: block;
+ font-size: 14px;
+ font-weight: bold;
+ margin-bottom: 12px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ font-size: 18px;
+ }
+`;
+
+const InputField = styled.input`
+ ${inputStyle}
+`;
+
+const TextArea = styled.textarea`
+ ${inputStyle}
+ height: 200px;
+ resize: none;
+`;
+
+function InputItem({
+ id,
+ label,
+ value,
+ onChange,
+ placeholder,
+ onKeyDown,
+ isTextArea,
+}) {
+ return (
+
+ {label && }
+ {isTextArea ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export default InputItem;
diff --git a/src/components/UI/LoadingSpinner.jsx b/src/components/UI/LoadingSpinner.jsx
new file mode 100644
index 00000000..69b6b78d
--- /dev/null
+++ b/src/components/UI/LoadingSpinner.jsx
@@ -0,0 +1,52 @@
+import React, { useEffect, useState } from 'react'
+import styled from 'styled-components'
+import { PulseLoader } from 'react-spinners'
+
+const MaskedBackground = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #fff;
+ z-index: 9998;
+`
+
+const SpinnerOverlay = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ background: rgba(0, 0, 0, 0.2);
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 9999;
+`
+
+const LoadingSpinner = ({
+ size = 20,
+ color = 'var(--blue)',
+ minLoadTime = 500
+}) => {
+ const [isVisible, setIsVisible] = useState(true)
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setIsVisible(false)
+ }, minLoadTime)
+
+ return () => clearTimeout(timer)
+ }, [minLoadTime])
+
+ return isVisible ? (
+
+
+
+
+
+ ) : null
+}
+
+export default LoadingSpinner
diff --git a/src/components/UI/PaginationBar.css b/src/components/UI/PaginationBar.css
index 1841297a..f9f394f7 100644
--- a/src/components/UI/PaginationBar.css
+++ b/src/components/UI/PaginationBar.css
@@ -6,11 +6,11 @@
}
.paginationButton {
- border: 1px solid #e5e7eb;
+ border: 1px solid var(--gray-200);
border-radius: 50%;
width: 40px;
height: 40px;
- color: #6b7280;
+ color: var(--gray-500);
font-weight: 600;
font-size: 16px;
display: flex;
diff --git a/src/components/UI/TagInput.jsx b/src/components/UI/TagInput.jsx
new file mode 100644
index 00000000..7c186ebd
--- /dev/null
+++ b/src/components/UI/TagInput.jsx
@@ -0,0 +1,77 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import InputItem from "./InputItem";
+import { FlexContainer } from "../../styles/CommonStyles";
+import DeleteButton from "./DeleteButton";
+
+const TagButtonsSection = styled.div`
+ display: flex;
+ gap: 12px;
+ margin-top: 12px;
+ flex-wrap: wrap;
+`;
+
+const Tag = styled(FlexContainer)`
+ background-color: ${({ theme }) => theme.colors.gray[50]};
+ color: ${({ theme }) => theme.colors.gray[800]};
+ padding: 14px 14px 14px 16px;
+ border-radius: 999px;
+ min-width: 100px;
+`;
+
+const TagText = styled.span`
+ font-size: 16px;
+ line-height: 24px;
+ margin-right: 8px;
+ max-width: calc(100% - 28px);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+function TagInput({ tags, onAddTag, onRemoveTag }) {
+ const [input, setInput] = useState("");
+
+ const onPressEnter = (event) => {
+ if (event.nativeEvent.isComposing) return;
+
+ const inputString = input.trim();
+ if (event.key === "Enter" && inputString) {
+ event.preventDefault();
+ onAddTag(inputString);
+ setInput("");
+ }
+
+ // 입력 포커스를 잃었을때, 이전 까지 입력된 값으로 태그를 추가하도록 추후 변경 검
+ }
+
+ return (
+
+ setInput(e.target.value)}
+ onKeyDown={onPressEnter}
+ placeholder="태그를 입력해 주세요"
+ />
+
+ {tags.length > 0 && (
+
+ {tags.map((tag) => (
+
+ {tag}
+
+ onRemoveTag(tag)}
+ label={`${tag} 태그`}
+ />
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default TagInput;
diff --git a/src/index.js b/src/index.js
index b391783d..cbc45ee8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,10 +2,16 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles/global.css";
+import { ThemeProvider } from "styled-components";
+import theme from "./styles/theme";
+import GlobalStyle from "./styles/GlobalStyle";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
-
+
+
+
+
);
diff --git a/src/pages/AddItemPage/AddItemPage.jsx b/src/pages/AddItemPage/AddItemPage.jsx
index e7ba934c..7bef2229 100644
--- a/src/pages/AddItemPage/AddItemPage.jsx
+++ b/src/pages/AddItemPage/AddItemPage.jsx
@@ -1,7 +1,90 @@
-import React from "react";
+import React, { useState } from "react";
+import {
+ Button,
+ Container,
+ FlexContainer,
+ SectionTitle,
+} from "../../styles/CommonStyles";
+import styled from "styled-components";
+import InputItem from "../../components/UI/InputItem";
+import TagInput from "../../components/UI/TagInput";
+import ImageUpload from "../../components/UI/ImageUpload";
+
+const TitleSection = styled(FlexContainer)`
+ margin-bottom: 16px;
+`;
+
+const InputSection = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ gap: 24px;
+ }
+`;
function AddItemPage() {
- return
AddItemPage
;
+ const [name, setName] = useState("");
+ const [description, setDescription] = useState("");
+ const [price, setPrice] = useState("");
+ const [tags, setTags] = useState([]);
+
+ const addTag = (tag) => {
+ if (!tags.includes(tag)) {
+ setTags([...tags, tag]);
+ }
+ };
+
+ const removeTag = (tagToRemove) => {
+ setTags(tags.filter((tag) => tag !== tagToRemove));
+ };
+
+ const isSubmitDisabled = !name || !description || !price || !tags.length;
+
+ return (
+
+
+
+ );
}
export default AddItemPage;
diff --git a/src/pages/ItemPage/ItemPage.jsx b/src/pages/ItemPage/ItemPage.jsx
new file mode 100644
index 00000000..cabe21db
--- /dev/null
+++ b/src/pages/ItemPage/ItemPage.jsx
@@ -0,0 +1,78 @@
+import React, { useEffect, useState } from 'react'
+import { useParams } from 'react-router-dom'
+import styled from 'styled-components'
+import { Container, LineDivider, StyledLink } from '../../styles/CommonStyles'
+import { getProductDetail } from '../../api/itemApi'
+import ItemProfileSection from './components/ItemProfileSection'
+import ItemCommentSection from './components/ItemCommentSection'
+import { ReactComponent as BackIcon } from '../../assets/images/icons/ic_back.svg'
+import LoadingSpinner from '../../components/UI/LoadingSpinner'
+
+const BackToMarketPageLink = styled(StyledLink)`
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 18px;
+ font-weight: 600;
+ margin: 0 auto;
+`
+
+function ItemPage () {
+ const [product, setProduct] = useState(null)
+ const [isLoading, setIsLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ const { productId } = useParams()
+
+ useEffect(() => {
+ async function fetchProduct () {
+ if (!productId) {
+ setError('상품 아이디가 제공되지 않았어요.')
+ setIsLoading(false)
+ return
+ }
+
+ setIsLoading(true)
+ try {
+ const data = await getProductDetail(productId)
+ if (!data) {
+ throw new Error('해당 상품의 데이터를 찾을 수 없습니다.')
+ }
+ setProduct(data)
+ } catch (error) {
+ setError(error.message)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ fetchProduct()
+ }, [productId])
+
+ if (error) {
+ alert(`오류: ${error}`)
+ }
+
+ if (!productId || !product) return null
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ 목록으로 돌아가기
+
+
+
+ >
+ )
+}
+
+export default ItemPage
diff --git a/src/pages/ItemPage/components/CommentThread.jsx b/src/pages/ItemPage/components/CommentThread.jsx
new file mode 100644
index 00000000..5c63ca58
--- /dev/null
+++ b/src/pages/ItemPage/components/CommentThread.jsx
@@ -0,0 +1,158 @@
+import React, { useEffect, useState } from 'react'
+import { getProductComments } from '../../../api/itemApi'
+import styled from 'styled-components'
+import { ReactComponent as EmptyStateImage } from '../../../assets/images/ui/empty-comments.svg'
+import { ReactComponent as SeeMoreIcon } from '../../../assets/images/icons/ic_kebab.svg'
+import DefaultProfileImage from '../../../assets/images/ui/ic_profile.svg'
+import { LineDivider } from '../../../styles/CommonStyles'
+import { formatUpdatedAt } from '../../../utils/dateUtils'
+
+const CommentContainer = styled.div`
+ padding: 24px 0;
+ position: relative;
+`
+
+const SeeMoreButton = styled.button`
+ position: absolute;
+ right: 0;
+`
+
+const CommentContent = styled.p`
+ font-size: 16px;
+ line-height: 140%;
+ margin-bottom: 24px;
+`
+
+const AuthorProfile = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+`
+
+const UserProfileImage = styled.img`
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ object-fit: cover;
+`
+
+const Username = styled.p`
+ color: var(--gray-600);
+ font-size: 14px;
+ margin-bottom: 4px;
+`
+
+const Timestamp = styled.p`
+ color: ${({ theme }) => theme.colors.gray[400]};
+ font-size: 12px;
+`
+
+const CommentItem = ({ item }) => {
+ const authorInfo = item.writer
+ const formattedTimestamp = formatUpdatedAt(item.updatedAt)
+
+ return (
+ <>
+
+
+
+
+
+ {item.content}
+
+
+
+
+
+ {authorInfo.nickname}
+ {formattedTimestamp}
+
+
+
+
+
+ >
+ )
+}
+
+const EmptyStateContainer = styled.div`
+ margin: 24px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+`
+
+const EmptyStateText = styled.p`
+ color: ${({ theme }) => theme.colors.gray[400]};
+ font-size: 16px;
+ line-height: 24px;
+`
+
+const EmptyState = () => {
+ return (
+
+
+ 아직 문의가 없습니다.
+
+ )
+}
+
+const ThreadContainer = styled.div`
+ margin-bottom: 40px;
+`
+
+function CommentThread ({ productId }) {
+ const [comments, setComments] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ if (!productId) return
+
+ const fetchComments = async () => {
+ setIsLoading(true)
+ const params = {
+ limit: 10 // 페이지당 보여줄 댓글 개수
+ }
+
+ try {
+ const data = await getProductComments({ productId, params })
+ setComments(data.list)
+ setError(null)
+ } catch (error) {
+ console.error('Error fetching comments:', error)
+ setError('상품의 댓글을 불러오지 못했어요.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ fetchComments()
+ }, [productId])
+
+ if (isLoading) {
+ return
상품 댓글 로딩중...
+ }
+
+ if (error) {
+ return
오류: {error}
+ }
+
+ if (comments && !comments.length) {
+ return
+ } else {
+ return (
+
+ {comments.map(item => (
+
+ ))}
+
+ )
+ }
+}
+
+export default CommentThread
diff --git a/src/pages/ItemPage/components/ItemCommentSection.jsx b/src/pages/ItemPage/components/ItemCommentSection.jsx
new file mode 100644
index 00000000..f5cf80c2
--- /dev/null
+++ b/src/pages/ItemPage/components/ItemCommentSection.jsx
@@ -0,0 +1,86 @@
+import React, { useState } from 'react'
+import styled from 'styled-components'
+import { Button } from '../../../styles/CommonStyles'
+import CommentThread from './CommentThread'
+
+const COMMENT_PLACEHOLDER =
+ '개인정보를 불법으로 무단복제,훼손,유포 시 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다.'
+
+const CommentInputSection = styled.section`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+`
+
+const SectionTitle = styled.h1`
+ font-size: 16px;
+ font-weight: 600;
+`
+
+const TextArea = styled.textarea`
+ background-color: ${({ theme }) => theme.colors.gray[100]};
+ border: none;
+ border-radius: 12px;
+ padding: 16px 24px;
+ height: 104px;
+ resize: none;
+
+ &::placeholder {
+ color: ${({ theme }) => theme.colors.gray[400]};
+ font-size: 14px;
+ line-height: 24px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ font-size: 16px;
+ }
+ }
+
+ &:focus {
+ outline-color: ${({ theme }) => theme.colors.blue.primary};
+ }
+`
+
+const PostCommentButton = styled(Button)`
+ align-self: flex-end;
+ font-weight: 600;
+ font-size: 14px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ font-size: 16px;
+ }
+`
+
+function ItemCommentSection ({ productId }) {
+ const [comment, setComment] = useState('')
+
+ const handleInputChange = e => {
+ setComment(e.target.value)
+ }
+
+ const handlePostComment = () => {}
+
+ return (
+ <>
+
+ 문의하기
+
+
+
+
+ 등록
+
+
+
+
+ >
+ )
+}
+
+export default ItemCommentSection
diff --git a/src/pages/ItemPage/components/ItemProfileSection.jsx b/src/pages/ItemPage/components/ItemProfileSection.jsx
new file mode 100644
index 00000000..a7ec6e5f
--- /dev/null
+++ b/src/pages/ItemPage/components/ItemProfileSection.jsx
@@ -0,0 +1,142 @@
+import React from 'react'
+import styled from 'styled-components'
+import { LineDivider } from '../../../styles/CommonStyles'
+import TagDisplay from './TagDisplay'
+import LikeButton from './LikeButton'
+import { ReactComponent as SeeMoreIcon } from '../../../assets/images/icons/ic_kebab.svg'
+
+const SectionContainer = styled.section`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ flex-direction: row;
+ }
+
+ @media ${({ theme }) => theme.mediaQuery.desktop} {
+ gap: 24px;
+ }
+`
+
+const ItemImage = styled.div`
+ width: 100%;
+ height: 100%;
+
+ img {
+ border-radius: 12px;
+ width: 100%;
+ height: auto;
+ }
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ width: 40%;
+ max-width: 486px;
+ }
+`
+
+const ItemDetailsContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ flex: 1;
+ align-items: flex-start;
+`
+
+const MainDetails = styled.div`
+ width: 100%;
+ position: relative;
+`
+
+const SeeMoreButton = styled.button`
+ position: absolute;
+ right: 0;
+`
+
+const ItemTitle = styled.h1`
+ font-size: 16px;
+ font-weight: 600;
+ margin-bottom: 8px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ font-size: 20px;
+ margin-bottom: 12px;
+ }
+
+ @media ${({ theme }) => theme.mediaQuery.desktop} {
+ font-size: 24px;
+ margin-bottom: 16px;
+ }
+`
+
+const ItemPrice = styled.h2`
+ font-size: 24px;
+ font-weight: 600;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ font-size: 32px;
+ }
+
+ @media ${({ theme }) => theme.mediaQuery.desktop} {
+ font-size: 40px;
+ }
+`
+
+const Description = styled.p`
+ font-size: 16px;
+ line-height: 140%;
+`
+
+const SectionLabel = styled.h3`
+ color: var(--gray-600);
+ font-size: 14px;
+ font-weight: 500;
+ margin-bottom: 8px;
+`
+
+const TagDisplaySection = styled.div`
+ margin: 24px 0;
+`
+
+function ItemProfileSection ({ product }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {product.name}
+ {product.price.toLocaleString()}원
+
+
+
+
+
+ 상품 소개
+ {product.description}
+
+
+
+ 상품 태그
+
+
+
+
+
+
+
+ )
+}
+
+export default ItemProfileSection
diff --git a/src/pages/ItemPage/components/LikeButton.jsx b/src/pages/ItemPage/components/LikeButton.jsx
new file mode 100644
index 00000000..1819fe3d
--- /dev/null
+++ b/src/pages/ItemPage/components/LikeButton.jsx
@@ -0,0 +1,39 @@
+import React from 'react'
+import styled from 'styled-components'
+import { ReactComponent as HeartSvg } from '../../../assets/images/icons/ic_heart.svg'
+import { FlexContainer } from '../../../styles/CommonStyles'
+import Icon from '../../../components/UI/Icon'
+
+const PillButton = styled.button`
+ color: var(--gray-500);
+ font-size: 16px;
+ padding: 4px 12px;
+ border-radius: 999px;
+ border: 1px solid var(--gray-200);
+
+ &:hover svg path {
+ fill: var(--red);
+ stroke: var(--red);
+ }
+`
+
+const ButtonContent = styled(FlexContainer)`
+ gap: 4px;
+`
+
+function LikeButton ({ productId, isFavorite, favoriteCount }) {
+ return (
+
+
+
+ {favoriteCount.toLocaleString()}
+
+
+ )
+}
+
+export default LikeButton
diff --git a/src/pages/ItemPage/components/TagDisplay.jsx b/src/pages/ItemPage/components/TagDisplay.jsx
new file mode 100644
index 00000000..0635adf9
--- /dev/null
+++ b/src/pages/ItemPage/components/TagDisplay.jsx
@@ -0,0 +1,30 @@
+import React from 'react'
+import styled from 'styled-components'
+
+const TagsDisplaySection = styled.div`
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+`
+
+const Tag = styled.div`
+ background-color: ${({ theme }) => theme.colors.gray[50]};
+ color: ${({ theme }) => theme.colors.gray[800]};
+ padding: 6px 16px;
+ border-radius: 999px;
+ font-size: 16px;
+`
+
+function TagDisplay ({ tags }) {
+ if (!tags || tags.length === 0) return null
+
+ return (
+
+ {tags.map((tag, index) => (
+ #{tag}
+ ))}
+
+ )
+}
+
+export default TagDisplay
diff --git a/src/pages/MarketPage/MarketPage.css b/src/pages/MarketPage/MarketPage.css
index f304e4a4..557553c8 100644
--- a/src/pages/MarketPage/MarketPage.css
+++ b/src/pages/MarketPage/MarketPage.css
@@ -1,12 +1,12 @@
.sectionTitle {
- color: #111827;
+ color: var(--gray-900);
font-weight: bold;
font-size: 20px;
line-height: normal;
}
.itemCard {
- color: #1f2937;
+ color: var(--gray-800);
overflow: hidden;
cursor: pointer;
}
@@ -46,7 +46,7 @@
display: flex;
align-items: center;
gap: 4px;
- color: #4b5563;
+ color: var(--gray-600);
font-size: 12px;
}
@@ -75,20 +75,9 @@
padding-bottom: 16px;
}
-.sortButtonWrapper {
- position: relative;
-}
-
-.sortDropdownTriggerButton {
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- padding: 9px;
- margin-left: 8px;
-}
-
.searchBarWrapper {
display: flex;
- background-color: #f3f4f6;
+ background-color: var(--gray-100);
border-radius: 12px;
padding: 9px 16px;
flex: 1;
@@ -103,7 +92,7 @@
}
.searchBarInput::placeholder {
- color: #9ca3af;
+ color: var(--gray-400);
font-size: 16px;
}
diff --git a/src/pages/MarketPage/components/AllItemsSection.jsx b/src/pages/MarketPage/components/AllItemsSection.jsx
index c8d06c2c..030ba9d5 100644
--- a/src/pages/MarketPage/components/AllItemsSection.jsx
+++ b/src/pages/MarketPage/components/AllItemsSection.jsx
@@ -1,113 +1,108 @@
-import React, { useEffect, useState } from 'react'
-import { getProducts } from '../../../api/itemApi'
-import ItemCard from './ItemCard'
-import { ReactComponent as SortIcon } from '../../../assets/images/icons/ic_sort.svg'
-import { ReactComponent as SearchIcon } from '../../../assets/images/icons/ic_search.svg'
-import { Link } from 'react-router-dom'
-import DropdownList from '../../../components/UI/DropdownList'
-import PaginationBar from '../../../components/UI/PaginationBar'
+import React, { useEffect, useState } from "react";
+import { getProducts } from "../../../api/itemApi";
+import ItemCard from "./ItemCard";
+import { ReactComponent as SearchIcon } from "../../../assets/images/icons/ic_search.svg";
+import { Link } from "react-router-dom";
+import DropdownMenu from "../../../components/UI/DropdownMenu";
+import PaginationBar from "../../../components/UI/PaginationBar";
+import LoadingSpinner from "../../../components/UI/LoadingSpinner";
const getPageSize = () => {
- const width = window.innerWidth
+ const width = window.innerWidth;
if (width < 768) {
// Mobile viewport
- return 4
+ return 4;
} else if (width < 1280) {
// Tablet viewport
- return 6
+ return 6;
} else {
// Desktop viewport
- return 10
+ return 10;
}
-}
+};
-function AllItemsSection () {
- const [orderBy, setOrderBy] = useState('recent')
- const [page, setPage] = useState(1)
- const [pageSize, setPageSize] = useState(getPageSize())
- const [itemList, setItemList] = useState([])
- const [isDropdownVisible, setIsDropdownVisible] = useState(false)
- const [totalPageNum, setTotalPageNum] = useState()
+function AllItemsSection() {
+ const [orderBy, setOrderBy] = useState("recent");
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(getPageSize());
+ const [itemList, setItemList] = useState([]);
+ const [totalPageNum, setTotalPageNum] = useState();
+ const [isLoading, setIsLoading] = useState(true);
const fetchSortedData = async ({ orderBy, page, pageSize }) => {
- const products = await getProducts({ orderBy, page, pageSize })
- setItemList(products.list)
- setTotalPageNum(Math.ceil(products.totalCount / pageSize))
- }
+ setIsLoading(true);
+ try {
+ const products = await getProducts({ orderBy, page, pageSize });
+ setItemList(products.list);
+ setTotalPageNum(Math.ceil(products.totalCount / pageSize));
+ } catch (error) {
+ console.error("오류: ", error.message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
- const handleSortSelection = sortOption => {
- setOrderBy(sortOption)
- setIsDropdownVisible(false)
- }
+ const handleSortSelection = (sortOption) => {
+ setOrderBy(sortOption);
+ };
useEffect(() => {
const handleResize = () => {
- setPageSize(getPageSize())
- }
+ setPageSize(getPageSize());
+ };
- // Recalculate and set pageSize whenever the screen size changes
- window.addEventListener('resize', handleResize)
- fetchSortedData({ orderBy, page, pageSize })
+ window.addEventListener("resize", handleResize);
+ fetchSortedData({ orderBy, page, pageSize });
// Cleanup function
return () => {
- window.removeEventListener('resize', handleResize)
- }
- }, [orderBy, page, pageSize])
-
- const toggleDropdown = () => {
- setIsDropdownVisible(!isDropdownVisible)
- }
+ window.removeEventListener("resize", handleResize);
+ };
+ }, [orderBy, page, pageSize]);
- const onPageChange = pageNumber => {
- setPage(pageNumber)
- }
+ const onPageChange = (pageNumber) => {
+ setPage(pageNumber);
+ };
return (
-
-
-
판매 중인 상품
-
- 상품 등록하기
-
-
+ <>
+
-
-
-
-
+
+
+
판매 중인 상품
+
+ 상품 등록하기
+
-
-
- {isDropdownVisible && (
-
- )}
+
+
-
-
- {itemList?.map(item => (
-
- ))}
-
+
+ {itemList?.map((item) => (
+
+ ))}
+
-
-
- )
+ >
+ );
}
-export default AllItemsSection
+export default AllItemsSection;
diff --git a/src/pages/MarketPage/components/BestItemsSection.jsx b/src/pages/MarketPage/components/BestItemsSection.jsx
index d4f9e50b..89138c63 100644
--- a/src/pages/MarketPage/components/BestItemsSection.jsx
+++ b/src/pages/MarketPage/components/BestItemsSection.jsx
@@ -1,56 +1,68 @@
-import React, { useEffect, useState } from 'react'
-import ItemCard from './ItemCard'
-import { getProducts } from '../../../api/itemApi'
+import React, { useEffect, useState } from "react";
+import ItemCard from "./ItemCard";
+import { getProducts } from "../../../api/itemApi";
+import LoadingSpinner from "../../../components/UI/LoadingSpinner";
const getPageSize = () => {
- const width = window.innerWidth
+ const width = window.innerWidth;
if (width < 768) {
// Mobile viewport
- return 1
+ return 1;
} else if (width < 1280) {
// Tablet viewport
- return 2
+ return 2;
} else {
// Desktop viewport
- return 4
+ return 4;
}
-}
+};
-function BestItemsSection () {
- const [itemList, setItemList] = useState([])
- const [pageSize, setPageSize] = useState(getPageSize())
+function BestItemsSection() {
+ const [itemList, setItemList] = useState([]);
+ const [pageSize, setPageSize] = useState(getPageSize());
+ const [isLoading, setIsLoading] = useState(true);
const fetchSortedData = async ({ orderBy, pageSize }) => {
- const products = await getProducts({ orderBy, pageSize })
- setItemList(products.list)
- }
+ setIsLoading(true);
+ try {
+ const products = await getProducts({ orderBy, pageSize });
+ setItemList(products.list);
+ } catch (error) {
+ console.error("오류: ", error.message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
useEffect(() => {
const handleResize = () => {
- setPageSize(getPageSize())
- }
+ setPageSize(getPageSize());
+ };
- // Recalculate and set pageSize whenever the screen size changes
- window.addEventListener('resize', handleResize)
- fetchSortedData({ orderBy: 'favorite', pageSize })
+ window.addEventListener("resize", handleResize);
+ fetchSortedData({ orderBy: "favorite", pageSize });
// Cleanup function
return () => {
- window.removeEventListener('resize', handleResize)
- }
- }, [pageSize])
+ window.removeEventListener("resize", handleResize);
+ };
+ }, [pageSize]);
return (
-
-
베스트 상품
+ <>
+
+
+
+
베스트 상품
-
- {itemList?.map(item => (
-
- ))}
+
+ {itemList?.map((item) => (
+
+ ))}
+
-
- )
+ >
+ );
}
-export default BestItemsSection
+export default BestItemsSection;
diff --git a/src/pages/MarketPage/components/ItemCard.jsx b/src/pages/MarketPage/components/ItemCard.jsx
index 226c06ba..dbda7efe 100644
--- a/src/pages/MarketPage/components/ItemCard.jsx
+++ b/src/pages/MarketPage/components/ItemCard.jsx
@@ -1,10 +1,15 @@
import React from "react";
import { ReactComponent as HeartIcon } from "../../../assets/images/icons/ic_heart.svg";
+import { Link } from "react-router-dom";
function ItemCard({ item }) {
return (
-
-
+
+
{item.name}
{item.price.toLocaleString()}원
@@ -13,7 +18,7 @@ function ItemCard({ item }) {
{item.favoriteCount}
-
+
);
}
diff --git a/src/styles/CommonStyles.js b/src/styles/CommonStyles.js
new file mode 100644
index 00000000..ce3c02a7
--- /dev/null
+++ b/src/styles/CommonStyles.js
@@ -0,0 +1,91 @@
+import { Link } from "react-router-dom";
+import styled from "styled-components";
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ padding: 16px;
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ padding: 16px 24px;
+ }
+
+ @media ${({ theme }) => theme.mediaQuery.desktop} {
+ max-width: 1200px;
+ padding: 24px 0;
+ margin: 0 auto;
+ }
+`;
+
+export const SectionTitle = styled.h1`
+ font-size: 20px;
+ font-weight: bold;
+ color: ${({ theme }) => theme.colors.gray[800]};
+
+ @media ${({ theme }) => theme.mediaQuery.tablet} {
+ font-size: 28px;
+ }
+`;
+
+export const FlexContainer = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+`;
+
+export const Button = styled.button`
+ background-color: ${({ theme }) => theme.colors.blue.primary};
+ color: ${({ theme }) => theme.colors.white};
+ padding: 11.5px 23px;
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: bold;
+ cursor: pointer;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.blue.hover};
+ }
+
+ &:focus {
+ background-color: ${({ theme }) => theme.colors.blue.focus};
+ }
+
+ &:disabled {
+ background-color: ${({ theme }) => theme.colors.gray[400]};
+ cursor: default;
+ pointer-events: none;
+ }
+`;
+
+export const StyledLink = styled(Link)`
+ background-color: ${({ theme }) => theme.colors.blue.primary};
+ color: ${({ theme }) => theme.colors.white};
+ padding: 11.5px 23px;
+ border-radius: ${(props) => (props.$pill ? "999px" : "8px")};
+ font-size: 16px;
+ font-weight: bold;
+ cursor: pointer;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.blue.hover};
+ }
+
+ &:focus {
+ background-color: ${({ theme }) => theme.colors.blue.focus};
+ }
+
+ &:disabled {
+ background-color: ${({ theme }) => theme.colors.gray[400]};
+ cursor: default;
+ pointer-events: none;
+ }
+`;
+
+export const LineDivider = styled.hr`
+ width: 100%;
+ border: none;
+ height: 1px;
+ background-color: var(--gray-200);
+ margin: ${(props) =>
+ props.$margin || "16px 0"};
+`;
diff --git a/src/styles/GlobalStyle.js b/src/styles/GlobalStyle.js
new file mode 100644
index 00000000..dad88e56
--- /dev/null
+++ b/src/styles/GlobalStyle.js
@@ -0,0 +1,6 @@
+import { createGlobalStyle } from "styled-components";
+
+const GlobalStyle = createGlobalStyle`
+`;
+
+export default GlobalStyle;
diff --git a/src/styles/global.css b/src/styles/global.css
index 851341a1..44f5f2ec 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -1,19 +1,23 @@
/* Mobile styles */
+/* Updated color palette */
:root {
/* Gray scale */
- --gray-900: #1b1d1f;
- --gray-800: #26282b;
- --gray-600: #454c53;
- --gray-500: #72787f;
- --gray-400: #9ea4a8;
+ --gray-900: #111827;
+ --gray-800: #1F2937;
+ --gray-700: #374151;
+ --gray-600: #4b5563;
+ --gray-500: #6b7280;
+ --gray-400: #9ca3af;
--gray-200: #e5e7eb;
- --gray-100: #e8ebed;
- --gray-50: #f7f7f8;
+ --gray-100: #f3f4f6;
+ --gray-50: #f9fafb;
/* Primary color */
--blue: #3692ff;
+ --red: #f74747;
+
/* Layout dimensions */
--header-height: 70px;
}
@@ -56,7 +60,7 @@ svg {
}
body {
- color: #374151;
+ color: var(--gray-700);
word-break: keep-all;
font-family: "Pretendard", sans-serif;
}
@@ -81,8 +85,8 @@ header {
}
footer {
- background-color: #111827;
- color: #9ca3af;
+ background-color: var(--gray-900);
+ color: var(--gray-400);
font-size: 16px;
padding: 32px;
display: flex;
@@ -113,13 +117,6 @@ footer {
padding: 0 16px;
}
-h1 {
- font-size: 40px;
- font-weight: 700;
- line-height: 56px;
- letter-spacing: 0.02em;
-}
-
.button {
background-color: var(--blue);
color: #ffffff;
@@ -137,7 +134,7 @@ h1 {
}
.button:disabled {
- background-color: #9ca3af;
+ background-color: var(--gray-400);
cursor: default;
pointer-events: none;
}
diff --git a/src/styles/theme.js b/src/styles/theme.js
new file mode 100644
index 00000000..36e563d0
--- /dev/null
+++ b/src/styles/theme.js
@@ -0,0 +1,31 @@
+// Updated color palette
+const colors = {
+ gray: {
+ 900: "#111827",
+ 800: "#1F2937",
+ 700: "#374151",
+ 600: "#4b5563",
+ 500: "#6b7280",
+ 400: "#9ca3af",
+ 200: "#e5e7eb",
+ 100: "#f3f4f6",
+ 50: "#f9fafb",
+ },
+ blue: { primary: "#3692ff", hover: "#1967D6", focus: "#1251AA" },
+ red: "#f74747",
+ white: "#FFF",
+ black: "#000",
+};
+
+const mediaQuery = {
+ mobile: "screen and (max-width: 767px)",
+ tablet: "screen and (min-width: 768px)",
+ desktop: "screen and (min-width: 1280px)",
+};
+
+const theme = {
+ colors,
+ mediaQuery,
+};
+
+export default theme;
diff --git a/src/utils/dateUtils.js b/src/utils/dateUtils.js
new file mode 100644
index 00000000..a82a64dd
--- /dev/null
+++ b/src/utils/dateUtils.js
@@ -0,0 +1,40 @@
+// 날짜 관련 Util Functions
+// Using "date-fns" library
+// 참고 링크: https://date-fns.org/
+
+// 날짜 포맷팅 참고:
+// - 대문자 'M'은 month, 소문자 'm'은 minute을 뜻해요.
+// - 'MM'은 month를 두 자리 숫자로 나타낸 것, 'M'은 한자리 숫자로 나타낸 것(예: 5월을 '05'가 아닌 '5'로 출력)
+// - 대문자 'HH'는 24시 체계 (예: 14시), 소문자 'hh'는 12시 체계 (예: 2시)
+// - 소문자 'hh'를 사용해 시간을 나타낼 경우, 'a'를 추가해 AM/PM까지 함께 표기해 주세요. (예: "yyyy.MM.dd hh:mm a")
+
+import {
+ format,
+ differenceInDays,
+ differenceInHours,
+ differenceInMinutes,
+ differenceInSeconds
+} from "date-fns";
+
+export const formatUpdatedAt = dateString => {
+ const date = new Date(dateString); // 입력된 날짜 문자열을 Date 객체로 변환
+ const now = new Date(); // 현재 기준 Date 객체 생성
+
+ const diffInDays = differenceInDays(now, date); // 현재 시간과 입력된 날짜의 차이를 일(day) 단위로 계산
+ const diffInHours = differenceInHours(now, date); // 현재 시간과 입력된 날짜의 차이를 시간(hour) 단위로 계산
+ const diffInMinutes = differenceInMinutes(now, date); // 현재 시간과 입력된 날짜의 차이를 분(minute) 단위로 계산
+ const diffInSeconds = differenceInSeconds(now, date); // 현재 시간과 입력된 날짜의 차이를 초(second) 단위로 계산
+
+ if (diffInSeconds < 60) {
+ return "방금 전"; // 차이가 1분 미만인 경우 "방금 전" 형식으로 출력
+ } else if (diffInMinutes < 60) {
+ return `${diffInMinutes}분 전`; // 차이가 1시간 미만인 경우 "N분 전" 형식으로 출력
+ } else if (diffInHours < 24) {
+ return `${diffInHours}시간 전`; // 차이가 1일 미만인 경우 "N시간 전" 형식으로 출력
+ } else if (diffInDays < 7) {
+ return `${diffInDays}일 전`; // 차이가 7일 이내인 경우 "N일 전" 형식으로 출력
+ } else {
+ // 차이가 7일 이상인 경우 포맷팅된 날짜 출력
+ return format(date, "yyyy.MM.dd hh:mm a");
+ }
+};