diff --git a/.eslintrc.json b/.eslintrc.json index 67ca6a5..4c1fda1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,9 +3,7 @@ "rules": { "no-var": "error", // var 금지 "no-multiple-empty-lines": "error", // 여러 줄 공백 금지 - "no-console": ["error", { "allow": ["warn", "error", "info"] }], // console.log() 금지 "eqeqeq": "error", // 일치 연산자 사용 필수 - "dot-notation": "error", // 가능하다면 dot notation 사용 - "no-unused-vars": "error" // 사용하지 않는 변수 금지 + "dot-notation": "error" // 가능하다면 dot notation 사용 } } diff --git a/.gitignore b/.gitignore index e137dcd..dfc24bc 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -.eslintcache \ No newline at end of file +.eslintcache +.env \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js index b4d2e65..1129fe3 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,8 +1,7 @@ module.exports = { singleQuote: true, bracketSpacing: true, - bracketSameLine: true, - arrowParens: 'avoid', printWidth: 120, tabWidth: 2, + trailingComma: 'all', }; diff --git a/package-lock.json b/package-lock.json index 8ae474e..69bdd40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,26 @@ "hasInstallScript": true, "dependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@reduxjs/toolkit": "^1.9.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.4.0", + "crypto-js": "^4.1.1", "lint-staged": "^13.2.2", + "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^4.10.1", + "react-redux": "^8.1.1", + "react-redux-toolkit": "^0.0.1-alpha.2", + "react-router": "^6.14.0", + "react-router-dom": "^6.14.0", "react-scripts": "5.0.1", + "react-select": "^5.7.3", "styled-components": "^6.0.0-rc.5", + "sweetalert": "^2.1.2", + "sweetalert2": "^11.7.12", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -3046,6 +3058,60 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", @@ -3059,11 +3125,69 @@ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, + "node_modules/@emotion/react": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, "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/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "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", @@ -3157,6 +3281,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz", + "integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==" + }, + "node_modules/@floating-ui/dom": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.2.tgz", + "integrity": "sha512-VKmvHVatWnewmGGy+7Mdy4cTJX71Pli6v/Wjb5RQBuq5wjUYx+Ef+kRThi8qggZqDgD8CogCpqhRoVp3+yQk+g==", + "dependencies": { + "@floating-ui/core": "^1.3.1" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -4046,6 +4183,37 @@ } } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.0.tgz", + "integrity": "sha512-Eu1V3kz3mV0wUpVTiFHuaT8UD1gj/0VnoFHQYX35xlslQUpe8CuYoKFn9d4WZFHm3yDywz6ALZuGdnUPKrNeAw==", + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4782,6 +4950,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, "node_modules/@types/eslint": { "version": "8.40.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz", @@ -4835,6 +5008,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -5137,6 +5319,11 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "node_modules/@types/object-assign": { + "version": "4.0.30", + "resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz", + "integrity": "sha512-HhE8gFfLj321pa6OE59QmOdL5NgIOhkdYn7MWnZTOcHOms8XFzNgr9+A0/GbN0XEX9wTM58yg4YXKhGr69QIUw==" + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -5185,6 +5372,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", + "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -5260,6 +5455,11 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@types/ws": { "version": "8.5.5", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", @@ -6105,6 +6305,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "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", @@ -7259,6 +7482,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -7995,6 +8223,15 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -8323,6 +8560,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -9412,6 +9654,11 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -9827,6 +10074,14 @@ "node": ">=10" } }, + "node_modules/gitbook-plugin-github": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gitbook-plugin-github/-/gitbook-plugin-github-2.0.0.tgz", + "integrity": "sha512-TDGQgdC5vFGEqn533SDf/8AV+VagxhGIUyCZAq0EqwkyS1F73lCl7w9l8UWDd2me7Sq9L3zcr7RaJ6FjSFAo7w==", + "engines": { + "gitbook": ">=2.5.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -10091,6 +10346,32 @@ "he": "bin/he" } }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -10516,6 +10797,14 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -10594,6 +10883,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -13780,6 +14091,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", @@ -16002,6 +16318,11 @@ "asap": "~2.0.6" } }, + "node_modules/promise-polyfill": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", + "integrity": "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -16049,6 +16370,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", @@ -16336,11 +16662,237 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-icons": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", + "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-redux": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz", + "integrity": "sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux-toolkit": { + "version": "0.0.1-alpha.2", + "resolved": "https://registry.npmjs.org/react-redux-toolkit/-/react-redux-toolkit-0.0.1-alpha.2.tgz", + "integrity": "sha512-lxs8RB48nFSEm5ek7wGTIhUEjIeGTVH6F8tG16GdqcMBfJ8Puq4qu5sOpVIwtcGpBY5mvRsozzQ6WfCklrAb5w==", + "dependencies": { + "axios": "^0.18.0", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "react": "^16.0.0", + "react-redux": "^5.0.0", + "react-router": "^4.3.1", + "react-router-dom": "^4.3.1", + "redux": "^4.0.0", + "redux-observable": "^0.18.0", + "rxjs": "^5.5.6", + "shortid": "^2.2.8", + "universal-cookie": "^3.0.7" + }, + "peerDependencies": { + "react": "^15.0.0-0 || ^16.0.0-0", + "react-redux": "^4.3.0 || ^5.0.0", + "react-router": "^4.0.0", + "react-router-dom": "^4.0.0", + "redux": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/react-redux-toolkit/node_modules/axios": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", + "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", + "dependencies": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "node_modules/react-redux-toolkit/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/react-redux-toolkit/node_modules/follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dependencies": { + "debug": "=3.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/react-redux-toolkit/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/react-redux-toolkit/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/react-redux-toolkit/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/react-redux-toolkit/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-redux-toolkit/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-redux-toolkit/node_modules/react-redux": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz", + "integrity": "sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4", + "loose-envify": "^1.1.0", + "prop-types": "^15.6.1", + "react-is": "^16.6.0", + "react-lifecycles-compat": "^3.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0-0", + "redux": "^2.0.0 || ^3.0.0 || ^4.0.0-0" + } + }, + "node_modules/react-redux-toolkit/node_modules/react-router": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz", + "integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==", + "dependencies": { + "history": "^4.7.2", + "hoist-non-react-statics": "^2.5.0", + "invariant": "^2.2.4", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.1", + "warning": "^4.0.1" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-redux-toolkit/node_modules/react-router-dom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz", + "integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==", + "dependencies": { + "history": "^4.7.2", + "invariant": "^2.2.4", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.1", + "react-router": "^4.3.1", + "warning": "^4.0.1" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-redux-toolkit/node_modules/react-router/node_modules/hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + }, + "node_modules/react-redux-toolkit/node_modules/redux-observable": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/redux-observable/-/redux-observable-0.18.0.tgz", + "integrity": "sha512-tu02n6jr6/bq/vyI9E/AHxIyIl0YsWloqvWqSBG0KqN6aQBujMBP6hlDAlQLj8hP+XQpqL293MLX6V612c0jSg==", + "dependencies": { + "gitbook-plugin-github": "^2.0.0" + }, + "peerDependencies": { + "redux": ">=3 <4", + "rxjs": ">=5 <6" + } + }, + "node_modules/react-redux-toolkit/node_modules/rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", + "dependencies": { + "symbol-observable": "1.0.1" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -16349,6 +16901,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.0.tgz", + "integrity": "sha512-OD+vkrcGbvlwkspUFDgMzsu1RXwdjNh83YgG/28lBnDzgslhCgxIqoExLlxsfTpIygp7fc+Hd3esloNwzkm2xA==", + "dependencies": { + "@remix-run/router": "1.7.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.0.tgz", + "integrity": "sha512-YEwlApKwzMMMbGbhh+Q7MsloTldcwMgHxUY/1g0uA62+B1hZo2jsybCWIDCL8zvIDB1FA0pBKY9chHbZHt+2dQ==", + "dependencies": { + "@remix-run/router": "1.7.0", + "react-router": "6.14.0" + }, + "engines": { + "node": ">=14" + }, + "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", @@ -16421,6 +17003,41 @@ } } }, + "node_modules/react-select": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.3.tgz", + "integrity": "sha512-z8i3NCuFFWL3w27xq92rBkVI2onT0jzIIPe480HlBjXJ3b5o6Q+Clp4ydyeKrj9DZZ3lrjawwLC5NGl0FSvUDg==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -16605,6 +17222,22 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -16731,6 +17364,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -16778,6 +17416,11 @@ "node": ">=8" } }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, "node_modules/resolve-url-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", @@ -17342,6 +17985,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shortid": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", + "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "dependencies": { + "nanoid": "^2.1.0" + } + }, + "node_modules/shortid/node_modules/nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -18050,6 +18706,32 @@ "boolbase": "~1.0.0" } }, + "node_modules/sweetalert": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/sweetalert/-/sweetalert-2.1.2.tgz", + "integrity": "sha512-iWx7X4anRBNDa/a+AdTmvAzQtkN1+s4j/JJRWlHpYE8Qimkohs8/XnFcWeYHH2lMA8LRCa5tj2d244If3S/hzA==", + "dependencies": { + "es6-object-assign": "^1.1.0", + "promise-polyfill": "^6.0.2" + } + }, + "node_modules/sweetalert2": { + "version": "11.7.12", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.7.12.tgz", + "integrity": "sha512-TQJy8mQymJLzqWPQOMQErd81Zd/rSYr0UL4pEc7bqEihtjS+zt7LWJXLhfPp93e+Hf3Z2FHMB6QGNskAMCsdTg==", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/limonte" + } + }, + "node_modules/symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha512-Kb3PrPYz4HanVF1LVGuAdW6LoVgIwjUYJGzFe7NDrBLCN4lsV/5J0MFurV+ygS4bRVwrCEt2c7MQ1R2a72oJDw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -18276,6 +18958,16 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -18608,6 +19300,25 @@ "node": ">=8" } }, + "node_modules/universal-cookie": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-3.1.0.tgz", + "integrity": "sha512-sP6WuFgqIUro7ikgI2ndrsw9Ro+YvVBe5O9cQfWnjTicpLaSMUEUUDjQF8m8utzWF2ONl7tRkcZd7v4n6NnzjQ==", + "dependencies": { + "@types/cookie": "^0.3.1", + "@types/object-assign": "^4.0.30", + "cookie": "^0.3.1", + "object-assign": "^4.1.0" + } + }, + "node_modules/universal-cookie/node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -18684,6 +19395,27 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18753,6 +19485,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -18789,6 +19526,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -21744,6 +22489,53 @@ "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", "requires": {} }, + "@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + } + } + }, + "@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "requires": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, "@emotion/is-prop-valid": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", @@ -21757,11 +22549,59 @@ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, + "@emotion/react": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", + "requires": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, "@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, + "@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "requires": {} + }, + "@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -21824,6 +22664,19 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz", "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==" }, + "@floating-ui/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz", + "integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==" + }, + "@floating-ui/dom": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.2.tgz", + "integrity": "sha512-VKmvHVatWnewmGGy+7Mdy4cTJX71Pli6v/Wjb5RQBuq5wjUYx+Ef+kRThi8qggZqDgD8CogCpqhRoVp3+yQk+g==", + "requires": { + "@floating-ui/core": "^1.3.1" + } + }, "@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -22470,6 +23323,22 @@ "source-map": "^0.7.3" } }, + "@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "requires": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + } + }, + "@remix-run/router": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.0.tgz", + "integrity": "sha512-Eu1V3kz3mV0wUpVTiFHuaT8UD1gj/0VnoFHQYX35xlslQUpe8CuYoKFn9d4WZFHm3yDywz6ALZuGdnUPKrNeAw==" + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -22997,6 +23866,11 @@ "@types/node": "*" } }, + "@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, "@types/eslint": { "version": "8.40.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz", @@ -23050,6 +23924,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -23299,6 +24182,11 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "@types/object-assign": { + "version": "4.0.30", + "resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz", + "integrity": "sha512-HhE8gFfLj321pa6OE59QmOdL5NgIOhkdYn7MWnZTOcHOms8XFzNgr9+A0/GbN0XEX9wTM58yg4YXKhGr69QIUw==" + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -23347,6 +24235,14 @@ "@types/react": "*" } }, + "@types/react-transition-group": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", + "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -23422,6 +24318,11 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "@types/ws": { "version": "8.5.5", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", @@ -24028,6 +24929,28 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz", "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==" }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -24882,6 +25805,11 @@ "which": "^2.0.1" } }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -25396,6 +26324,15 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -25653,6 +26590,11 @@ "is-symbol": "^1.0.2" } }, + "es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -26457,6 +27399,11 @@ "pkg-dir": "^4.1.0" } }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -26731,6 +27678,11 @@ "through2": "^4.0.0" } }, + "gitbook-plugin-github": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gitbook-plugin-github/-/gitbook-plugin-github-2.0.0.tgz", + "integrity": "sha512-TDGQgdC5vFGEqn533SDf/8AV+VagxhGIUyCZAq0EqwkyS1F73lCl7w9l8UWDd2me7Sq9L3zcr7RaJ6FjSFAo7w==" + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -26916,6 +27868,34 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, "hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -27222,6 +28202,14 @@ "side-channel": "^1.0.4" } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -27276,6 +28264,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + }, "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -29573,6 +30566,11 @@ "fs-monkey": "^1.0.4" } }, + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", @@ -30968,6 +31966,11 @@ "asap": "~2.0.6" } }, + "promise-polyfill": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", + "integrity": "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==" + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -31010,6 +32013,11 @@ } } }, + "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==" + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -31217,16 +32225,207 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-icons": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", + "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", + "requires": {} + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-redux": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz", + "integrity": "sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, + "react-redux-toolkit": { + "version": "0.0.1-alpha.2", + "resolved": "https://registry.npmjs.org/react-redux-toolkit/-/react-redux-toolkit-0.0.1-alpha.2.tgz", + "integrity": "sha512-lxs8RB48nFSEm5ek7wGTIhUEjIeGTVH6F8tG16GdqcMBfJ8Puq4qu5sOpVIwtcGpBY5mvRsozzQ6WfCklrAb5w==", + "requires": { + "axios": "^0.18.0", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "react": "^16.0.0", + "react-redux": "^5.0.0", + "react-router": "^4.3.1", + "react-router-dom": "^4.3.1", + "redux": "^4.0.0", + "redux-observable": "^0.18.0", + "rxjs": "^5.5.6", + "shortid": "^2.2.8", + "universal-cookie": "^3.0.7" + }, + "dependencies": { + "axios": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + }, + "react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-redux": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz", + "integrity": "sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==", + "requires": { + "@babel/runtime": "^7.1.2", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4", + "loose-envify": "^1.1.0", + "prop-types": "^15.6.1", + "react-is": "^16.6.0", + "react-lifecycles-compat": "^3.0.0" + } + }, + "react-router": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz", + "integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==", + "requires": { + "history": "^4.7.2", + "hoist-non-react-statics": "^2.5.0", + "invariant": "^2.2.4", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.1", + "warning": "^4.0.1" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + } + } + }, + "react-router-dom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz", + "integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==", + "requires": { + "history": "^4.7.2", + "invariant": "^2.2.4", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.1", + "react-router": "^4.3.1", + "warning": "^4.0.1" + } + }, + "redux-observable": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/redux-observable/-/redux-observable-0.18.0.tgz", + "integrity": "sha512-tu02n6jr6/bq/vyI9E/AHxIyIl0YsWloqvWqSBG0KqN6aQBujMBP6hlDAlQLj8hP+XQpqL293MLX6V612c0jSg==", + "requires": { + "gitbook-plugin-github": "^2.0.0" + } + }, + "rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", + "requires": { + "symbol-observable": "1.0.1" + } + } + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" }, + "react-router": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.0.tgz", + "integrity": "sha512-OD+vkrcGbvlwkspUFDgMzsu1RXwdjNh83YgG/28lBnDzgslhCgxIqoExLlxsfTpIygp7fc+Hd3esloNwzkm2xA==", + "requires": { + "@remix-run/router": "1.7.0" + } + }, + "react-router-dom": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.0.tgz", + "integrity": "sha512-YEwlApKwzMMMbGbhh+Q7MsloTldcwMgHxUY/1g0uA62+B1hZo2jsybCWIDCL8zvIDB1FA0pBKY9chHbZHt+2dQ==", + "requires": { + "@remix-run/router": "1.7.0", + "react-router": "6.14.0" + } + }, "react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -31282,6 +32481,33 @@ "workbox-webpack-plugin": "^6.4.1" } }, + "react-select": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.3.tgz", + "integrity": "sha512-z8i3NCuFFWL3w27xq92rBkVI2onT0jzIIPe480HlBjXJ3b5o6Q+Clp4ydyeKrj9DZZ3lrjawwLC5NGl0FSvUDg==", + "requires": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + } + }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -31425,6 +32651,20 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "requires": {} + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -31526,6 +32766,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -31558,6 +32803,11 @@ "global-dirs": "^0.1.1" } }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, "resolve-url-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", @@ -31957,6 +33207,21 @@ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==" }, + "shortid": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", + "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "requires": { + "nanoid": "^2.1.0" + }, + "dependencies": { + "nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + } + } + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -32494,6 +33759,25 @@ } } }, + "sweetalert": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/sweetalert/-/sweetalert-2.1.2.tgz", + "integrity": "sha512-iWx7X4anRBNDa/a+AdTmvAzQtkN1+s4j/JJRWlHpYE8Qimkohs8/XnFcWeYHH2lMA8LRCa5tj2d244If3S/hzA==", + "requires": { + "es6-object-assign": "^1.1.0", + "promise-polyfill": "^6.0.2" + } + }, + "sweetalert2": { + "version": "11.7.12", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.7.12.tgz", + "integrity": "sha512-TQJy8mQymJLzqWPQOMQErd81Zd/rSYr0UL4pEc7bqEihtjS+zt7LWJXLhfPp93e+Hf3Z2FHMB6QGNskAMCsdTg==" + }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha512-Kb3PrPYz4HanVF1LVGuAdW6LoVgIwjUYJGzFe7NDrBLCN4lsV/5J0MFurV+ygS4bRVwrCEt2c7MQ1R2a72oJDw==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -32657,6 +33941,16 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -32896,6 +34190,24 @@ "crypto-random-string": "^2.0.0" } }, + "universal-cookie": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-3.1.0.tgz", + "integrity": "sha512-sP6WuFgqIUro7ikgI2ndrsw9Ro+YvVBe5O9cQfWnjTicpLaSMUEUUDjQF8m8utzWF2ONl7tRkcZd7v4n6NnzjQ==", + "requires": { + "@types/cookie": "^0.3.1", + "@types/object-assign": "^4.0.30", + "cookie": "^0.3.1", + "object-assign": "^4.1.0" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==" + } + } + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -32942,6 +34254,18 @@ "requires-port": "^1.0.0" } }, + "use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "requires": {} + }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -32999,6 +34323,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -33028,6 +34357,14 @@ "makeerror": "1.0.12" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 8dc1d66..ea11b99 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,26 @@ "private": true, "dependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@reduxjs/toolkit": "^1.9.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.4.0", + "crypto-js": "^4.1.1", "lint-staged": "^13.2.2", + "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^4.10.1", + "react-redux": "^8.1.1", + "react-redux-toolkit": "^0.0.1-alpha.2", + "react-router": "^6.14.0", + "react-router-dom": "^6.14.0", "react-scripts": "5.0.1", + "react-select": "^5.7.3", "styled-components": "^6.0.0-rc.5", + "sweetalert": "^2.1.2", + "sweetalert2": "^11.7.12", "web-vitals": "^2.1.4" }, "scripts": { @@ -23,6 +35,7 @@ "format": "prettier --cache --write .", "lint": "eslint --cache ." }, + "proxy": "http://localhost:8080", "eslintConfig": { "extends": [ "react-app", diff --git a/public/images/splash.png b/public/images/splash.png new file mode 100644 index 0000000..accb73a Binary files /dev/null and b/public/images/splash.png differ diff --git a/public/images/walkthrough.gif b/public/images/walkthrough.gif new file mode 100644 index 0000000..65bfd0a Binary files /dev/null and b/public/images/walkthrough.gif differ diff --git a/public/images/walkthrough2.gif b/public/images/walkthrough2.gif new file mode 100644 index 0000000..ca41a91 Binary files /dev/null and b/public/images/walkthrough2.gif differ diff --git a/src/App.js b/src/App.js index 077bec7..2e37022 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,51 @@ +import { Routes, Route } from 'react-router-dom'; import './styles/App.css'; +import 'tailwindcss/tailwind.css'; + +import Chat from './pages/ChatPage'; +import ChatList from './pages/ChatListPage'; +import Home from './pages/HomePage'; +import Register from './pages/RegisterPage'; +import SignIn from './pages/SignInPage'; +import SplashScreen from './pages/SplashScreenPage'; +import Walkthrough from './pages/WalkthroughPage'; +import RegisterComplete from './pages/RegisterCompletePage'; +import WritingPage from './pages/WritingPage'; +import PostsPage from './pages/PostsPage'; +import PermissionPage from './pages/PermissionPage'; +import RegisterLocationPage from './pages/RegisterLocationPage'; +import CategoryPage from './pages/CategoryPage'; +import SearchPage from './pages/SearchPage'; +import MyPage from './pages/MyPage'; +import EditProfilePage from './pages/EditProfilePage'; +import FavoriteCategories from './components/home/FavoriteCategories'; function App() { - return
; + return ( +
+
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ ); } export default App; diff --git a/src/api/testApi.js b/src/api/testApi.js new file mode 100644 index 0000000..31ab7d9 --- /dev/null +++ b/src/api/testApi.js @@ -0,0 +1,5 @@ +const testApi = ({ skip, limit }) => { + return fetch(`https://dummyjson.com/posts?skip=${skip}&limit=${limit}`).then((res) => res.json()); +}; + +export default testApi; diff --git a/src/components/common/Carousel.jsx b/src/components/common/Carousel.jsx new file mode 100644 index 0000000..e080756 --- /dev/null +++ b/src/components/common/Carousel.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import styled from 'styled-components'; + +const Circle = styled.div` + width: 9px; + height: 9px; + border-radius: 50%; + transition: all 0.3s; + background: ${(props) => props.background || '#39B54A'}; +`; + +function Carousel({ carouselColor }) { + return ; +} + +export default Carousel; diff --git a/src/components/common/Category.jsx b/src/components/common/Category.jsx new file mode 100644 index 0000000..a4ade47 --- /dev/null +++ b/src/components/common/Category.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; + +import { selectedCategorySlice } from '../../redux/slices/selectedCategorySlice'; + +function Category({ src, firstName, lastName = '' }) { + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const onCategorySelect = (name) => { + dispatch(selectedCategorySlice.actions.setCategory(name)); + navigate('/search'); + }; + + return ( +
onCategorySelect(lastName === '' ? firstName : `${firstName}/${lastName}`)} + > + +
{firstName}
+
{lastName}
+
+ ); +} + +export default Category; diff --git a/src/components/common/CategoryList.jsx b/src/components/common/CategoryList.jsx new file mode 100644 index 0000000..3c1187d --- /dev/null +++ b/src/components/common/CategoryList.jsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; + +import Category from './Category'; +import { TEMPORARY_SRC } from '../../static/constants'; +import aquatic from '../../images/aquatic.png'; +import bread from '../../images/bread.png'; +import eco from '../../images/eco.png'; +import fruit from '../../images/fruit.png'; +import kimchi from '../../images/kimchi.png'; +import meat from '../../images/meat.png'; +import milk from '../../images/milk.png'; +import water from '../../images/water.png'; +import noodles from '../../images/noodles.png'; +import rice from '../../images/rice.png'; +import seasoning from '../../images/seasoning.png'; +import snack from '../../images/snack.png'; +import vegetable from '../../images/vegetable.png'; + +// TODO: 하드코딩된 값 constants로 추후 변경 예정 +function CategoryList() { + return ( +
+
+ + + + + + + + + + + + + + + + + +
+
+ ); +} + +export default CategoryList; diff --git a/src/components/common/FavoriteButton.jsx b/src/components/common/FavoriteButton.jsx new file mode 100644 index 0000000..32a5726 --- /dev/null +++ b/src/components/common/FavoriteButton.jsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const FavoriteButton = () => { + const [selectedCategory, setSelectedCategory] = useState(null); + + const navigate = useNavigate(); + const handleAddFavorite = () => { + navigate('/favorite-categories'); + if (selectedCategory) { + console.log(`추가 - 선택된 카테고리: ${selectedCategory}`); + } + }; + + const handleCategorySelection = (category) => { + setSelectedCategory(category); + }; + + return ( + + ); +}; + +export default FavoriteButton; diff --git a/src/components/common/FriendProfile.jsx b/src/components/common/FriendProfile.jsx new file mode 100644 index 0000000..620fd28 --- /dev/null +++ b/src/components/common/FriendProfile.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import styled from 'styled-components'; + +const Div = styled.div` + background-color: ${(props) => props.color || '#39B54A'}; +`; + +function FriendProfile({ name = '', svg, color }) { + return ( +
+
+
+
+ {svg} +
{name}
+
+ ); +} + +export default FriendProfile; diff --git a/src/components/common/FriendsProfile.jsx b/src/components/common/FriendsProfile.jsx new file mode 100644 index 0000000..e3a7e1d --- /dev/null +++ b/src/components/common/FriendsProfile.jsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import { useSelector } from 'react-redux'; +import FriendProfile from './FriendProfile'; + +function FriendsProfile() { + let friendsList = useSelector((state) => state.friends.friendsList); + let recruteList = JSON.parse(localStorage.getItem('recruteList')); + let isJoin = JSON.parse(localStorage.getItem('isJoin')); + console.log(friendsList, recruteList); + + return ( +
+ + + + } + /> + + {!isJoin + ? friendsList.map((el, index) => ( + + )) + : recruteList.map((el, index) => ( + + ))} +
+ ); +} + +export default FriendsProfile; diff --git a/src/components/common/IdPasswordForm.jsx b/src/components/common/IdPasswordForm.jsx new file mode 100644 index 0000000..343ceef --- /dev/null +++ b/src/components/common/IdPasswordForm.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import Input from './Input'; + +const IdPasswordForm = ({ label, type, value, onChange, color, errors, readOnly }) => { + return ( +
+ +
+ + {type === 'password' && ( + + + + )} +
+ {errors.isError && ( + + {errors.message} + + )} +
+ ); +}; + +export default IdPasswordForm; diff --git a/src/components/common/ImageAndMessage.jsx b/src/components/common/ImageAndMessage.jsx new file mode 100644 index 0000000..01a9425 --- /dev/null +++ b/src/components/common/ImageAndMessage.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import styled from 'styled-components'; + +const Wrapper = styled.div` + opacity: ${(props) => props.opacity}; + margin-top: ${(props) => props.marginTop}; +`; + +const SubTitle = styled.h1` + color: ${(props) => props.color || '#00c92c'}; +`; + +function ImageAndMessage({ mainMessage, subMessage, color, opacity, src, marginTop }) { + return ( + +
+
+ + {mainMessage} + +

{subMessage}

+
+ + ); +} + +export default ImageAndMessage; diff --git a/src/components/common/Input.jsx b/src/components/common/Input.jsx new file mode 100644 index 0000000..133211a --- /dev/null +++ b/src/components/common/Input.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import styled from 'styled-components'; + +const StyledInput = styled.input` + width: ${(props) => props.width || '360px'}; + margin-bottom: ${(props) => props.mb || 0}; + border-color: ${(props) => props.color}; + background: ${(props) => (props.readOnly ? '#ccc' : '#fff')}; + + &:focus { + outline-color: ${(props) => (props.readOnly ? 'initial' : '#39b54a')}; + outline-style: ${(props) => (props.readOnly ? 'none' : 'initial')}; + } +`; + +function Input({ type, onChange, placeholder, name, value, width, mb, color, autoComplete, readOnly }) { + return ( + + ); +} +export default Input; diff --git a/src/components/common/LongButton.jsx b/src/components/common/LongButton.jsx new file mode 100644 index 0000000..9f71cc0 --- /dev/null +++ b/src/components/common/LongButton.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import styled from 'styled-components'; + +const ButtonWrapper = styled.div` + bottom: ${(props) => props.bottom || '56px'}; +`; + +const StyledButton = styled.button` + background: ${(props) => props.background || '#00c92c'}; +`; + +function LongButton({ type, contents, onClick, background, bottom, customStyle }) { + return ( + + + {contents} + + + ); +} + +export default LongButton; diff --git a/src/components/common/MyProfile.jsx b/src/components/common/MyProfile.jsx new file mode 100644 index 0000000..6922df2 --- /dev/null +++ b/src/components/common/MyProfile.jsx @@ -0,0 +1,92 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { FcCheckmark } from 'react-icons/fc'; +import { debounce } from 'lodash'; +import { + setErrors, + setNewNickname, + updateNicknameFailure, + updateNicknameStart, + updateNicknameSuccess, +} from '../../redux/slices/userInfoChangeSlice'; + +function MyProfile({ cameraSvg = '', writingSvg }) { + const [isEditing, setIsEditing] = useState(false); + const { newNickname, errors } = useSelector((state) => state.userInfoChange); + + /** TODO: 서버에서 가져온 데이터로 추후 변경 */ + const nickname = localStorage.getItem('signup-nickname'); + const inputRef = useRef(); + const dispatch = useDispatch(); + + /** 닉네임 초기값 렌더링 */ + useEffect(() => { + if (inputRef.current) { + inputRef.current.value = nickname; + } + }, [nickname]); + + /** 수정모드 토글 */ + const isUpdateMode = () => { + setIsEditing((prevState) => !prevState); + if (inputRef.current) { + inputRef.current.readOnly = !inputRef.current.readOnly; + inputRef.current.focus(); + } + }; + + /** 닉네임 변경 시도 */ + const updateNickname = useCallback(async () => { + isUpdateMode(); + + try { + dispatch(updateNicknameStart()); + await dispatch(updateNicknameSuccess({ newNickname })); + } catch (error) { + dispatch(updateNicknameFailure(error.message)); + } + }, [dispatch, newNickname]); + + /** 닉네임 유효성 검사 */ + const handleNicknameChange = useCallback( + debounce((event) => { + event.preventDefault(); + const { value } = event.target; + + const validationErrors = { ...errors }; + dispatch(setNewNickname(value)); + validationErrors.newNickname = { + message: value.trim() === '' ? '닉네임을 입력해주세요.' : '', + isError: value.trim() === '', + }; + + dispatch(setErrors(validationErrors)); + }, 300), + [dispatch], + ); + + return ( +
+ {/* */} + +
+
+ +
+ +
+
+ ); +} + +export default MyProfile; diff --git a/src/components/common/SearchedOutput.jsx b/src/components/common/SearchedOutput.jsx new file mode 100644 index 0000000..069dd0f --- /dev/null +++ b/src/components/common/SearchedOutput.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { WON } from '../../static/constants'; + +function SearchedOutput({ src, name, location, price }) { + return ( +
+ {'searchedImage'} +
+
+
{name}
+
{location}
+
+
{`${price}${WON}`}
+
+
+ ); +} + +export default SearchedOutput; diff --git a/src/components/common/SearchedOutputLists.jsx b/src/components/common/SearchedOutputLists.jsx new file mode 100644 index 0000000..8e30b87 --- /dev/null +++ b/src/components/common/SearchedOutputLists.jsx @@ -0,0 +1,82 @@ +import React, { useEffect, useRef, useState } from 'react'; + +import SearchedOutput from './SearchedOutput'; +// TODO: 임시 API로 추후 수정 예정 +import testApi from '../../api/testApi'; +import useThrottle from '../../hooks/useThrottle'; +import useCustomQuery from '../../hooks/useCustomQuery'; + +import { ERROR_ALERT_MESSAGE, TIMEOUT, TEMPORARY_SRC } from '../../static/constants'; + +function SearchedOutputList({ searchedOutputList }) { + // TODO: skip, limit 값은 변경될 수도 + const skip = 15; + const limit = 15; + + const { isLoading, data } = useCustomQuery(() => testApi({ skip, limit })); + const [newData, setNewData] = useState(searchedOutputList); + const metaRef = useRef({ fetching: false, skip: 0 }); + + useEffect(() => { + if (!isLoading && data) setNewData(data); + }, [data]); + + const onScrollDropdown = (e) => { + if (!newData) return; + if (metaRef.current.skip + limit >= newData.total) return; + + const { clientHeight, scrollHeight, scrollTop } = e.target; + if (scrollHeight * 0.8 <= clientHeight + scrollTop) getMoreData(); + }; + + const getMoreData = async () => { + try { + if (metaRef.current.fetching) return; + + const skipNumber = metaRef.current.skip + limit; + metaRef.current = { ...metaRef.current, fetching: true, skip: skipNumber }; + + const data = await testApi({ skip: skipNumber, limit: limit }); + metaRef.current = { ...metaRef.current, fetching: false }; + // TODO: data에 담겨지는 키와 값들은 추후 API 확정되면 확인 후 코드 수정 예정 + setNewData((beforeData) => ({ + ...beforeData, + ...data, + posts: [...beforeData?.posts, ...data?.posts], + skip: skipNumber, + })); + } catch (error) { + console.error(ERROR_ALERT_MESSAGE); + alert(ERROR_ALERT_MESSAGE); + } + }; + + const throttleScroll = useThrottle(onScrollDropdown, TIMEOUT); + + return ( +
+ {!isLoading ? ( + <> + {newData?.posts?.map((data, id) => + searchedOutputList.map((el) => ( + + )), + )} + + ) : ( + '' + )} +
+ ); +} + +export default SearchedOutputList; diff --git a/src/components/common/SelectBoxs.jsx b/src/components/common/SelectBoxs.jsx new file mode 100644 index 0000000..48ed6f6 --- /dev/null +++ b/src/components/common/SelectBoxs.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import styled from 'styled-components'; +import { SlArrowDown } from 'react-icons/sl'; + +const SelectBox = styled.select` + position: relative; + width: 113px; + height: 48px; + border: 1px solid #d9d9d9; + border-radius: 10px; + font-size: 14px; + padding-left: 20px; + -webkit-appearance: none; /* Safari에서 기본 화살표 숨김 */ + -moz-appearance: none; /* Firefox에서 기본 화살표 숨김 */ + appearance: none; /* 기본 화살표 숨김 */ +`; +const IconWrapper = styled.div` + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + pointer-events: none; +`; + +export const MonthSelectBox = () => { + const months = Array.from({ length: 12 }, (_, index) => { + const monthIndex = index + 1; + const monthName = new Date(0, monthIndex).toLocaleString('default', { month: 'long' }); + return { id: monthName.toUpperCase(), value: `${monthIndex}월` }; + }); + + return ( +
+ + + {months.map((month) => ( + + ))} + + + + +
+ ); +}; + +export const DaySelectBox = () => { + const days = Array.from({ length: 31 }, (_, index) => ({ id: `${index + 1}day`, value: `${index + 1}일` })); + + return ( +
+ + + {days.map((day) => ( + + ))} + + + + +
+ ); +}; + +export const GenderSelectBox = () => { + return ( +
+ + + + + + + + +
+ ); +}; diff --git a/src/components/common/ShowCase.jsx b/src/components/common/ShowCase.jsx new file mode 100644 index 0000000..658dcde --- /dev/null +++ b/src/components/common/ShowCase.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import styled from 'styled-components'; + +const Show = styled.div` + /* 스크롤바 숨기기 */ + overflow-x: auto; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: transparent transparent; + + &::-webkit-scrollbar { + width: 0.4rem; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: transparent; + } +`; + +function ShowCase({ contents }) { + return {contents}; +} +export default ShowCase; diff --git a/src/components/common/navBar/BackButton.jsx b/src/components/common/navBar/BackButton.jsx new file mode 100644 index 0000000..1759054 --- /dev/null +++ b/src/components/common/navBar/BackButton.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +function BackButton() { + const navigate = useNavigate(); + const onClick = () => { + navigate(-1); + }; + + return ( + + ); +} + +export default BackButton; diff --git a/src/components/common/navBar/ChatBar.jsx b/src/components/common/navBar/ChatBar.jsx new file mode 100644 index 0000000..41b6158 --- /dev/null +++ b/src/components/common/navBar/ChatBar.jsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; + +function ChatBar() { + const [isInputFocused, setIsInputFocused] = useState(false); + const [inputValue, setInputValue] = useState(''); + + const handleInputChange = (e) => { + const inputValue = e.target.value; + setInputValue(inputValue); + }; + + const handleInputFocus = () => { + setIsInputFocused(true); + }; + + const handleInputBlur = () => { + setIsInputFocused(false); + }; + + return ( +
+ + +
+ ); +} + +export default ChatBar; diff --git a/src/components/common/navBar/HomeBar.jsx b/src/components/common/navBar/HomeBar.jsx new file mode 100644 index 0000000..df76a7a --- /dev/null +++ b/src/components/common/navBar/HomeBar.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +const HomeBar = () => { + const navigate = useNavigate(); + + return ( +
+ {/* 로고 */} +
+ + + + + + + + + + + + + + + + + + +
+
navigate('/search')}> + {/* 검색아이콘 */} + + + +
+
+ ); +}; + +export default HomeBar; diff --git a/src/components/common/navBar/SearchBar.jsx b/src/components/common/navBar/SearchBar.jsx new file mode 100644 index 0000000..2b3661c --- /dev/null +++ b/src/components/common/navBar/SearchBar.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +import BackButton from './BackButton'; + +function SearchBar({ placeholder }) { + const navigate = useNavigate(); + + return ( +
+ + +
+ ); +} + +export default SearchBar; diff --git a/src/components/common/navBar/TabBar.jsx b/src/components/common/navBar/TabBar.jsx new file mode 100644 index 0000000..f23cdb8 --- /dev/null +++ b/src/components/common/navBar/TabBar.jsx @@ -0,0 +1,154 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; + +const TabBar = () => { + const [activeTab, setActiveTab] = useState('TabHome'); + const navigate = useNavigate(); + const location = useLocation(); + + const changeTab = (tabId) => { + let path = '/'; + + if (tabId === 'TabCategories') path = '/category'; + else if (tabId === 'TabAddPost') path = '/writing'; + else if (tabId === 'TabChat') path = '/chatlist'; + else if (tabId === 'TabMyPage') path = '/myPage'; + + navigate(path); + }; + + useEffect(() => { + const currentPath = location.pathname; + let tabId = 'TabHome'; + + if (currentPath === '/') tabId = 'TabHome'; + else if (currentPath === '/category') tabId = 'TabCategories'; + else if (currentPath === '/writing') tabId = 'TabAddPost'; + else if (currentPath === '/chatlist') tabId = 'TabChat'; + else if (currentPath === '/myPage') tabId = 'TabMyPage'; + + setActiveTab(tabId); + }, [location.pathname]); + + const Tabs = [ + { + id: 'TabHome', + title: '홈', + svg: ( + + + + ), + }, + { + id: 'TabCategories', + title: '카테고리', + svg: ( + + + + + + + + + + + + + ), + }, + { + id: 'TabAddPost', + title: '글쓰기', + svg: ( +
+ + + +
+ ), + }, + { + id: 'TabChat', + title: '채팅', + svg: ( + + + + + + + + + + + + ), + }, + { + id: 'TabMyPage', + title: '마이페이지', + svg: ( + + + + + ), + }, + ]; + + return ( +
+
    + {Tabs.map((tab) => ( +
  • changeTab(tab.id)} + > +
    {tab.svg}
    + {tab.title} +
  • + ))} +
+
+ ); +}; + +export default TabBar; diff --git a/src/components/common/navBar/TextAndBackBar.jsx b/src/components/common/navBar/TextAndBackBar.jsx new file mode 100644 index 0000000..2ccdcd2 --- /dev/null +++ b/src/components/common/navBar/TextAndBackBar.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import BackButton from './BackButton'; + +function TextAndBackBar({ title }) { + const navigate = useNavigate(); + const handleMoveBack = () => navigate(-1); + + return ( + <> +
+
+ +

+ {title} +

+
+
+ + ); +} + +export default TextAndBackBar; diff --git a/src/components/common/navBar/TextBar.jsx b/src/components/common/navBar/TextBar.jsx new file mode 100644 index 0000000..4d9b51a --- /dev/null +++ b/src/components/common/navBar/TextBar.jsx @@ -0,0 +1,11 @@ +function TextBar({ title }) { + return ( +
+

+ {title} +

+
+ ); +} + +export default TextBar; diff --git a/src/components/home/Favorite.jsx b/src/components/home/Favorite.jsx new file mode 100644 index 0000000..b5575ff --- /dev/null +++ b/src/components/home/Favorite.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import FavoriteButton from '../common/FavoriteButton'; +import Title from './Title'; + +const Favorite = () => { + return ( +
+ + <div className="flex justify-between overflow-hidden"> + <FavoriteButton /> + <FavoriteButton /> + <FavoriteButton /> + <FavoriteButton /> + <FavoriteButton /> + </div> + </div> + ); +}; + +export default Favorite; diff --git a/src/components/home/FavoriteCategories.jsx b/src/components/home/FavoriteCategories.jsx new file mode 100644 index 0000000..9506166 --- /dev/null +++ b/src/components/home/FavoriteCategories.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import CategoryList from '../common/CategoryList'; +import TextAndBackBar from '../common/navBar/TextAndBackBar'; + +const FavoriteCategories = () => { + return ( + <div> + <TextAndBackBar title={'카테고리'} /> + <CategoryList /> + </div> + ); +}; + +export default FavoriteCategories; diff --git a/src/components/home/PostList.jsx b/src/components/home/PostList.jsx new file mode 100644 index 0000000..c276b00 --- /dev/null +++ b/src/components/home/PostList.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import Title from './Title'; + +// 해당 페이지 프로젝트 기간 이후 개발예정 +const PostList = () => { + const postTitle = ['실시간 인기글', '방금 올라온 글', '나를 위한 추천']; + const posts = [ + { + id: 1, + image: '', + title: '프로틴 바/ 다크 초코씨솔트&카라멜넛, 마이프로마이프로마이프로', + address: '서울시 서초구 서초대로', + price: '20,000원', + }, + { + id: 2, + image: '', + title: '프로틴 바/ 다크 초코씨솔트&카라멜넛, 마이프로마이프로마이프로', + address: '서울시 서초구 서초대로', + price: '20,000원', + }, + { + id: 3, + image: '', + title: '프로틴 바/ 다크 초코씨솔트&카라멜넛, 마이프로마이프로마이프로', + address: '서울시 서초구 서초대로', + price: '20,000원', + }, + { + id: 4, + image: '', + title: '프로틴 바/ 다크 초코씨솔트&카라멜넛, 마이프로마이프로마이프로', + address: '서울시 서초구 서초대로', + price: '20,000원', + }, + ]; + + return ( + <div> + {postTitle.map((title) => ( + <div key={title}> + <Title title={title} /> + <ul className="flex"> + {posts.map((post, index) => ( + <li + key={post.id} + className={`w-[132px] h-[233px] mb-[25px] ${ + index === posts.length - 1 ? '' : 'mr-[20px]' + } cursor-pointer`} + > + <div className="w-[132px] h-[132px] bg-gray overflow-hidden"> + <img className="w-full" src={post.image} alt="" /> + </div> + <h4 className="w-[132px] text-[13px] line-clamp-2 font-semibold whitespace-normal overflow-hidden leading-[20px] mt-[7px]"> + {post.title} + </h4> + <p className="text-[10px] text-[#6b6b6b] overflow-hidden leading-[20px]">{post.address}</p> + <h3 className="text-[16px] font-semibold leading-[30px]">{post.price}</h3> + </li> + ))} + </ul> + </div> + ))} + </div> + ); +}; + +export default PostList; diff --git a/src/components/home/Title.jsx b/src/components/home/Title.jsx new file mode 100644 index 0000000..de56211 --- /dev/null +++ b/src/components/home/Title.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function Title({ title }) { + return <h2 className="text-[20px] font-extrabold leading-[30px] mb-[18px]">{title}</h2>; +} + +export default Title; diff --git a/src/components/myPage/MyPageCategory.jsx b/src/components/myPage/MyPageCategory.jsx new file mode 100644 index 0000000..5cc16e8 --- /dev/null +++ b/src/components/myPage/MyPageCategory.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import styled from 'styled-components'; + +const Button = styled.button` + color: ${(props) => props.color || '#6B6B6B'}; +`; + +function MyPageCategory({ name, color, onClick }) { + return ( + <div className="flex mb-[29px] ml-[15px]" onClick={onClick}> + {/* <div className="w-[30px] h-[30px] ml-[15px] mr-[26px] bg-gray"></div> */} + <Button className="text-[16px] text-medium" color={color}> + {name} + </Button> + </div> + ); +} + +export default MyPageCategory; diff --git a/src/components/permissionPage/OptionalPermission.jsx b/src/components/permissionPage/OptionalPermission.jsx new file mode 100644 index 0000000..1ee1c0a --- /dev/null +++ b/src/components/permissionPage/OptionalPermission.jsx @@ -0,0 +1,15 @@ +import React from 'react'; + +function OptionalPermission({ svg, title, description }) { + return ( + <div className="flex"> + <div className="flex items-center justify-center w-[44px] h-[44px] mr-[26px] rounded-full bg-gray">{svg}</div> + <div className="flex flex-col justify-around"> + <div className="text-[13px]">{title}</div> + <div className="text-[10px] text-deepGray">{description}</div> + </div> + </div> + ); +} + +export default OptionalPermission; diff --git a/src/components/registerLocationPage/NearLoacation.jsx b/src/components/registerLocationPage/NearLoacation.jsx new file mode 100644 index 0000000..e987784 --- /dev/null +++ b/src/components/registerLocationPage/NearLoacation.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +function NearLoacation({ location, onClick }) { + return ( + <div + className="w-full h-[42px] border-b-[0.5px] border-gray text-[13px] leading-[29px] mb-[13px] cursor-pointer" + onClick={onClick} + > + {location} + </div> + ); +} + +export default NearLoacation; diff --git a/src/components/writingPage/Input.jsx b/src/components/writingPage/Input.jsx new file mode 100644 index 0000000..ef5c19d --- /dev/null +++ b/src/components/writingPage/Input.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function Input({ placeholder, onChange }) { + return ( + <div className="mx-[15px] text-[13px] text-darkGray border-b-[0.5px] border-gray"> + <input placeholder={placeholder} onChange={onChange} className="h-[54px] w-[100%] outline-0" /> + </div> + ); +} + +export default Input; diff --git a/src/hooks/useCustomQuery.js b/src/hooks/useCustomQuery.js new file mode 100644 index 0000000..f5ccee7 --- /dev/null +++ b/src/hooks/useCustomQuery.js @@ -0,0 +1,29 @@ +import { useState, useRef } from 'react'; +import { ERROR_ALERT_MESSAGE } from '../static/constants'; + +const useCustomQuery = (query) => { + const requestRef = useRef(false); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + const [error, setError] = useState(''); + const [data, setData] = useState(); + + if (requestRef.current) return { isLoading, data }; + requestRef.current = true; + + query() + .then((data) => { + setData(data); + }) + .catch(() => { + setIsError(true); + setError(ERROR_ALERT_MESSAGE); + }) + .finally(() => { + setIsLoading(false); + }); + + return { isLoading, isError, setData, data, error }; +}; + +export default useCustomQuery; diff --git a/src/hooks/useThrottle.js b/src/hooks/useThrottle.js new file mode 100644 index 0000000..ffd3b9a --- /dev/null +++ b/src/hooks/useThrottle.js @@ -0,0 +1,16 @@ +import { useRef } from 'react'; + +const useThrottle = (callback, timeout) => { + const timer = useRef(null); + + return (...args) => { + if (!timer.current) { + timer.current = setTimeout(() => { + callback(...args); + timer.current = null; + }, timeout); + } + }; +}; + +export default useThrottle; diff --git a/src/images/aquatic.png b/src/images/aquatic.png new file mode 100755 index 0000000..d07c051 Binary files /dev/null and b/src/images/aquatic.png differ diff --git a/src/images/bread.png b/src/images/bread.png new file mode 100755 index 0000000..8b3fede Binary files /dev/null and b/src/images/bread.png differ diff --git a/src/images/eco.png b/src/images/eco.png new file mode 100755 index 0000000..eb665d2 Binary files /dev/null and b/src/images/eco.png differ diff --git a/src/images/fruit.png b/src/images/fruit.png new file mode 100755 index 0000000..5f522a9 Binary files /dev/null and b/src/images/fruit.png differ diff --git a/src/images/kimchi.png b/src/images/kimchi.png new file mode 100755 index 0000000..e6115d8 Binary files /dev/null and b/src/images/kimchi.png differ diff --git a/src/images/meat.png b/src/images/meat.png new file mode 100755 index 0000000..49a4fb7 Binary files /dev/null and b/src/images/meat.png differ diff --git a/src/images/milk.png b/src/images/milk.png new file mode 100755 index 0000000..5226357 Binary files /dev/null and b/src/images/milk.png differ diff --git a/src/images/noodles.png b/src/images/noodles.png new file mode 100755 index 0000000..76e3dda Binary files /dev/null and b/src/images/noodles.png differ diff --git a/src/images/rice.png b/src/images/rice.png new file mode 100755 index 0000000..fa01d16 Binary files /dev/null and b/src/images/rice.png differ diff --git a/src/images/seasoning.png b/src/images/seasoning.png new file mode 100755 index 0000000..63774e1 Binary files /dev/null and b/src/images/seasoning.png differ diff --git a/src/images/snack.png b/src/images/snack.png new file mode 100755 index 0000000..5de4308 Binary files /dev/null and b/src/images/snack.png differ diff --git a/src/images/vegetable.png b/src/images/vegetable.png new file mode 100755 index 0000000..abe0dec Binary files /dev/null and b/src/images/vegetable.png differ diff --git a/src/images/water.png b/src/images/water.png new file mode 100755 index 0000000..0e770f2 Binary files /dev/null and b/src/images/water.png differ diff --git a/src/index.js b/src/index.js index 593edf1..a085fe7 100644 --- a/src/index.js +++ b/src/index.js @@ -2,9 +2,15 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; +import { BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { store } from './redux/store'; + const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - <React.StrictMode> - <App /> - </React.StrictMode> + <BrowserRouter> + <Provider store={store}> + <App /> + </Provider> + </BrowserRouter>, ); diff --git a/src/pages/CategoryListPage.jsx b/src/pages/CategoryListPage.jsx new file mode 100644 index 0000000..968c8ad --- /dev/null +++ b/src/pages/CategoryListPage.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import CategoryList from '../components/common/CategoryList'; +import LongButton from '../components/common/LongButton'; +import TextAndBackBar from '../components/common/navBar/TextAndBackBar'; + +import { CATEGORY, SELECT } from '../static/constants'; + +function CategoryListPage() { + return ( + <div> + <TextAndBackBar title={CATEGORY} /> + <CategoryList /> + {/* TODO: 원하는 카테고리 선택 후 버튼 클릭 했을 때 즐겨찾기에 추가 */} + <LongButton contents={SELECT} /> + </div> + ); +} + +export default CategoryListPage; diff --git a/src/pages/CategoryPage.jsx b/src/pages/CategoryPage.jsx new file mode 100644 index 0000000..6fc0fd2 --- /dev/null +++ b/src/pages/CategoryPage.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import SearchBar from '../components/common/navBar/SearchBar'; +import CategoryList from '../components/common/CategoryList'; +import TabBar from '../components/common/navBar/TabBar'; + +import { ENTER_INPUT } from '../static/constants'; + +function CategoryPage() { + return ( + <div> + <SearchBar placeholder={ENTER_INPUT} placeholderColor={'white'} /> + <div className="h-[39px] mx-[15px] mt-[20px] border-b-[0.5px] border-deepGray text-[13px]">카테고리</div> + <CategoryList /> + <TabBar /> + </div> + ); +} + +export default CategoryPage; diff --git a/src/pages/ChatListPage.jsx b/src/pages/ChatListPage.jsx new file mode 100644 index 0000000..65750a9 --- /dev/null +++ b/src/pages/ChatListPage.jsx @@ -0,0 +1,181 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import TabBar from '../components/common/navBar/TabBar'; +import TextBar from '../components/common/navBar/TextBar'; +import ShowCase from '../components/common/ShowCase'; + +const ChatListPage = () => { + const chatData = [ + { + id: 1, + photo: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + message: 'TODO님, 근처에서 다양한 물품들이 매일 올', + timestamp: '2023년06월25일 12:20', + unread: 10, + }, + { + id: 2, + photo: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + message: 'TODO님, 근처에서 다양한 물품들이 매일 올', + timestamp: '2023년06월25일 12:20', + unread: 10, + }, + { + id: 3, + photo: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + message: 'TODO님, 근처에서 다양한 물품들이 매일 올', + timestamp: '2023년06월25일 12:20', + unread: 10, + }, + { + id: 4, + photo: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + message: 'TODO님, 근처에서 다양한 물품들이 매일 올', + timestamp: '2023년06월25일 12:20', + unread: 10, + }, + { + id: 5, + photo: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + message: 'TODO님, 근처에서 다양한 물품들이 매일 올', + timestamp: '2023년06월25일 12:20', + unread: 10, + }, + { + id: 6, + photo: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + message: 'TODO님, 근처에서 다양한 물품들이 매일 올', + timestamp: '2023년06월25일 12:20', + unread: 10, + }, + { + id: 7, + photo: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + message: 'TODO님, 근처에서 다양한 물품들이 매일 올', + timestamp: '2023년06월25일 12:20', + unread: 10, + }, + { + id: 8, + photo: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + message: 'TODO님, 근처에서 다양한 물품들이 매일 올', + timestamp: '2023년06월25일 12:20', + unread: 10, + }, + { + id: 9, + photo: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + message: 'TODO님, 근처에서 다양한 물품들이 매일 올', + timestamp: '2023년06월25일 12:20', + unread: 10, + }, + { + id: 10, + photo: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + message: 'TODO님, 근처에서 다양한 물품들이 매일 올', + timestamp: '2023년06월25일 12:20', + unread: 10, + }, + ]; + const lastChat = chatData[chatData.length - 1]; // 마지막 채팅 데이터 가져오기 + const lastChatTime = new Date(lastChat.timestamp); // 채팅 시간을 Date 객체로 변환 + const formattedTime = lastChatTime.toLocaleTimeString('ko-KR', { hour: 'numeric', minute: 'numeric' }); // 원하는 형식으로 시간 변환 + + const navigate = useNavigate(); + const handleOpenChat = () => { + navigate('/chat'); + }; + + return ( + <> + <TextBar title={'채팅'} /> + <ShowCase + contents={ + <div className=""> + {/* 채팅리스트가 없을 때 */} + {!chatData && ( + <div className="absolute top-1/2 left-1/2 translate-x-[-50%] translate-y-[-50%]"> + <svg + className="m-auto" + width="78" + height="78" + viewBox="0 0 78 78" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M10.7772 64.6003C9.70112 64.6003 8.66895 64.1737 7.90684 63.414C7.14474 62.6544 6.71487 61.6236 6.71143 60.5475V54.9705L6.72118 21.125C6.72118 18.9701 7.5772 16.9035 9.10093 15.3798C10.6247 13.856 12.6913 13 14.8462 13H63.1802C64.2472 13 65.3037 13.2102 66.2895 13.6185C67.2752 14.0268 68.1709 14.6253 68.9254 15.3798C69.6799 16.1342 70.2784 17.0299 70.6867 18.0157C71.095 19.0015 71.3052 20.058 71.3052 21.125V48.4575C71.3052 49.5245 71.095 50.581 70.6867 51.5668C70.2784 52.5526 69.6799 53.4483 68.9254 54.2027C68.1709 54.9572 67.2752 55.5557 66.2895 55.964C65.3037 56.3723 64.2472 56.5825 63.1802 56.5825H22.4837C21.8437 56.5809 21.2097 56.7061 20.6183 56.951C20.027 57.1959 19.4901 57.5556 19.0387 58.0093L13.6502 63.4075C13.2736 63.7859 12.8258 64.0861 12.3328 64.2908C11.8397 64.4954 11.311 64.6006 10.7772 64.6003ZM14.8462 16.2565C13.5532 16.2565 12.3133 16.7701 11.399 17.6844C10.4848 18.5986 9.97118 19.8386 9.97118 21.1315L9.94518 54.9672V60.5378C9.94553 60.6984 9.99349 60.8553 10.083 60.9887C10.1725 61.1221 10.2995 61.2259 10.448 61.2871C10.5966 61.3484 10.7599 61.3642 10.9174 61.3326C11.0749 61.301 11.2195 61.2235 11.3329 61.1098L16.7409 55.705C17.4932 54.9484 18.3882 54.3485 19.3739 53.9401C20.3597 53.5318 21.4167 53.323 22.4837 53.326H63.1802C64.4731 53.326 65.7131 52.8124 66.6273 51.8981C67.5416 50.9839 68.0552 49.7439 68.0552 48.451V21.1315C68.0552 19.8386 67.5416 18.5986 66.6273 17.6844C65.7131 16.7701 64.4731 16.2565 63.1802 16.2565H14.8462Z" + fill="#9D9D9D" + /> + <path + d="M13 61.1L10.4 62.4L7.80005 61.1V19.5L13 15.6H58.5001H66.3001L68.9001 18.2L70.2001 46.8L67.6001 53.3L61.1001 54.6H20.8L13 61.1Z" + fill="#9D9D9D" + /> + <rect x="22" y="33" width="5" height="5" rx="2.5" fill="white" /> + <rect x="37" y="33" width="5" height="5" rx="2.5" fill="white" /> + <rect x="52" y="33" width="5" height="5" rx="2.5" fill="white" /> + </svg> + <p className="text-[20px] text-deepGray text-center">채팅 내역이 없습니다</p> + </div> + )} + {/* 채팅리스트가 있을 때 */} + <div> + <ul className="mt-[25px]"> + {chatData.map((chat) => ( + <li key={chat.id} className="w-full cursor-pointer" onClick={handleOpenChat}> + <div className="relative flex h-[60px]"> + <div className="w-[44px] h-[44px] rounded-full bg-gray mr-[12px]"> + <img src={chat.photo} alt="Product" className="w-full" /> + </div> + <div> + <div className="flex leading-[17px] "> + <h3 className="w-[150px] mr-[10px] mb-[5px] text-[13px] font-semibold text-ellipsis line-clamp-1 whitespace-normal overflow-hidden"> + {chat.title} + </h3> + <span className="text-[13px] font-semibold text-gray">{chat.people}</span> + </div> + <p className="w-[200px] text-[12px] leading-[25px] text-deepGray text-ellipsis line-clamp-1 whitespace-normal overflow-hidden"> + {chat.message} + </p> + </div> + <div className="absolute top-0 right-[14px] w-[56px] flex flex-wrap justify-end"> + <p className="text-[10px] text-gray leading-[15px] text-right mb-[10px]">{formattedTime}</p> + <div className="w-[20px] h-[20] rounded-full bg-[#EE0707] text-center text-white text-[13px]"> + <p>{chat.unread}</p> + </div> + </div> + </div> + <div className="w-full h-[1px] mb-[20px] bg-gray"></div> + </li> + ))} + </ul> + </div> + </div> + } + /> + <TabBar /> + </> + ); +}; + +export default ChatListPage; diff --git a/src/pages/ChatPage.jsx b/src/pages/ChatPage.jsx new file mode 100644 index 0000000..a1f1156 --- /dev/null +++ b/src/pages/ChatPage.jsx @@ -0,0 +1,167 @@ +import React from 'react'; +import FriendsProfile from '../components/common/FriendsProfile'; +import ChatBar from '../components/common/navBar/ChatBar'; +import TextAndBackBar from '../components/common/navBar/TextAndBackBar'; +import ShowCase from '../components/common/ShowCase'; + +const chatData = [ + { + id: 1, + name: '동네친구', + profile: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + postTimestamp: '2023년 06월 25일 오전 12:20', + people: 5, + unread: 10, + me: true, + messages: [ + { + message: 'TODO님, 근처에서 다양한 물품들이 매일 올라오고 있어요', + timestamp: '2023년 06월 25일 오전 12:20', + }, + { + message: '확인해보세요', + timestamp: '2023년 06월 25일 오전 12:21', + }, + ], + }, + { + id: 2, + name: '친구2', + profile: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + unread: 10, + me: false, + messages: [ + { + message: 'TODO님, 근처에서 다양한 물품들이 매일 올라오고 있어요', + timestamp: '2023년 06월 25일 오전 12:20', + }, + { + message: '확인해보세요', + timestamp: '2023년 06월 25일 오전 12:21', + }, + ], + }, + { + id: 3, + name: '친구3', + profile: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + unread: 10, + me: false, + messages: [ + { + message: 'TODO님, 근처에서 다양한 물품들이 매일 올라오고 있어요', + timestamp: '2023년 06월 25일 오전 12:20', + }, + { + message: '확인해보세요', + timestamp: '2023년 06월 25일 오전 12:21', + }, + ], + }, + { + id: 4, + name: '친구1', + profile: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + unread: 10, + me: false, + messages: [ + { + message: 'TODO님, 근처에서 다양한 물품들이 매일 올라오고 있어요', + timestamp: '2023년 06월 25일 오전 12:20', + }, + { + message: '확인해보세요', + timestamp: '2023년 06월 25일 오전 12:21', + }, + ], + }, + { + id: 5, + name: '친구4', + profile: '', + title: '프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트 프로틴 바/ 다크 초코씨솔트', + people: 5, + unread: 10, + me: false, + messages: [ + { + message: 'TODO님, 근처에서 다양한 물품들이 매일 올라오고 있어요', + timestamp: '2023년 06월 25일 오전 12:20', + }, + { + message: '확인해보세요', + timestamp: '2023년 06월 25일 오전 12:21', + }, + ], + }, +]; + +const ChatPage = () => { + return ( + <> + <TextAndBackBar title={chatData[0].title} /> + <ShowCase + contents={ + <> + <div className="sticky top-0 bg-white bg-opacity-90"> + <h3 className="pt-[23px] pb-[4px] text-[16px] leading-[20px] z-10">파티구성원</h3> + <FriendsProfile /> + </div> + <div className="w-[96px] h-[20px] mt-[20px] mb-[25px] mx-auto bg-gray rounded-[37px]"> + <p className="text-deepGray text-[10px] leading-[20px] text-center"> + {chatData[0].postTimestamp.substring(0, 13)} + </p> + </div> + <div className="flex flex-column flex-wrap w-full"> + {chatData.map((chat) => ( + <div key={chat.id} className={`flex ${chat.me ? 'flex-row-reverse w-full' : ''}`}> + {!chat.me && ( + <div className="w-[45px] h-[45px] rounded-full bg-deepGray overflow-hidden"> + <img src={chat.profile} alt="profile" className="w-full h-full" /> + </div> + )} + <div className="ml-[12px]"> + {!chat.me && <p className="text-[13px] leading-[20px] mb-[8px]">{chat.name}</p>} + {chat.messages.map((msg, index) => ( + <div + key={index} + className={`flex items-end mb-[10px] ${chat.me ? 'flex-row-reverse' : 'flex-row'}`} + > + <div + key={index} + className={` + ${chat.me ? 'bg-deepGray rounded-tl-[20px]' : 'bg-mainColor rounded-tr-[20px]'} + max-w-[208px] w-fit text-white text-[13px] leading-[20px] py-[7px] px-[10px] rounded-b-[20px]`} + > + <p>{msg.message}</p> + </div> + {/* 추후 메세지 보낸 시간이 1분 지나면 보여지도록 설정 */} + <div + className={`text-[10px] text-[#6b6b6b] ${ + chat.me ? 'text-right mr-[10px]' : 'text-left ml-[10px]' + }`} + > + {msg.timestamp.substring(14, 22)} + </div> + </div> + ))} + </div> + </div> + ))} + </div> + </> + } + /> + <ChatBar /> + </> + ); +}; + +export default ChatPage; diff --git a/src/pages/EditProfilePage.jsx b/src/pages/EditProfilePage.jsx new file mode 100644 index 0000000..c95fe3f --- /dev/null +++ b/src/pages/EditProfilePage.jsx @@ -0,0 +1,128 @@ +import React, { useCallback } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { debounce } from 'lodash'; + +import MyProfile from '../components/common/MyProfile'; +import LongButton from '../components/common/LongButton'; +import TextAndBackBar from '../components/common/navBar/TextAndBackBar'; +import IdPasswordForm from '../components/common/IdPasswordForm'; + +import { EDIT, CHANGE_INFO } from '../static/constants'; +import { + resetFields, + setErrors, + setNewPassword, + setNewPasswordCheck, + updatePasswordFailure, + updatePasswordStart, + updatePasswordSuccess, +} from '../redux/slices/userInfoChangeSlice'; + +function EditProfilePage() { + const inputFields = [ + { id: 'email', label: '아이디', type: 'email' }, + { id: 'newPassword', label: '비밀번호', type: 'password' }, + { id: 'newPasswordCheck', label: '비밀번호 재확인', type: 'password' }, + ]; + const { newPassword, newPasswordCheck, errors } = useSelector((state) => state.userInfoChange); + + const navigate = useNavigate(); + const dispatch = useDispatch(); + + // TODO: 서버에서 가져온 데이터로 추후 변경 + const email = localStorage.getItem('signup-email'); + + // 유효성 검사 + const validateField = useCallback( + debounce((name, value) => { + const validationErrors = { ...errors }; + + if (name === 'newPassword') { + dispatch(setNewPassword(value)); + validationErrors.newPassword = { + message: + value.trim() === '' + ? '' + : value.length < 8 || value.length > 16 + ? '8~16자리의 비밀번호를 입력해주세요.' + : '', + isError: value.trim() === '' || value.length < 8 || value.length > 16, + }; + } else if (name === 'newPasswordCheck') { + dispatch(setNewPasswordCheck(value)); + validationErrors.newPasswordCheck = { + message: value.trim() === '' ? '' : value !== newPassword ? '비밀번호가 틀렸습니다. 다시 입력해주세요.' : '', + isError: value.trim() === '' || value !== newPassword, + }; + } + + dispatch(setErrors(validationErrors)); + }, 300), + [errors, newPassword, newPasswordCheck, dispatch], + ); + + /** 비밀번호 변경 */ + const updatePassword = () => { + return async (dispatch) => { + try { + dispatch(updatePasswordStart()); + await dispatch(updatePasswordSuccess({ newPassword })); + alert('비밀번호 변경이 완료되었습니다.'); + dispatch(resetFields()); + navigate('/myPage'); + } catch (error) { + dispatch(updatePasswordFailure(error.message)); + } + }; + }; + + /** 유효성검사 확인 후 폼제출 */ + const handleSubmit = (event) => { + event.preventDefault(); + const validationErrors = {}; + + Object.keys(errors).forEach((key) => { + if (validationErrors[key] === undefined) { + validationErrors[key] = errors[key]; + } + }); + dispatch(setErrors(validationErrors)); + + const isFormValid = Object.values(validationErrors).every((error) => !error.isError); + + if (isFormValid) { + dispatch(updatePassword()); + console.log('클릭'); + } + }; + + return ( + <div> + <TextAndBackBar title={CHANGE_INFO} /> + <MyProfile /> + + <form className="flex flex-wrap justify-center"> + <div className="flex flex-wrap w-[360px]"> + {inputFields.slice(0, 3).map((field) => ( + <React.Fragment key={field.id}> + <IdPasswordForm + key={field.id} + label={field.label} + type={field.type} + value={field.id === 'email' ? email : null} + color={errors[field.id] && errors[field.id].isError ? '#ff0000' : '#d9d9d9'} + onChange={(event) => validateField(field.id, event.target.value)} + errors={errors[field.id] && errors[field.id].isError ? errors[field.id] : ''} + readOnly={field.id === 'email' ? 'readOnly' : ''} + /> + </React.Fragment> + ))} + </div> + </form> + <LongButton contents={EDIT} onClick={handleSubmit} /> + </div> + ); +} + +export default EditProfilePage; diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx new file mode 100644 index 0000000..dc4d127 --- /dev/null +++ b/src/pages/HomePage.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import HomeBar from '../components/common/navBar/HomeBar'; +import TabBar from '../components/common/navBar/TabBar'; +import ShowCase from '../components/common/ShowCase'; +import Favorite from '../components/home/Favorite'; +import PostList from '../components/home/PostList'; + +const HomePage = () => { + return ( + <> + <HomeBar /> + <ShowCase + contents={ + <div> + <Favorite /> + <PostList /> + </div> + } + /> + <TabBar /> + </> + ); +}; + +export default HomePage; diff --git a/src/pages/MyPage.jsx b/src/pages/MyPage.jsx new file mode 100644 index 0000000..0d58d95 --- /dev/null +++ b/src/pages/MyPage.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import MyPageCategory from '../components/myPage/MyPageCategory'; +import MyProfile from '../components/common/MyProfile'; +import TabBar from '../components/common/navBar/TabBar'; + +import { MY_PAGE, SETTING_LOCATION, CHANGE_INFO, LOGOUT } from '../static/constants'; +import { logoutFailure, logoutStart, logoutSuccess } from '../redux/slices/authSlice'; +import { getUserInfoFailure, getUserInfoStart, getUserInfoSuccess } from '../redux/slices/myPageSlice'; + +function MyPage() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const username = localStorage.getItem('username'); + + /** 내정보 변경 */ + const moveToEditProfile = () => { + try { + dispatch(getUserInfoStart()); + dispatch(getUserInfoSuccess(username)); + navigate('/editProfile'); + } catch (error) { + dispatch(getUserInfoFailure()); + } + }; + + /** 로그아웃 시도 */ + const handleLogout = async () => { + try { + dispatch(logoutStart()); + dispatch(logoutSuccess()); + navigate('/'); + } catch (error) { + console.log('로그아웃 실패'); + dispatch(logoutFailure('로그아웃에 실패했습니다.')); + } + }; + + return ( + <div className="mt-[47px]"> + <div className="flex flex-col justify-center items-center"> + <div className="text-[16px] font-medium">{MY_PAGE}</div> + <MyProfile + // cameraSvg={ + // <svg + // className="absolute top-[100px] right-[142px]" + // width="31" + // height="31" + // viewBox="0 0 31 31" + // fill="none" + // xmlns="http://www.w3.org/2000/svg" + // > + // <rect width="31" height="31" rx="15.5" fill="black" fillOpacity="0.6" /> + // <path + // d="M9.5 11.3333H11.75L13.25 10H17.75L19.25 11.3333H21.5C21.8978 11.3333 22.2794 11.4738 22.5607 11.7239C22.842 11.9739 23 12.313 23 12.6667V20.6667C23 21.0203 22.842 21.3594 22.5607 21.6095C22.2794 21.8595 21.8978 22 21.5 22H9.5C9.10218 22 8.72064 21.8595 8.43934 21.6095C8.15804 21.3594 8 21.0203 8 20.6667V12.6667C8 12.313 8.15804 11.9739 8.43934 11.7239C8.72064 11.4738 9.10218 11.3333 9.5 11.3333ZM15.5 13.3333C14.5054 13.3333 13.5516 13.6845 12.8483 14.3096C12.1451 14.9348 11.75 15.7826 11.75 16.6667C11.75 17.5507 12.1451 18.3986 12.8483 19.0237C13.5516 19.6488 14.5054 20 15.5 20C16.4946 20 17.4484 19.6488 18.1517 19.0237C18.8549 18.3986 19.25 17.5507 19.25 16.6667C19.25 15.7826 18.8549 14.9348 18.1517 14.3096C17.4484 13.6845 16.4946 13.3333 15.5 13.3333ZM15.5 14.6667C16.0967 14.6667 16.669 14.8774 17.091 15.2525C17.5129 15.6275 17.75 16.1362 17.75 16.6667C17.75 17.1971 17.5129 17.7058 17.091 18.0809C16.669 18.456 16.0967 18.6667 15.5 18.6667C14.9033 18.6667 14.331 18.456 13.909 18.0809C13.4871 17.7058 13.25 17.1971 13.25 16.6667C13.25 16.1362 13.4871 15.6275 13.909 15.2525C14.331 14.8774 14.9033 14.6667 15.5 14.6667Z" + // fill="white" + // /> + // </svg> + // } + writingSvg={ + <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + d="M14.1677 0.732422L12.2771 2.62305L17.3552 7.70117L19.2458 5.81055C20.2224 4.83398 20.2224 3.25195 19.2458 2.27539L17.7068 0.732422C16.7302 -0.244141 15.1482 -0.244141 14.1716 0.732422H14.1677ZM11.3943 3.50586L2.28879 12.6152C1.88254 13.0215 1.58567 13.5254 1.4216 14.0762L0.0387918 18.7754C-0.0588645 19.1074 0.0309793 19.4629 0.273167 19.7051C0.515354 19.9473 0.870823 20.0371 1.19895 19.9434L5.89817 18.5605C6.44895 18.3965 6.95285 18.0996 7.3591 17.6934L16.4724 8.58398L11.3943 3.50586Z" + fill="#9D9D9D" + /> + </svg> + } + /> + </div> + + <div> + <MyPageCategory name={SETTING_LOCATION} /> + <MyPageCategory name={CHANGE_INFO} onClick={moveToEditProfile} /> + <MyPageCategory name={LOGOUT} color={'#EE0707'} onClick={handleLogout} /> + </div> + + <TabBar /> + </div> + ); +} + +export default MyPage; diff --git a/src/pages/PermissionPage.jsx b/src/pages/PermissionPage.jsx new file mode 100644 index 0000000..59a025d --- /dev/null +++ b/src/pages/PermissionPage.jsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import OptionalPermission from '../components/permissionPage/OptionalPermission'; +import LongButton from '../components/common/LongButton'; + +import { + PERMISSION_FIRST_TITLE, + PERMISSION_SECOND_TITLE, + SELECT_PERMISSION, + NOTIFICATION, + NOTIFICATION_DESCRIPTION, + LOCATION, + LOCATION_DESCRIPTION, + CAMERA, + CAMERA_DESCRIPTION, + MIKE, + MIKE_DESCRIPTION, + STORAGE, + STORAGE_DESCRIPTION, + PERMISSION_MESSAGE, + CONFIRM, +} from '../static/constants'; + +function PermissionPage() { + const navigate = useNavigate(); + const handleConfirm = () => { + navigate('/register-location'); + }; + + return ( + <div className="pt-[136px] relative"> + <div className="h-[50px] pl-[16px] text-[16px] font-semibold"> + <div> + {PERMISSION_FIRST_TITLE} + <br /> + {PERMISSION_SECOND_TITLE} + </div> + </div> + + <div className="flex items-center h-[26px] mt-[26px] mb-[14px] pl-[15px] text-deepGray text-[10px]"> + {SELECT_PERMISSION} + </div> + + <div className="flex flex-col h-[280px] pl-[15px] justify-between"> + <OptionalPermission + svg={ + <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + d="M21.6601 17.1816C21.1181 16.248 20.3125 13.6064 20.3125 10.1562C20.3125 8.08425 19.4894 6.09711 18.0242 4.63198C16.5591 3.16685 14.572 2.34375 12.5 2.34375C10.428 2.34375 8.44082 3.16685 6.9757 4.63198C5.51057 6.09711 4.68747 8.08425 4.68747 10.1562C4.68747 13.6074 3.88083 16.248 3.33883 17.1816C3.20043 17.419 3.12705 17.6886 3.12611 17.9634C3.12516 18.2381 3.19669 18.5083 3.33346 18.7466C3.47024 18.9849 3.66743 19.1829 3.90515 19.3206C4.14287 19.4584 4.41271 19.5311 4.68747 19.5312H8.67282C8.85306 20.4132 9.33241 21.2059 10.0298 21.7752C10.7272 22.3444 11.5997 22.6554 12.5 22.6554C13.4002 22.6554 14.2728 22.3444 14.9701 21.7752C15.6675 21.2059 16.1469 20.4132 16.3271 19.5312H20.3125C20.5871 19.5309 20.8569 19.4581 21.0944 19.3203C21.332 19.1824 21.5291 18.9844 21.6657 18.7462C21.8024 18.5079 21.8738 18.2378 21.8729 17.9631C21.8719 17.6885 21.7985 17.4189 21.6601 17.1816ZM12.5 21.0938C12.0154 21.0936 11.5428 20.9433 11.1472 20.6635C10.7516 20.3836 10.4525 19.9881 10.291 19.5312H14.709C14.5474 19.9881 14.2483 20.3836 13.8527 20.6635C13.4571 20.9433 12.9845 21.0936 12.5 21.0938ZM4.68747 17.9688C5.43942 16.6758 6.24997 13.6797 6.24997 10.1562C6.24997 8.49865 6.90845 6.90894 8.08055 5.73683C9.25265 4.56473 10.8424 3.90625 12.5 3.90625C14.1576 3.90625 15.7473 4.56473 16.9194 5.73683C18.0915 6.90894 18.75 8.49865 18.75 10.1562C18.75 13.6768 19.5586 16.6729 20.3125 17.9688H4.68747Z" + fill="white" + /> + </svg> + } + title={NOTIFICATION} + description={NOTIFICATION_DESCRIPTION} + /> + <OptionalPermission + svg={ + <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M7.552 10.4167C7.552 9.1044 8.0733 7.84588 9.00121 6.91796C9.92913 5.99005 11.1876 5.46875 12.4999 5.46875C13.8122 5.46875 15.0707 5.99005 15.9986 6.91796C16.9265 7.84588 17.4478 9.1044 17.4478 10.4167C17.4478 11.7289 16.9265 12.9875 15.9986 13.9154C15.0707 14.8433 13.8122 15.3646 12.4999 15.3646C11.1876 15.3646 9.92913 14.8433 9.00121 13.9154C8.0733 12.9875 7.552 11.7289 7.552 10.4167ZM12.4999 7.03125C11.6021 7.03125 10.741 7.38793 10.1061 8.02282C9.47118 8.6577 9.1145 9.5188 9.1145 10.4167C9.1145 11.3145 9.47118 12.1756 10.1061 12.8105C10.741 13.4454 11.6021 13.8021 12.4999 13.8021C13.3978 13.8021 14.2589 13.4454 14.8938 12.8105C15.5287 12.1756 15.8853 11.3145 15.8853 10.4167C15.8853 9.5188 15.5287 8.6577 14.8938 8.02282C14.2589 7.38793 13.3978 7.03125 12.4999 7.03125Z" + fill="white" + /> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M3.67078 9.22605C3.84883 7.06646 4.83247 5.0528 6.42642 3.58487C8.02036 2.11694 10.108 1.30208 12.2749 1.30209H12.7249C14.8919 1.30208 16.9795 2.11694 18.5735 3.58487C20.1674 5.0528 21.1511 7.06646 21.3291 9.22605C21.5272 11.6315 20.7841 14.0201 19.2562 15.8885L14.2635 21.9938C14.0498 22.255 13.7808 22.4656 13.4758 22.6102C13.1708 22.7547 12.8375 22.8297 12.4999 22.8297C12.1624 22.8297 11.8291 22.7547 11.5241 22.6102C11.2191 22.4656 10.9501 22.255 10.7364 21.9938L5.74369 15.8885C4.21569 14.0201 3.47253 11.6316 3.67078 9.22605ZM12.2749 2.86459C10.5004 2.86485 8.79092 3.53232 7.4857 4.7345C6.18047 5.93668 5.37498 7.58568 5.22911 9.35417C5.06411 11.3561 5.68253 13.3439 6.95411 14.899L11.9468 21.0052C12.0139 21.0874 12.0985 21.1536 12.1943 21.199C12.2902 21.2445 12.3949 21.268 12.501 21.268C12.6071 21.268 12.7118 21.2445 12.8077 21.199C12.9035 21.1536 12.988 21.0874 13.0552 21.0052L18.0479 14.899C19.3187 13.3436 19.9364 11.3558 19.7708 9.35417C19.6249 7.5855 18.8193 5.93635 17.5138 4.73415C16.2084 3.53194 14.4986 2.86459 12.7239 2.86459H12.2739H12.2749Z" + fill="white" + /> + </svg> + } + title={LOCATION} + description={LOCATION_DESCRIPTION} + /> + <OptionalPermission + svg={ + <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + d="M20.3125 5.46875H17.6055L16.2744 3.47266C16.2031 3.36577 16.1065 3.27813 15.9933 3.21749C15.88 3.15684 15.7535 3.12508 15.625 3.125H9.375C9.24651 3.12508 9.12003 3.15684 9.00675 3.21749C8.89347 3.27813 8.7969 3.36577 8.72559 3.47266L7.39355 5.46875H4.6875C4.0659 5.46875 3.46976 5.71568 3.03022 6.15522C2.59068 6.59476 2.34375 7.1909 2.34375 7.8125V18.75C2.34375 19.3716 2.59068 19.9677 3.03022 20.4073C3.46976 20.8468 4.0659 21.0938 4.6875 21.0938H20.3125C20.9341 21.0938 21.5302 20.8468 21.9698 20.4073C22.4093 19.9677 22.6562 19.3716 22.6562 18.75V7.8125C22.6562 7.1909 22.4093 6.59476 21.9698 6.15522C21.5302 5.71568 20.9341 5.46875 20.3125 5.46875ZM21.0938 18.75C21.0938 18.9572 21.0114 19.1559 20.8649 19.3024C20.7184 19.4489 20.5197 19.5312 20.3125 19.5312H4.6875C4.4803 19.5312 4.28159 19.4489 4.13507 19.3024C3.98856 19.1559 3.90625 18.9572 3.90625 18.75V7.8125C3.90625 7.6053 3.98856 7.40659 4.13507 7.26007C4.28159 7.11356 4.4803 7.03125 4.6875 7.03125H7.8125C7.94115 7.03133 8.06784 6.99964 8.1813 6.93899C8.29476 6.87835 8.39149 6.79061 8.46289 6.68359L9.79297 4.6875H15.2061L16.5371 6.68359C16.6085 6.79061 16.7052 6.87835 16.8187 6.93899C16.9322 6.99964 17.0588 7.03133 17.1875 7.03125H20.3125C20.5197 7.03125 20.7184 7.11356 20.8649 7.26007C21.0114 7.40659 21.0938 7.6053 21.0938 7.8125V18.75ZM12.5 8.59375C11.6502 8.59375 10.8194 8.84576 10.1128 9.3179C9.40617 9.79005 8.85543 10.4611 8.53021 11.2463C8.20499 12.0314 8.11989 12.8954 8.28569 13.7289C8.45148 14.5624 8.86072 15.328 9.46165 15.929C10.0626 16.5299 10.8282 16.9391 11.6617 17.1049C12.4952 17.2707 13.3592 17.1856 14.1443 16.8604C14.9295 16.5352 15.6006 15.9845 16.0727 15.2778C16.5449 14.5712 16.7969 13.7405 16.7969 12.8906C16.7956 11.7514 16.3425 10.6592 15.5369 9.8537C14.7314 9.04816 13.6392 8.59504 12.5 8.59375ZM12.5 15.625C11.9592 15.625 11.4305 15.4646 10.9809 15.1642C10.5312 14.8637 10.1807 14.4367 9.97377 13.937C9.76681 13.4374 9.71266 12.8876 9.81816 12.3572C9.92367 11.8268 10.1841 11.3395 10.5665 10.9571C10.9489 10.5747 11.4361 10.3143 11.9665 10.2088C12.497 10.1033 13.0468 10.1574 13.5464 10.3644C14.046 10.5714 14.4731 10.9218 14.7735 11.3715C15.074 11.8212 15.2344 12.3498 15.2344 12.8906C15.2344 13.6158 14.9463 14.3113 14.4335 14.8241C13.9207 15.3369 13.2252 15.625 12.5 15.625Z" + fill="white" + /> + </svg> + } + title={CAMERA} + description={CAMERA_DESCRIPTION} + /> + <OptionalPermission + svg={ + <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + d="M5.46875 10.1562C5.67595 10.1562 5.87466 10.2386 6.02118 10.3851C6.16769 10.5316 6.25 10.7303 6.25 10.9375V12.5C6.25 14.1576 6.90848 15.7473 8.08058 16.9194C9.25268 18.0915 10.8424 18.75 12.5 18.75C14.1576 18.75 15.7473 18.0915 16.9194 16.9194C18.0915 15.7473 18.75 14.1576 18.75 12.5V10.9375C18.75 10.7303 18.8323 10.5316 18.9788 10.3851C19.1253 10.2386 19.324 10.1562 19.5312 10.1562C19.7385 10.1562 19.9372 10.2386 20.0837 10.3851C20.2302 10.5316 20.3125 10.7303 20.3125 10.9375V12.5C20.3125 14.4368 19.5931 16.3045 18.2939 17.7408C16.9947 19.1772 15.2083 20.0798 13.2812 20.2734V23.4375H17.9688C18.176 23.4375 18.3747 23.5198 18.5212 23.6663C18.6677 23.8128 18.75 24.0115 18.75 24.2188C18.75 24.426 18.6677 24.6247 18.5212 24.7712C18.3747 24.9177 18.176 25 17.9688 25H7.03125C6.82405 25 6.62534 24.9177 6.47882 24.7712C6.33231 24.6247 6.25 24.426 6.25 24.2188C6.25 24.0115 6.33231 23.8128 6.47882 23.6663C6.62534 23.5198 6.82405 23.4375 7.03125 23.4375H11.7188V20.2734C9.79169 20.0798 8.00527 19.1772 6.70606 17.7408C5.40685 16.3045 4.68748 14.4368 4.6875 12.5V10.9375C4.6875 10.7303 4.76981 10.5316 4.91632 10.3851C5.06284 10.2386 5.26155 10.1562 5.46875 10.1562Z" + fill="white" + /> + <path + d="M15.625 12.5C15.625 13.3288 15.2958 14.1237 14.7097 14.7097C14.1237 15.2958 13.3288 15.625 12.5 15.625C11.6712 15.625 10.8763 15.2958 10.2903 14.7097C9.70424 14.1237 9.375 13.3288 9.375 12.5V4.6875C9.375 3.8587 9.70424 3.06384 10.2903 2.47779C10.8763 1.89174 11.6712 1.5625 12.5 1.5625C13.3288 1.5625 14.1237 1.89174 14.7097 2.47779C15.2958 3.06384 15.625 3.8587 15.625 4.6875V12.5ZM12.5 0C11.2568 0 10.0645 0.49386 9.18544 1.37294C8.30636 2.25201 7.8125 3.4443 7.8125 4.6875V12.5C7.8125 13.7432 8.30636 14.9355 9.18544 15.8146C10.0645 16.6936 11.2568 17.1875 12.5 17.1875C13.7432 17.1875 14.9355 16.6936 15.8146 15.8146C16.6936 14.9355 17.1875 13.7432 17.1875 12.5V4.6875C17.1875 3.4443 16.6936 2.25201 15.8146 1.37294C14.9355 0.49386 13.7432 0 12.5 0Z" + fill="white" + /> + </svg> + } + title={MIKE} + description={MIKE_DESCRIPTION} + /> + <OptionalPermission + svg={ + <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + d="M19.3441 2.91332C19.1932 2.76243 19.014 2.64275 18.8168 2.56111C18.6197 2.47947 18.4083 2.43747 18.1949 2.4375H4.78461C4.47636 2.43743 4.17113 2.4981 3.88633 2.61603C3.60154 2.73396 3.34277 2.90684 3.1248 3.1248C2.90684 3.34277 2.73396 3.60154 2.61603 3.88633C2.4981 4.17113 2.43743 4.47636 2.4375 4.78461V21.2154C2.43743 21.5236 2.4981 21.8289 2.61603 22.1137C2.73396 22.3985 2.90684 22.6572 3.1248 22.8752C3.34277 23.0932 3.60154 23.266 3.88633 23.384C4.17113 23.5019 4.47636 23.5626 4.78461 23.5625H21.2154C21.8373 23.5606 22.4332 23.3127 22.873 22.873C23.3127 22.4332 23.5606 21.8373 23.5625 21.2154V7.80508C23.5625 7.59167 23.5205 7.38034 23.4389 7.18316C23.3572 6.98598 23.2376 6.80682 23.0867 6.6559L19.3441 2.91332ZM13 21.125C12.3572 21.125 11.7289 20.9344 11.1944 20.5773C10.6599 20.2202 10.2434 19.7126 9.99739 19.1187C9.75141 18.5249 9.68705 17.8714 9.81245 17.241C9.93785 16.6105 10.2474 16.0314 10.7019 15.5769C11.1564 15.1224 11.7355 14.8128 12.366 14.6874C12.9964 14.562 13.6499 14.6264 14.2437 14.8724C14.8376 15.1184 15.3452 15.5349 15.7023 16.0694C16.0594 16.6039 16.25 17.2322 16.25 17.875C16.2505 18.3019 16.1668 18.7248 16.0037 19.1194C15.8406 19.5139 15.6012 19.8724 15.2993 20.1743C14.9974 20.4762 14.6389 20.7156 14.2444 20.8787C13.8498 21.0418 13.4269 21.1255 13 21.125ZM15.4375 9.75H5.6875C5.47201 9.75 5.26535 9.6644 5.11298 9.51202C4.9606 9.35965 4.875 9.15299 4.875 8.9375V5.6875C4.875 5.47201 4.9606 5.26535 5.11298 5.11298C5.26535 4.9606 5.47201 4.875 5.6875 4.875H15.4375C15.653 4.875 15.8597 4.9606 16.012 5.11298C16.1644 5.26535 16.25 5.47201 16.25 5.6875V8.9375C16.25 9.15299 16.1644 9.35965 16.012 9.51202C15.8597 9.6644 15.653 9.75 15.4375 9.75Z" + stroke="white" + strokeWidth="1.5" + strokeLinecap="round" + strokeLinejoin="round" + /> + </svg> + } + title={STORAGE} + description={STORAGE_DESCRIPTION} + /> + </div> + + <div className="flex items-center w-[360px] h-[50px] mt-[26px] ml-[15px] px-[27px] rounded-[10px] bg-lightGray text-[10px] text-deepGray"> + {PERMISSION_MESSAGE} + </div> + + <LongButton className="start-button" contents={CONFIRM} onClick={handleConfirm} /> + </div> + ); +} + +export default PermissionPage; diff --git a/src/pages/PostsPage.jsx b/src/pages/PostsPage.jsx new file mode 100644 index 0000000..1ec8cff --- /dev/null +++ b/src/pages/PostsPage.jsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Swal from 'sweetalert2'; + +import BackButton from '../components/common/navBar/BackButton'; +import FriendsProfile from '../components/common/FriendsProfile'; +import { friendsSlice } from '../redux/slices/friendsSlice'; + +import { JOIN_ALERT, CONFIRM, CANCEL, SUM, WON, DIVISION, ACTUAL_PAYMENT_AMOUNT, JOIN } from '../static/constants'; + +function PostsPage() { + const dispatch = useDispatch(); + + const imageUrl = JSON.parse(localStorage.getItem('imageUrl')); + const title = JSON.parse(localStorage.getItem('title')); + const category = JSON.parse(localStorage.getItem('category')); + const totalAmount = JSON.parse(localStorage.getItem('totalAmount')); + const maxPeople = JSON.parse(localStorage.getItem('maxPeople')); + const textarea = JSON.parse(localStorage.getItem('textarea')); + const divisionAmount = (totalAmount / (maxPeople + 1)).toLocaleString(); + + let friendsList = useSelector((state) => state.friends.friendsList); + friendsList = friendsList.map((el, idx) => (idx < maxPeople ? (el = true) : (el = false))); + let recruteList = useSelector((state) => state.friends.recruteList); + recruteList = friendsList.map((el) => (el ? (el = '모집대기중') : '')); + + useEffect(() => { + dispatch(friendsSlice.actions.setFriendsList(friendsList)); + dispatch(friendsSlice.actions.setRecruteList(recruteList)); + }, []); + + const joinAsMember = () => { + Swal.fire({ + text: JOIN_ALERT, + showCancelButton: true, + confirmButtonColor: '#39B54A', + cancelButtonColor: '#CCCCCC', + confirmButtonText: CONFIRM, + cancelButtonText: CANCEL, + reverseButtons: true, + }).then((result) => { + if (result.isConfirmed) { + localStorage.setItem('isJoin', true); + localStorage.setItem('recruteList', JSON.stringify(recruteList)); + + const index = recruteList.findIndex((item) => item === '모집대기중'); + + if (index !== -1) { + recruteList[index] = '파티원'; + localStorage.setItem('recruteList', JSON.stringify(recruteList)); + + let test = JSON.parse(localStorage.getItem('recruteList')); + dispatch(friendsSlice.actions.setRecruteList(test)); + } + } + }); + }; + + let isJoin = JSON.parse(localStorage.getItem('isJoin')); + + return ( + <div className=""> + <div className="w-[100%] mt-[47px]"> + <BackButton /> + </div> + + <div className="flex justify-center"> + <img + alt="" + src={imageUrl} + className="flex w-[360px] h-[238px] mt-[11px] bg-gray rounded-[15px] cursor-pointer" + /> + </div> + + <div className="overflow-scroll h-[400px]"> + <div className="mx-[15px]"> + <FriendsProfile /> + </div> + <div className="mt-[15px] mx-[15px] mb-[3px] text-[16px] font-semibold">{title}</div> + <div className="mx-[15px] text-[10px] text-smokeGray">{category}</div> + <div className="pt-[34px] mb-[26px] mx-[15px] text-[13px]">{textarea}</div> + + <div className="w-[360px] h-[200px] p-[14px] mx-[15px] mb-[93px] rounded-[10px] bg-hexGray"> + <div className="w-[332px] h-[113px] p-[13px] rounded-[10px] bg-white font-medium text-[13px]"> + <div className="flex justify-between pb-[13px] border-b-[0.5px] border-gray font-medium"> + <div>{SUM}</div> + <div> + <div>{`${totalAmount.toLocaleString()}${WON}`}</div> + <div className="float-right">{DIVISION}</div> + </div> + </div> + + <div className="flex justify-between items-center h-[47px] font-bold text-[16px] text-mainColor"> + <div>{ACTUAL_PAYMENT_AMOUNT}</div> + <div>{`${divisionAmount}${WON}`}</div> + </div> + </div> + + <div className="flex justify-between px-[13px] pt-[10px] text-[13px] font-medium"> + <div>{`내 1/${maxPeople + 1} 부담금`}</div> + <div>{`${divisionAmount}${WON}`}</div> + </div> + + <div className="flex justify-between px-[13px] pt-[10px] text-[13px] font-medium"> + <div>{`파티원 ${maxPeople}명의 몫`}</div> + <div>{`${(totalAmount - totalAmount / maxPeople).toLocaleString()}${WON}`}</div> + </div> + </div> + </div> + + <div className="flex justify-between w-[100%] h-[107px] px-[15px] pt-[18px] fixed bottom-0 border-t-[0.2px] border-gray bg-white"> + <div className="flex flex-col h-[50px]"> + <div className="text-[10px] text-smokeGray">{`${SUM} ${totalAmount.toLocaleString()}${WON}`}</div> + <div className="font-bold text-[16px] text-mainColor">{`${ACTUAL_PAYMENT_AMOUNT} ${divisionAmount}${WON}`}</div> + </div> + <button + onClick={joinAsMember} + className={`w-[133px] h-[36px] rounded-[5px] text-white text-[13px] ${ + isJoin ? 'bg-smokeGray' : 'bg-mainColor' + }`} + disabled={isJoin} + > + {JOIN} + </button> + </div> + </div> + ); +} + +export default PostsPage; diff --git a/src/pages/RegisterCompletePage.jsx b/src/pages/RegisterCompletePage.jsx new file mode 100644 index 0000000..e1d8163 --- /dev/null +++ b/src/pages/RegisterCompletePage.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import ImageAndMessage from '../components/common/ImageAndMessage'; +import LongButton from '../components/common/LongButton'; +import TextBar from '../components/common/navBar/TextAndBackBar'; + +const RegisterCompletePage = () => { + const navigate = useNavigate(); + + const handleMoveSignIn = () => { + navigate(`/signin`); + localStorage.removeItem('signup-nickname'); + localStorage.removeItem('signup-password'); + localStorage.removeItem('registeredLocation'); + }; + const nickname = localStorage.getItem('signup-nickname'); + + return ( + <> + <TextBar title={'회원가입'} /> + <ImageAndMessage + marginTop={'26px'} + color={'#39B54A'} + src={process.env.PUBLIC_URL + '/images/walkthrough.gif'} + mainMessage={'회원가입 완료'} + subMessage={ + <> + {/* data 받아와서 user.name 넣어주기 */}'{nickname}' 님의 회원가입이 + <br /> + 성공적으로 완료되었습니다. + </> + } + /> + <LongButton className="start-button" contents={'로그인 바로가기'} onClick={handleMoveSignIn} /> + </> + ); +}; +export default RegisterCompletePage; diff --git a/src/pages/RegisterLocationCompletePage.jsx b/src/pages/RegisterLocationCompletePage.jsx new file mode 100644 index 0000000..e212bf5 --- /dev/null +++ b/src/pages/RegisterLocationCompletePage.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +import TextAndBackBar from '../components/common/navBar/TextAndBackBar'; +import LongButton from '../components/common/LongButton'; +import ImageAndMessage from '../components/common/ImageAndMessage'; + +import { MOVE_TO_HOME, REGISTER_LOCATION_COMPLETE } from '../static/constants'; + +const RegisterLocationCompletePage = () => { + const navigate = useNavigate(); + const moveToHomePage = () => navigate('/'); + + return ( + <div className="relative w-[390px] overflow-hidden"> + <TextAndBackBar title={'위치등록'} /> + <div className="mt-[-21px]"> + <ImageAndMessage mainMessage={REGISTER_LOCATION_COMPLETE} color={'#39B54A'} /> + </div> + + <LongButton type={'button'} contents={MOVE_TO_HOME} onClick={moveToHomePage} /> + </div> + ); +}; + +export default RegisterLocationCompletePage; diff --git a/src/pages/RegisterLocationPage.jsx b/src/pages/RegisterLocationPage.jsx new file mode 100644 index 0000000..46a70c1 --- /dev/null +++ b/src/pages/RegisterLocationPage.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import CryptoJS from 'crypto-js'; + +import NearLoacation from '../components/registerLocationPage/NearLoacation'; +import SearchBar from '../components/common/navBar/SearchBar'; +import ShowCase from '../components/common/ShowCase'; + +import { SEARCH_LOCATION, NEAR_LOCATION } from '../static/constants'; + +function RegisterLocationPage() { + // TODO: 추후 받아온 데이터를 사용할 예정 + const locationList = [ + '서울특별시 서초구 1', + '서울특별시 서초구 2', + '서울특별시 서초구 3', + '서울특별시 서초구 4', + '서울특별시 서초구 5', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + '서울특별시 서초구 역삼동', + ]; + const REACT_APP_SECRET_KEY = process.env.REACT_APP_SECRET_KEY; + + const navigate = useNavigate(); + const onClickLocation = (location) => { + const encrypt = CryptoJS.AES.encrypt(location, REACT_APP_SECRET_KEY).toString(); + localStorage.setItem('registeredLocation', encrypt); + navigate('/register-complete'); + }; + + return ( + <div className="flex flex-col"> + <SearchBar placeholder={SEARCH_LOCATION} /> + + <div className="flex items-center h-[29px] my-[20px] ml-[15px] font-semibold text-[12px]">{NEAR_LOCATION}</div> + <ShowCase + className="flex flex-col items-center gap-[13px]" + contents={ + locationList && + locationList.map((el, id) => <NearLoacation key={id} location={el} onClick={() => onClickLocation(el)} />) + } + /> + </div> + ); +} + +export default RegisterLocationPage; diff --git a/src/pages/RegisterPage.jsx b/src/pages/RegisterPage.jsx new file mode 100644 index 0000000..faf4e43 --- /dev/null +++ b/src/pages/RegisterPage.jsx @@ -0,0 +1,192 @@ +import React, { useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import { debounce } from 'lodash'; +import TextAndBackBar from '../components/common/navBar/TextAndBackBar'; +import LongButton from '../components/common/LongButton'; +import IdPasswordForm from '../components/common/IdPasswordForm'; +import { signupFailure, signupStart, signupSuccess } from '../redux/slices/authSlice'; +import { + setEmail, + setPassword, + setPasswordCheck, + setNickname, + setErrors, + resetFields, +} from '../redux/slices/registerSlice'; +import { checkEmail } from '../redux/api/authApi'; + +const RegisterPage = () => { + const inputFields = [ + { id: 'email', label: '아이디', type: 'email' }, + { id: 'password', label: '비밀번호', type: 'password' }, + { id: 'passwordCheck', label: '비밀번호 재확인', type: 'password' }, + { id: 'nickname', label: '닉네임', type: 'text' }, + ]; + const nicknameField = inputFields.find((field) => field.id === 'nickname'); + + const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; + const existedEmail = 'test@test.com'; + + const { email, password, passwordCheck, nickname, errors } = useSelector((state) => state.register); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + // 회원가입 API 호출 + const callSaveUserInfo = () => { + try { + dispatch(signupStart()); + dispatch(signupSuccess({ email, password, nickname })); + dispatch(resetFields()); + navigate(`/permission`); + } catch (error) { + dispatch(signupFailure(error.message)); + } + }; + + // 사용자 이메일 체크 + const handleCheckEmail = async (value) => { + try { + const result = await checkEmail(value); + return result === null; + } catch (error) { + console.error('사용자 이메일 체크 오류 :', error); + return false; + } + }; + + // 유효성 검사 후 상태 업데이트 + const validateField = useCallback( + debounce(async (name, value) => { + const validationErrors = { ...errors }; + console.log(name, value); + if (name === 'email') { + dispatch(setEmail(value)); + const isValidEmail = emailRegex.test(value); + + if (!value.trim()) { + validationErrors.email = { message: '아이디를 입력해주세요.', isError: true }; + } else if (!isValidEmail) { + validationErrors.email = { message: '유효한 이메일 형식이 아닙니다.', isError: true }; + } else { + const isAvailable = await handleCheckEmail(value); + if (!isAvailable) { + validationErrors.email = { message: '이미 사용중인 아이디입니다.', isError: true }; + } else { + validationErrors.email = { message: '', isError: false }; + } + } + } else if (name === 'password') { + dispatch(setPassword(value)); + validationErrors.password = { + message: + value.trim() === '' + ? '8~16자리의 비밀번호를 입력해주세요.' + : value.length < 8 || value.length > 16 + ? '비밀번호는 8~16자리여야 합니다.' + : '', + isError: value.trim() === '' || value.length < 8 || value.length > 16, + }; + } else if (name === 'passwordCheck') { + dispatch(setPasswordCheck(value)); + validationErrors.passwordCheck = { + message: + value.trim() === '' + ? '확인을 위하여 위와 동일하게 입력해주세요.' + : value !== password + ? '비밀번호가 틀렸습니다. 다시 입력해주세요.' + : '', + isError: value.trim() === '' || value !== password, + }; + } else if (name === 'nickname') { + dispatch(setNickname(value)); + validationErrors.nickname = { + message: value.trim() === '' ? '닉네임을 입력해주세요.' : '', + isError: value.trim() === '', + }; + } + + dispatch(setErrors(validationErrors)); + }, 300), + [errors, emailRegex, existedEmail, email, password, passwordCheck, nickname], + ); + + // 유효성검사 확인 후 폼제출 + const handleSubmit = (event) => { + event.preventDefault(); + const validationErrors = {}; + + inputFields.forEach((field) => { + if (field.id === 'email' && email.trim() === '') { + validationErrors[field.id] = { message: `${field.label}를 입력해주세요.`, isError: true }; + } else if (field.id === 'password' && password.trim() === '') { + validationErrors[field.id] = { message: `8~16자리의 ${field.label}를 입력해주세요.`, isError: true }; + } else if (field.id === 'passwordCheck' && passwordCheck.trim() === '') { + validationErrors[field.id] = { message: `확인을 위하여 위와 동일하게 입력해주세요.`, isError: true }; + } else if (field.id === 'nickname' && nickname.trim() === '') { + validationErrors[field.id] = { message: '닉네임을 입력해주세요.', isError: true }; + } + }); + + Object.keys(errors).forEach((key) => { + if (validationErrors[key] === undefined) { + validationErrors[key] = errors[key]; + } + }); + + const isFormValid = Object.values(validationErrors).every((error) => !error.isError); + + dispatch(setErrors(validationErrors)); + + if (isFormValid) { + callSaveUserInfo(); + } + }; + + // 에러메세지 초기값 보여주기 + useEffect(() => { + const validationErrors = {}; + + inputFields.forEach((field) => { + validationErrors[field.id] = errors[field.id]; + }); + + dispatch(setErrors(validationErrors)); + }, []); + + return ( + <> + <TextAndBackBar title={'회원가입'} /> + <form className="mt-[25px] flex flex-wrap justify-center"> + <div className="flex flex-wrap w-[360px]"> + {/* 아이디, 비밀번호, 비밀번호 재확인 */} + {inputFields.slice(0, 3).map((field) => ( + <IdPasswordForm + key={field.id} + label={field.label} + type={field.type} + color={errors[field.id].isError ? '#ff0000' : '#d9d9d9'} + onChange={(event) => validateField(field.id, event.target.value)} + errors={errors[field.id]} + /> + ))} + {/* 닉네임 */} + {nicknameField && ( + <IdPasswordForm + key={nicknameField.id} + autoComplete="off" + label={nicknameField.label} + type={nicknameField.type} + color={errors.nickname.isError ? '#ff0000' : '#d9d9d9'} + onChange={(event) => validateField(nicknameField.id, event.target.value)} + errors={errors.nickname} + /> + )} + </div> + </form> + <LongButton contents={'가입하기'} onClick={handleSubmit} /> + </> + ); +}; + +export default RegisterPage; diff --git a/src/pages/SearchPage.jsx b/src/pages/SearchPage.jsx new file mode 100644 index 0000000..4acb27e --- /dev/null +++ b/src/pages/SearchPage.jsx @@ -0,0 +1,168 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import Select from 'react-select'; + +import SearchBar from '../components/common/navBar/SearchBar'; +import SearchedOutputList from '../components/common/SearchedOutputLists'; +import TabBar from '../components/common/navBar/TabBar'; + +import { ENTER_INPUT } from '../static/constants'; + +// TODO: 하드코딩되어 있는 값들 모두 수정 필요 +function SearchPage() { + const options = [ + { value: '낮은 가격순', label: '낮은 가격순' }, + { value: '높은 가격순', label: '높은 가격순' }, + ]; + const searchedOutputList = [ + { + id: 1, + name: '전체', + location: '서울시 서초구 서초대로', + price: '20000', + category: '전체', + }, + { + id: 2, + name: '과일', + location: '서울시 서초구 서초대로', + price: '20000', + category: '과일', + }, + { + id: 3, + name: '채소', + location: '서울시 서초구 서초대로', + price: '20000', + category: '채소', + }, + { + id: 4, + name: '쌀/잡곡/견과', + location: '서울시 서초구 서초대로', + price: '20000', + category: '쌀/잡곡/견과', + }, + { + id: 5, + name: '정육/계란류', + location: '서울시 서초구 서초대로', + price: '20000', + category: '정육/계란류', + }, + { + id: 6, + name: '수산물/건해산', + location: '서울시 서초구 서초대로', + price: '20000', + category: '수산물/건해산', + }, + { + id: 7, + name: '우유/유제품', + location: '서울시 서초구 서초대로', + price: '20000', + category: '우유/유제품', + }, + { + id: 8, + name: '김치/반찬/델리', + location: '서울시 서초구 서초대로', + price: '20000', + category: '김치/반찬/델리', + }, + { + id: 100, + name: '김치/반찬/델리', + location: '서울시 서초구 서초대로', + price: '15000', + category: '김치/반찬/델리', + }, + { + id: 9, + name: '생수/음료/주류', + location: '서울시 서초구 서초대로', + price: '20000', + category: '생수/음료/주류', + }, + { + id: 10, + name: '커피/차/원두', + location: '서울시 서초구 서초대로', + price: '20000', + category: '커피/차/원두', + }, + { + id: 11, + name: '면류/통조림', + location: '서울시 서초구 서초대로', + price: '20000', + category: '면류/통조림', + }, + { + id: 12, + name: '양념/오일', + location: '서울시 서초구 서초대로', + price: '20000', + category: '양념/오일', + }, + { + id: 13, + name: '과자/간식', + location: '서울시 서초구 서초대로', + price: '20000', + category: '과자/간식', + }, + { + id: 14, + name: '베이커리/잼', + location: '서울시 서초구 서초대로', + price: '20000', + category: '베이커리/잼', + }, + { + id: 15, + name: '친환경/유기농', + location: '서울시 서초구 서초대로', + price: '20000', + category: '친환경/유기농', + }, + ]; + + const [selectedOption, setSelectedOption] = useState(options[0]); + const selectedCategory = useSelector((state) => state.selectedCategory.category); + const onChangeSelector = (selectedOption) => setSelectedOption(selectedOption.value); + + useEffect(() => { + setSelectedOption(options[0].value); + }, []); + + return ( + <div className="flex flex-col"> + <SearchBar placeholder={ENTER_INPUT} placeholderColor={'white'} /> + + <div className="flex mx-[15px] mt-[15px] mb-[25px] justify-between items-center"> + <div className="flex w-[152px] h-[38px] items-center justify-center rounded-[5px] bg-orange text-white text-[15px]"> + {selectedCategory} + </div> + <Select defaultValue={selectedOption} options={options} onChange={onChangeSelector} /> + </div> + + <div className="mx-[16px] mb-[15px] text-[13px]">총 {searchedOutputList.length}개</div> + <SearchedOutputList + searchedOutputList={ + selectedOption === '낮은 가격순' + ? searchedOutputList + .filter((el) => el.category === selectedCategory) + .sort((a, b) => parseInt(a.price) - parseInt(b.price)) + : searchedOutputList + .filter((el) => el.category === selectedCategory) + .sort((a, b) => parseInt(b.price) - parseInt(a.price)) + } + /> + <TabBar /> + </div> + ); +} + +export default SearchPage; diff --git a/src/pages/SignInPage.jsx b/src/pages/SignInPage.jsx new file mode 100644 index 0000000..e05ab9f --- /dev/null +++ b/src/pages/SignInPage.jsx @@ -0,0 +1,113 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { debounce } from 'lodash'; +import Input from '../components/common/Input'; +import LongButton from '../components/common/LongButton'; +import TextAndBackBar from '../components/common/navBar/TextAndBackBar'; +import { setUsername, setError, setPassword } from '../redux/slices/signinSlice'; +import { loginFailure, loginStart } from '../redux/slices/authSlice'; + +const SignInPage = () => { + const { username, password, error } = useSelector((state) => state.signin); + const [loginSuccess, setLoginSuccess] = useState(false); + const [isButtonClicked, setIsButtonClicked] = useState(false); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const emailRef = useRef(); + + /** TODO: 응답 데이터의 이메일 정보를 이메일 입력 필드에 자동으로 채워넣기 수정예정 */ + // const storedEmail = localStorage.getItem('signup-username'); + // useEffect(() => { + // if (emailRef.current && storedEmail) { + // emailRef.current.value = storedEmail; + // } + // }, [storedEmail]); + + // 아이디/비밀번호 상태관리 & 디바운스 처리 + const onChangeHandler = useCallback( + debounce((event) => { + event.preventDefault(); + const { name, value } = event.target; + if (name === 'email') { + dispatch(setUsername(value)); + } else if (name === 'password') { + dispatch(setPassword(value)); + } + console.log(value); + }, 300), + [dispatch], + ); + + // 로그인 시도 + const handleSignIn = () => { + setIsButtonClicked(true); + + try { + dispatch(loginStart()); + + if (loginSuccess) { + dispatch(loginSuccess({ username, password })); + navigate('/'); + localStorage.removeItem('signup-username'); + } else { + dispatch(setError('일치하는 회원정보가 없거나, 비밀번호가 일치하지 않습니다.')); + setLoginSuccess(false); + } + } catch (error) { + dispatch(loginFailure()); + } + }; + + // 회원가입 페이지 & 게스트 로그인 이동 + /** 게스트 로그인은 추후 서버에서 데이터 받아와서 연결 필요 */ + const handleMovePage = (route) => { + navigate(route); + }; + + return ( + <div className="relative"> + <TextAndBackBar title={'로그인'} /> + <form className="input-wrapper flex flex-wrap justify-center mt-[122px]"> + <Input + type={'text'} + placeholder={'아이디(이메일) 입력'} + mb={'15px'} + useRef={emailRef} + onChange={onChangeHandler} + color={isButtonClicked && !loginSuccess ? '#ff0000' : '#d9d9d9'} + /> + <Input + type={'password'} + placeholder={'비밀번호 입력'} + autoComplete={'autoComplete'} + onChange={onChangeHandler} + color={isButtonClicked && !loginSuccess ? '#ff0000' : '#d9d9d9'} + /> + {!loginSuccess && isButtonClicked && ( + <span className="text-[13px] relative left-[-25px] mt-[15px] text-[#ff0000]">{error}</span> + )} + </form> + <LongButton type={'submit'} contents={'로그인'} bottom={'405px'} onClick={handleSignIn} /> + <div className="w-full h-[44px] px-[15px] fixed top-[444px] left-1/2 translate-x-[-50%] flex flex-nowrap justify-between text-[13px] text-gray text-center leading-[44px]"> + <p className="cursor-pointer" onClick={() => handleMovePage('/')}> + 게스트 로그인 + </p> + <p className="cursor-pointer" onClick={() => handleMovePage('/register')}> + 회원가입 + </p> + </div> + + {/* 간편로그인 : 추후 api 생성 시 기능개발예정 */} + {/* <div className="fixed bottom-[70px] left-[50%] translate-x-[-50%]"> + <p className="text-[13px] text-[#6b6b6b] text-center">또는</p> + <div className="flex mt-[25px]"> + <div className="w-[44px] h-[44px] rounded-full bg-[#ea4235] mr-[15px]"></div> + <div className="w-[44px] h-[44px] rounded-full bg-[#02CD39] mr-[15px]"></div> + <div className="w-[44px] h-[44px] rounded-full bg-[#FBE300]"></div> + </div> + </div> */} + </div> + ); +}; +export default SignInPage; diff --git a/src/pages/SplashScreenPage.jsx b/src/pages/SplashScreenPage.jsx new file mode 100644 index 0000000..736ad57 --- /dev/null +++ b/src/pages/SplashScreenPage.jsx @@ -0,0 +1,90 @@ +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const SplashScreenPage = () => { + const navigate = useNavigate(); + + useEffect(() => { + const moveToWalkthrough = setTimeout(() => { + navigate('/walkthrough'); + }, 1500); + return () => clearTimeout(moveToWalkthrough); + }, [navigate]); + + return ( + <div className="relative w-full h-full flex flex-nowrap flex-col"> + <h2 className="text-[19px] text-[#42210B] font-semibold leading-[29px] text-center mt-[228px]"> + 식재료 쉐어의 즐거움 + </h2> + <div> + <svg + className="mx-auto mt-[10px]" + width="160" + height="78" + viewBox="0 0 160 78" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <g clip-path="url(#clip0_265_2633)"> + <path + d="M46.9853 60.0092V77.602H30.1684V60.0092H6.9234C4.45048 60.0092 2.67899 59.3247 1.60701 57.954C0.535041 56.5852 0 55.0505 0 53.3498C0 52.5278 0.122889 51.7208 0.370559 50.9252C0.618228 50.1314 1.01525 49.4036 1.56542 48.7475C2.1137 48.0895 2.82835 47.5691 3.70937 47.1864C4.5885 46.8036 5.65858 46.6113 6.9234 46.6113H70.2322C71.4951 46.6113 72.5671 46.8036 73.4462 47.1864C74.3254 47.571 75.04 48.0895 75.5902 48.7475C76.1384 49.4055 76.5374 50.1314 76.785 50.9252C77.0327 51.7208 77.1556 52.5278 77.1556 53.3498C77.1556 55.0486 76.6205 56.5833 75.5486 57.954C74.4766 59.3247 72.7051 60.0092 70.2322 60.0092H46.9872H46.9853Z" + fill="#42210B" + /> + <path + d="M15.053 41.9693V29.0183H45.9152V25.8112C45.9152 22.8511 46.6696 20.7149 48.1821 19.3989C49.6927 18.0829 51.7685 17.4268 54.4059 17.4268C57.0433 17.4268 59.0228 18.098 60.505 19.4404C61.9891 20.7828 62.7303 22.9077 62.7303 25.8112V29.0183H68.5004C69.7085 29.0183 70.754 29.2107 71.6331 29.5934C72.5123 29.978 73.2118 30.4701 73.7355 31.0735C74.2573 31.6768 74.643 32.3612 74.8887 33.1286C75.1364 33.8959 75.2593 34.6916 75.2593 35.5117C75.2593 36.3319 75.1364 37.1294 74.8887 37.8949C74.6411 38.6623 74.2422 39.3486 73.6939 39.95C73.1437 40.5534 72.4442 41.0323 71.5915 41.3886C70.7389 41.745 69.7085 41.9222 68.5004 41.9222" + fill="#42210B" + /> + <path + d="M8.65519 41.9244C7.4452 41.9244 6.41482 41.7471 5.56405 41.3908C4.71139 41.0344 4.00997 40.5555 3.4617 39.9522C2.91153 39.3489 2.51451 38.6645 2.26684 37.8971C2.01917 37.1297 1.89628 36.336 1.89628 35.5139C1.89628 34.6919 2.01917 33.8981 2.26684 33.1307C2.51451 32.3634 2.91153 31.679 3.4617 31.0756C4.00997 30.4723 4.71139 29.9802 5.56405 29.5956C6.41482 29.2128 7.4452 29.0205 8.65519 29.0205H15.053V41.9715" + fill="#39B54A" + /> + <path + d="M67.4284 1.89062C68.6365 1.89062 69.682 2.09614 70.5612 2.50716C71.4403 2.91818 72.1549 3.44044 72.7051 4.07018C73.2534 4.69991 73.6523 5.4126 73.9 6.20825C74.1476 7.0039 74.2705 7.78446 74.2705 8.55183C74.2705 9.37387 74.1476 10.1827 73.9 10.9765C73.6523 11.7721 73.2534 12.4848 72.7051 13.1146C72.1549 13.7443 71.4403 14.2647 70.5612 14.6757C69.682 15.0867 68.6365 15.2922 67.4284 15.2922H9.64398C8.43399 15.2922 7.40361 15.0867 6.55284 14.6757C5.70017 14.2647 4.99876 13.7443 4.45048 13.1146C3.90032 12.4848 3.50329 11.7721 3.25562 10.9765C3.00795 10.1827 2.88506 9.37387 2.88506 8.55183C2.88506 7.78446 3.00795 7.0039 3.25562 6.20825C3.50329 5.41448 3.88708 4.70179 4.40889 4.07018C4.9307 3.44044 5.61699 2.91818 6.46965 2.50716C7.32042 2.09614 8.37916 1.89062 9.64398 1.89062H67.4284Z" + fill="#39B54A" + /> + <path + d="M84.822 8.82349V8.60478C84.822 5.16954 87.6144 2.38477 91.0591 2.38477H125.956C126.999 2.38477 127.894 2.57708 128.635 2.95982C129.376 3.34445 129.981 3.85163 130.448 4.48136C130.915 5.11109 131.244 5.81058 131.437 6.57795C131.63 7.34532 131.726 8.08629 131.726 8.79709C131.726 9.5079 131.614 10.2357 131.397 10.9748C131.176 11.7157 130.847 12.3869 130.408 12.9884C129.968 13.5917 129.378 14.0857 128.637 14.4684C127.896 14.8531 127.001 15.0435 125.958 15.0435H91.061C87.6163 15.0435 84.8239 12.2587 84.8239 8.82349H84.822Z" + fill="#39B54A" + /> + <path + d="M90.8889 29.6484L129.894 29.5938C130.941 29.5938 131.851 29.7861 132.622 30.1688C133.394 30.5534 134 31.0455 134.441 31.6489C134.881 32.2522 135.212 32.9102 135.433 33.621C135.653 34.3337 135.764 35.0464 135.764 35.7591C135.764 36.5265 135.653 37.2806 135.433 38.0197C135.212 38.7588 134.881 39.4319 134.441 40.0333C133.998 40.6367 133.394 41.1307 132.622 41.5134C131.849 41.898 130.94 42.0885 129.894 42.0885H90.9173" + fill="#42210B" + /> + <path + d="M130.985 53.1053V53.6389C130.985 53.8312 130.957 54.0085 130.902 54.1725C134.087 54.0631 137.277 54.4063 140.464 55.2C143.65 55.9957 146.592 57.3098 149.284 59.1462C151.976 60.9826 154.285 63.3809 156.207 66.3391C158.13 69.2992 159.393 72.9155 160 77.1897H143.102C142.497 75.3251 141.631 73.6546 140.506 72.1745C139.379 70.6945 138.018 69.4765 136.426 68.5168C134.832 67.559 133.046 66.8727 131.068 66.4617C129.091 66.0507 127.001 65.9828 124.803 66.2562C122.878 68.2283 120.626 69.9836 118.044 71.5165C115.459 73.0493 112.769 74.3239 109.965 75.3401C107.163 76.3545 104.346 77.0936 101.516 77.5593C98.6858 78.025 96.0068 78.1211 93.4791 77.8478C92.4903 77.7365 91.5696 77.4782 90.7169 77.0672C89.8642 76.6562 89.1231 76.1226 88.4917 75.4646C87.8602 74.8066 87.3649 74.0524 87.0075 73.204C86.6502 72.3536 86.499 71.4354 86.5538 70.4493C86.6634 68.5865 87.3081 67.0782 88.4917 65.9281C89.6733 64.778 91.0591 64.1747 92.6548 64.12C93.1501 64.12 93.7116 64.1068 94.345 64.0785C94.9764 64.0521 95.5398 63.9823 96.0352 63.873C97.9579 63.6543 99.8542 63.2433 101.722 62.6399C103.59 62.0366 105.32 61.2711 106.916 60.3378C108.509 59.4064 109.937 58.2978 111.202 57.0082C112.465 55.7204 113.455 54.2272 114.17 52.5284C115.268 49.8982 116.56 48.0901 118.044 47.1021C119.528 46.1161 121.367 45.705 123.566 45.8691C125.763 45.9803 127.565 46.5968 128.966 47.7187C130.367 48.8424 131.04 50.6373 130.985 53.1034V53.1053Z" + fill="#42210B" + /> + <path + d="M140.71 8.13742C140.71 5.45258 141.548 3.42386 143.225 2.05316C144.9 0.684341 146.838 -0.00195312 149.036 -0.00195312C150.08 -0.00195312 151.11 0.177162 152.128 0.531622C153.145 0.887967 154.037 1.40834 154.807 2.09275C155.576 2.77905 156.194 3.62749 156.661 4.64185C157.128 5.6562 157.363 6.81951 157.363 8.13554V49.8958H140.712V8.13742H140.71Z" + fill="#42210B" + /> + <path + d="M15.053 1.89258V15.2904C18.855 15.2904 21.9367 18.3636 21.9367 22.1552C21.9367 25.9468 18.855 29.0201 15.053 29.0201V41.971C26.1508 41.971 35.1463 33.0002 35.1463 21.9328C35.1463 10.8653 26.1508 1.89258 15.053 1.89258Z" + fill="#39B54A" + /> + <path + d="M90.8889 2.38281V15.0623C94.6966 15.3187 97.7065 18.4787 97.7065 22.34C97.7065 26.2014 94.6966 29.3613 90.8889 29.6178V42.088C101.883 42.088 110.797 33.2001 110.797 22.2344C110.797 11.2688 101.885 2.38281 90.8889 2.38281Z" + fill="#39B54A" + /> + <path + d="M91.0591 42.0885C87.6144 42.0885 84.822 39.3037 84.822 35.8685C84.822 32.4445 87.5974 29.6635 91.0326 29.6484" + fill="#39B54A" + /> + </g> + <defs> + <clipPath id="clip0_265_2633"> + <rect width="160" height="78" fill="white" /> + </clipPath> + </defs> + </svg> + </div> + <div id="logo" className="w-full h-full flex justify-center items-center"> + <div className="fixed bottom-0"> + <img src={process.env.PUBLIC_URL + '/images/splash.png'} alt="Splash" /> + </div> + </div> + </div> + ); +}; + +export default SplashScreenPage; diff --git a/src/pages/WalkthroughPage.jsx b/src/pages/WalkthroughPage.jsx new file mode 100644 index 0000000..25f6578 --- /dev/null +++ b/src/pages/WalkthroughPage.jsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import Carousel from '../components/common/Carousel'; +import ImageAndMessage from '../components/common/ImageAndMessage'; +import LongButton from '../components/common/LongButton'; + +const WalkthroughPage = () => { + const [carouselColor, setCarouselColor] = useState(['#00c92c', '#d9d9d9']); + const [walkthroughPosition, setWalkthroughPosition] = useState(0); + const [firstImageVisible, setFirstImageVisible] = useState(true); + const [buttonClicked, setButtonClicked] = useState(false); + const navigate = useNavigate(); + + const startHandler = () => { + if (buttonClicked) { + navigate('/signin'); + } else { + setCarouselColor(['#d9d9d9', '#00c92c']); + setFirstImageVisible(false); + if (firstImageVisible) { + setTimeout(() => { + setWalkthroughPosition(-50); + }, 100); + } + setButtonClicked(true); + } + }; + + return ( + <div className="relative w-[390px] overflow-hidden"> + <div + className="walkthroughWrapper w-[780px] flex transition-all duration-500 ease" + style={{ + transform: `translateX(${walkthroughPosition}%)`, + }} + > + <ImageAndMessage + opacity={firstImageVisible ? 1 : 0} + src={process.env.PUBLIC_URL + '/images/walkthrough.gif'} + mainMessage={'우리동네'} + subMessage={ + <> + 가까운 동네 친구들과 + <br /> 혼자 사기 힘들었던 상품을 + <br /> 이제 부담없이 나눠가져요 + </> + } + /> + <ImageAndMessage + opacity={firstImageVisible ? 0 : 1} + imgLeft={'480px'} + msgLeft={'470px'} + src={process.env.PUBLIC_URL + '/images/walkthrough2.gif'} + mainMessage={'함께사요'} + color={'#EE0707'} + subMessage={ + <> + 한번에 많은 물건을 혼자사기 + <br /> + 부담스럽다면 동네 친구들과 함께 + <br /> + 구입하고 먹을만큼 나눠가져요 + </> + } + /> + </div> + <div className="flex justify-between w-[30px] fixed bottom-[125px] left-1/2 translate-x-[-50%]"> + <Carousel carouselColor={carouselColor[0]} /> + <Carousel carouselColor={carouselColor[1]} /> + </div> + <LongButton type={'button'} contents={'시작하기'} onClick={startHandler} /> + </div> + ); +}; + +export default WalkthroughPage; diff --git a/src/pages/WritingPage.jsx b/src/pages/WritingPage.jsx new file mode 100644 index 0000000..80cd7ee --- /dev/null +++ b/src/pages/WritingPage.jsx @@ -0,0 +1,190 @@ +import React, { useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import Select from 'react-select'; + +import LongButton from '../components/common/LongButton'; +import Input from '../components/writingPage/Input'; +import BackButton from '../components/common/navBar/BackButton'; +import { writingSlice } from '../redux/slices/writingSlice'; + +import { ADD_IMAGE, ARTICLE_TITLE, TOTAL_AMOUNT, MAXIMUM_PEOPLE, PLEASE_WRITE_TEXT, DONE } from '../static/constants'; + +function WritingPage() { + const options = [ + // TODO: 하드코딩된 값 constants로 추후 변경 예정 + { value: '전체', label: '전체' }, + { value: '과일', label: '과일' }, + { value: '채소', label: '채소' }, + { value: '쌀/잡곡/견과', label: '쌀/잡곡/견과' }, + { value: '정육/계란류', label: '정육/계란류' }, + { value: '수산물/건해산', label: '수산물/건해산' }, + { value: '우유/유제품', label: '우유/유제품' }, + { value: '김치/반찬/델리', label: '김치/반찬/델리' }, + { value: '생수/음료/주류', label: '생수/음료/주류' }, + { value: '정육/계란류', label: '정육/계란류' }, + { value: '커피/차/원두', label: '커피/차/원두' }, + { value: '면류/통조림', label: '면류/통조림' }, + { value: '양념/오일', label: '양념/오일' }, + { value: '과자/간식', label: '과자/간식' }, + { value: '베이커리/잼', label: '베이커리/잼' }, + { value: '친환경/유기농', label: '친환경/유기농' }, + ]; + const min = 1; + const max = 4; + + const imgRef = useRef(null); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const title = useSelector((state) => state.writing.title); + const imageUrl = useSelector((state) => state.writing.imageUrl); + const totalAmount = useSelector((state) => state.writing.totalAmount); + const textarea = useSelector((state) => state.writing.textarea); + + const [count, setCount] = useState(min); + const [selectedCategory, setSelectedCategory] = useState(options[0]); + + const addImages = () => imgRef.current?.click(); + const removeImage = () => dispatch(writingSlice.actions.setImageUrl(null)); + const onChangeImage = (e) => { + const files = e.target.files[0]; + const reader = new FileReader(); + + if (files === undefined) return; + + reader.readAsDataURL(files); + reader.onload = () => { + dispatch(writingSlice.actions.setImageUrl(reader.result)); + }; + }; + + const onChangeTitle = (e) => { + dispatch(writingSlice.actions.setTitle(e.target.value)); + }; + + const onChangeCategory = (e) => { + const selectedValue = e.value; + setSelectedCategory(selectedValue); + dispatch(writingSlice.actions.setCategory(selectedValue)); + }; + + const onChangeTotalAmount = (e) => { + const totalAmount = e.target.value; + + if (!/^[0-9]*$/.test(totalAmount)) e.target.value = ''; + else dispatch(writingSlice.actions.setTotalAmount(totalAmount)); + }; + + const decreaseCount = () => { + const newCount = Math.max(count - 1, min); + setCount(newCount); + dispatch(writingSlice.actions.setMaxPeople(newCount)); + }; + const increaseCount = () => { + const newCount = Math.min(count + 1, max); + setCount(newCount); + dispatch(writingSlice.actions.setMaxPeople(newCount)); + }; + + const onChangeTextarea = (e) => { + dispatch(writingSlice.actions.setTextarea(e.target.value)); + }; + + const onclickDoneButton = () => navigate('/posts'); + + return ( + <div className="mt-[47px]"> + <BackButton /> + <div className="flex justify-center"> + <div + className="flex items-center justify-center w-[360px] h-[238px] mt-[11px] bg-gray rounded-[15px] cursor-pointer" + onClick={addImages} + onChange={onChangeImage} + > + <button className="flex flex-col items-center text-[13px] text-darkGray"> + <input type="file" className="hidden" multiple accept="image/png, image/jpg, image/jpeg" ref={imgRef} /> + {imageUrl && <img alt="" src={imageUrl} className="w-[360px] h-[238px] rounded-[15px] z-[50]" />} + <div className="flex flex-col top-[177px] absolute"> + <svg + width="76" + height="68" + viewBox="0 0 76 68" + fill="none" + xmlns="http://www.w3.org/2000/svg" + className="mb-[20px]" + > + <path + d="M8 8H19.25L26.75 0.5H49.25L56.75 8H68C69.9891 8 71.8968 8.79018 73.3033 10.1967C74.7098 11.6032 75.5 13.5109 75.5 15.5V60.5C75.5 62.4891 74.7098 64.3968 73.3033 65.8033C71.8968 67.2098 69.9891 68 68 68H8C6.01088 68 4.10322 67.2098 2.6967 65.8033C1.29018 64.3968 0.5 62.4891 0.5 60.5V15.5C0.5 13.5109 1.29018 11.6032 2.6967 10.1967C4.10322 8.79018 6.01088 8 8 8ZM38 19.25C33.0272 19.25 28.2581 21.2254 24.7417 24.7417C21.2254 28.2581 19.25 33.0272 19.25 38C19.25 42.9728 21.2254 47.7419 24.7417 51.2583C28.2581 54.7746 33.0272 56.75 38 56.75C42.9728 56.75 47.7419 54.7746 51.2583 51.2583C54.7746 47.7419 56.75 42.9728 56.75 38C56.75 33.0272 54.7746 28.2581 51.2583 24.7417C47.7419 21.2254 42.9728 19.25 38 19.25ZM38 26.75C40.9837 26.75 43.8452 27.9353 45.955 30.045C48.0647 32.1548 49.25 35.0163 49.25 38C49.25 40.9837 48.0647 43.8452 45.955 45.955C43.8452 48.0647 40.9837 49.25 38 49.25C35.0163 49.25 32.1548 48.0647 30.045 45.955C27.9353 43.8452 26.75 40.9837 26.75 38C26.75 35.0163 27.9353 32.1548 30.045 30.045C32.1548 27.9353 35.0163 26.75 38 26.75Z" + fill="#9D9D9D" + /> + </svg> + {ADD_IMAGE} + </div> + </button> + </div> + </div> + + <div> + {imageUrl ? ( + <button + onClick={removeImage} + className="flex items-center justify-center h-[32px] w-[32px] top-[114px] ml-[331px] rounded-full bg-[black] opacity-[60%] absolute z-[999]" + > + <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + d="M13.6857 12.6573L8.02886 7.00048L13.6857 1.34362C13.8221 1.20723 13.8987 1.02225 13.8987 0.829365C13.8987 0.63648 13.8221 0.451495 13.6857 0.315104C13.5493 0.178715 13.3643 0.102091 13.1715 0.102091C12.9786 0.102091 12.7936 0.178715 12.6572 0.315105L7.00034 5.97196L1.34348 0.315104C1.20709 0.178714 1.02211 0.102091 0.829224 0.102091C0.636339 0.102091 0.451354 0.178714 0.314964 0.315104C0.245591 0.379402 0.190806 0.457824 0.154302 0.545084C0.117798 0.632344 0.100422 0.726415 0.103345 0.820958C0.10627 0.915501 0.129426 1.00832 0.171253 1.09316C0.21308 1.17799 0.272605 1.25288 0.34582 1.31277L6.00267 6.96962L0.314964 12.6573C0.178574 12.7937 0.101951 12.9787 0.101951 13.1716C0.10195 13.3645 0.178574 13.5495 0.314964 13.6859C0.451355 13.8222 0.636339 13.8989 0.829224 13.8989C1.02211 13.8989 1.20709 13.8222 1.34348 13.6859L7.00034 8.029L12.6572 13.6859C12.7936 13.8222 12.9786 13.8989 13.1715 13.8989C13.3643 13.8989 13.5493 13.8222 13.6857 13.6859C13.8221 13.5495 13.8987 13.3645 13.8987 13.1716C13.8987 12.9787 13.8221 12.7937 13.6857 12.6573Z" + fill="white" + /> + </svg> + </button> + ) : ( + '' + )} + </div> + + <Input placeholder={ARTICLE_TITLE} onChange={onChangeTitle} /> + + <div className="mx-[15px] py-[15px] text-red border-b-[0.5px] border-gray"> + <Select defaultValue={selectedCategory} options={options} onChange={onChangeCategory} /> + </div> + + <Input placeholder={TOTAL_AMOUNT} onChange={onChangeTotalAmount} /> + + <div className="flex justify-between py-[15px] mx-[15px] text-[13px] text-darkGray border-b-[0.5px] border-gray"> + <div className="flex items-center">{MAXIMUM_PEOPLE}</div> + <div className="flex"> + <button onClick={decreaseCount}> + <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + d="M22.9167 0H2.08333C1.5308 0 1.00089 0.219493 0.610194 0.610194C0.219493 1.00089 0 1.5308 0 2.08333V22.9167C0 23.4692 0.219493 23.9991 0.610194 24.3898C1.00089 24.7805 1.5308 25 2.08333 25H22.9167C23.4692 25 23.9991 24.7805 24.3898 24.3898C24.7805 23.9991 25 23.4692 25 22.9167V2.08333C25 1.5308 24.7805 1.00089 24.3898 0.610194C23.9991 0.219493 23.4692 0 22.9167 0ZM19.7917 13.5417H5.20833C4.93207 13.5417 4.66711 13.4319 4.47176 13.2366C4.27641 13.0412 4.16667 12.7763 4.16667 12.5C4.16667 12.2237 4.27641 11.9588 4.47176 11.7634C4.66711 11.5681 4.93207 11.4583 5.20833 11.4583H19.7917C20.0679 11.4583 20.3329 11.5681 20.5282 11.7634C20.7236 11.9588 20.8333 12.2237 20.8333 12.5C20.8333 12.7763 20.7236 13.0412 20.5282 13.2366C20.3329 13.4319 20.0679 13.5417 19.7917 13.5417Z" + fill="#A4A4A4" + /> + </svg> + </button> + <div className="flex justify-center items-center w-[63px] mx-[10px]">{count}</div> + <button onClick={increaseCount}> + <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + d="M22.9167 0H2.08333C1.5308 0 1.00089 0.219493 0.610194 0.610194C0.219493 1.00089 0 1.5308 0 2.08333V22.9167C0 23.4692 0.219493 23.9991 0.610194 24.3898C1.00089 24.7805 1.5308 25 2.08333 25H22.9167C23.4692 25 23.9991 24.7805 24.3898 24.3898C24.7805 23.9991 25 23.4692 25 22.9167V2.08333C25 1.5308 24.7805 1.00089 24.3898 0.610194C23.9991 0.219493 23.4692 0 22.9167 0ZM19.7917 13.5417H13.5417V19.7917C13.5417 20.0679 13.4319 20.3329 13.2366 20.5282C13.0412 20.7236 12.7763 20.8333 12.5 20.8333C12.2237 20.8333 11.9588 20.7236 11.7634 20.5282C11.5681 20.3329 11.4583 20.0679 11.4583 19.7917V13.5417H5.20833C4.93207 13.5417 4.66711 13.4319 4.47176 13.2366C4.27641 13.0412 4.16667 12.7763 4.16667 12.5C4.16667 12.2237 4.27641 11.9588 4.47176 11.7634C4.66711 11.5681 4.93207 11.4583 5.20833 11.4583H11.4583V5.20833C11.4583 4.93207 11.5681 4.66711 11.7634 4.47176C11.9588 4.27641 12.2237 4.16667 12.5 4.16667C12.7763 4.16667 13.0412 4.27641 13.2366 4.47176C13.4319 4.66711 13.5417 4.93207 13.5417 5.20833V11.4583H19.7917C20.0679 11.4583 20.3329 11.5681 20.5282 11.7634C20.7236 11.9588 20.8333 12.2237 20.8333 12.5C20.8333 12.7763 20.7236 13.0412 20.5282 13.2366C20.3329 13.4319 20.0679 13.5417 19.7917 13.5417Z" + fill="#A4A4A4" + /> + </svg> + </button> + </div> + </div> + <textarea + placeholder={PLEASE_WRITE_TEXT} + onChange={onChangeTextarea} + className="w-[92%] h-[150px] py-[20px] mx-[15px] focus:outline-none text-[13px] text-darkGray" + /> + + {!title || totalAmount === 0 || !textarea ? ( + <LongButton contents={DONE} className="disabled" background={'#CCCCCC'} customStyle={'disabled'} /> + ) : ( + <LongButton contents={DONE} onClick={onclickDoneButton} /> + )} + </div> + ); +} + +export default WritingPage; diff --git a/src/redux/api/authApi.js b/src/redux/api/authApi.js new file mode 100644 index 0000000..294d872 --- /dev/null +++ b/src/redux/api/authApi.js @@ -0,0 +1,87 @@ +import axios from 'axios'; +import CryptoJS from 'crypto-js'; + +const encryptionKey = process.env.REACT_APP_SECRET_KEY; +const BASE_URL = 'http://localhost:8080/api/v1'; + +// 유저정보 암호화 후 로컬스토리지에 저장 +export const saveUserInfo = async ({ username, password, nickname }) => { + const encryptedusername = CryptoJS.AES.encrypt(username, encryptionKey).toString(); + const encryptedPassword = CryptoJS.AES.encrypt(password, encryptionKey).toString(); + + try { + localStorage.setItem('signup-username', encryptedusername); + localStorage.setItem('signup-password', encryptedPassword); + localStorage.setItem('signup-nickname', nickname); + } catch (error) { + throw new Error(error.message); + } +}; + +// 사용자 아이디 검증 +export const checkEmail = async ({ email }) => { + try { + const response = await axios.post(`${BASE_URL}/auth/join/check`, email); + const result = response.data; + return result; // 서버로부터 중복 여부 확인 결과 반환 + } catch (error) { + throw new Error(error.message); + } +}; + +// 회원가입 API 호출 +export const signUpAPI = async ({ address }) => { + const nickname = localStorage.getItem('signup-nickname'); + const encryptedUsername = localStorage.getItem('signup-email'); + const encryptedPassword = localStorage.getItem('signup-password'); + + const decryptedUsername = CryptoJS.AES.decrypt(encryptedUsername, encryptionKey).toString(CryptoJS.enc.Utf8); + const decryptedPassword = CryptoJS.AES.decrypt(encryptedPassword, encryptionKey).toString(CryptoJS.enc.Utf8); + + const userInfo = { + userId: '', + pw: decryptedPassword, + email: decryptedUsername, + name: nickname, + address: address, // 복호화 확인 필요 + }; + + try { + const response = await axios.post(`${BASE_URL}/auth/join/Proc`, userInfo); + const result = response.data; + + if (result.ACCESS_TOKEN) { + userInfo.userId = result.ACCESS_TOKEN; // userId 값 설정 + } + console.log(result); + } catch (error) { + throw new Error(error.message); + } +}; + +// 로그인 API 호출 +export const signInAPI = async ({ email, password }) => { + try { + const response = await axios.post(`${BASE_URL}/auth/login`, { + email, + password, + }); + const result = response.data; + + if (result.ACCESS_TOKEN) { + localStorage.setItem('signin-token', result.ACCESS_TOKEN); + } + } catch (error) { + throw new Error(error.message); + } +}; + +// 로그아웃 API 호출 +export const logoutAPI = async () => { + try { + await axios.get(`${BASE_URL}/logout`); + localStorage.removeItem('signin-token'); + } catch (error) { + throw new Error(error.message); + } +}; \ No newline at end of file diff --git a/src/redux/api/userInfoUpdateAPI.js b/src/redux/api/userInfoUpdateAPI.js new file mode 100644 index 0000000..887e572 --- /dev/null +++ b/src/redux/api/userInfoUpdateAPI.js @@ -0,0 +1,56 @@ +import axios from 'axios'; +import CryptoJS from 'crypto-js'; + +const encryptionKey = process.env.REACT_APP_SECRET_KEY; +const BASE_URL = 'http://localhost:8080/api/v1'; + +// 유저정보 암호화 후 로컬스토리지에 저장 +export const UserInfoUpdate = async ({ newPassword, newNickname }) => { + const encryptedPassword = CryptoJS.AES.encrypt(newPassword, encryptionKey).toString(); + + try { + // TODO: 서버에 저장된 유저정보로 변경하기 + const storedPassword = localStorage.getItem('signup-password'); + const storedNickname = localStorage.getItem('signup-nickname'); + + // 새로운 값을 로컬 스토리지에 비동기적으로 저장하기 + if (newPassword !== undefined) { + localStorage.setItem('signup-password', encryptedPassword); + } else { + localStorage.setItem('signup-password', storedPassword); + } + + if (newNickname !== undefined) { + localStorage.setItem('signup-nickname', newNickname); + } else { + localStorage.setItem('signup-nickname', storedNickname); + } + } catch (error) { + throw new Error(error.message); + } +}; + +// 사용자 회원정보 조회 +export const inquireUserInfoAPI = async ({ username }) => { + try { + const response = await axios.post(`${BASE_URL}/user/detail`, username); + const result = response.data; + console.log(result); + localStorage.setItem('username', result.username); + return result; + } catch (error) { + throw new Error(error.message); + } +}; + +// 사용자 정보(비밀번호) 변경 +export const updatePasswordAPI = async ({ username }) => { + try { + const response = await axios.post(`${BASE_URL}/user/modify`, username); + const result = response.data; + console.log(result); + return result; + } catch (error) { + throw new Error(error.message); + } +}; diff --git a/src/redux/slices/authSlice.js b/src/redux/slices/authSlice.js new file mode 100644 index 0000000..f5ff667 --- /dev/null +++ b/src/redux/slices/authSlice.js @@ -0,0 +1,77 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { signInAPI, signupAPI, logoutAPI, inquireUserInfoAPI } from '../api/authApi'; +import { saveUserInfo } from '../api/authApi'; + +const initialState = { + user: { + id: '', + username: '', + password: '', + nickname: '', + address: '', + }, + isLoading: false, + error: null, +}; + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + loginStart(state) { + state.isLoading = true; + state.error = null; + }, + loginSuccess(state, action) { + state.isLoading = false; + state.user = action.payload; + signInAPI(state.user); + state.error = null; + }, + loginFailure(state, action) { + state.isLoading = false; + state.error = action.payload; + }, + logoutStart: (state) => { + state.isLoading = true; + state.error = null; + }, + logoutSuccess: (state) => { + state.isLoading = false; + logoutAPI() + state.error = null; + }, + logoutFailure: (state, action) => { + state.isLoading = false; + state.error = action.payload; + }, + signupStart(state) { + state.isLoading = true; + state.error = null; + }, + signupSuccess(state, action) { + state.isLoading = false; + state.user = action.payload; + saveUserInfo(state.user); + state.error = null; + }, + signupFailure(state, action) { + state.isLoading = false; + state.error = action.payload; + }, + }, +}); + +export const { + loginStart, + loginSuccess, + loginFailure, + logoutStart, + logoutSuccess, + logoutFailure, + signupStart, + signupSuccess, + signupFailure, +} = authSlice.actions; + +export default authSlice.reducer; diff --git a/src/redux/slices/friendsSlice.js b/src/redux/slices/friendsSlice.js new file mode 100644 index 0000000..65722bb --- /dev/null +++ b/src/redux/slices/friendsSlice.js @@ -0,0 +1,19 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + friendsList: [false, false, false, false], + recruteList: ['', '', '', ''], +}; + +export const friendsSlice = createSlice({ + name: 'friends', + initialState, + reducers: { + setFriendsList: (state, action) => { + state.friendsList = action.payload; + }, + setRecruteList: (state, action) => { + state.recruteList = action.payload; + }, + }, +}); diff --git a/src/redux/slices/myPageSlice.js b/src/redux/slices/myPageSlice.js new file mode 100644 index 0000000..005e6ab --- /dev/null +++ b/src/redux/slices/myPageSlice.js @@ -0,0 +1,33 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { inquireUserInfoAPI } from '../api/userInfoUpdateAPI'; + +const initialState = { + username: '', + isLoading: false, + error: null, +}; + +const myPageSlice = createSlice({ + name: 'mypage', + initialState, + reducers: { + getUserInfoStart(state) { + state.isLoading = true; + state.error = null; + }, + getUserInfoSuccess(state, action) { + state.isLoading = false; + state.username = action.payload; + inquireUserInfoAPI(state.username); + state.error = null; + }, + getUserInfoFailure(state, action) { + state.isLoading = false; + state.error = action.payload; + }, + }, +}); + +export const { getUserInfoStart, getUserInfoSuccess, getUserInfoFailure } = myPageSlice.actions; + +export default myPageSlice.reducer; diff --git a/src/redux/slices/registerSlice.js b/src/redux/slices/registerSlice.js new file mode 100644 index 0000000..9803d0b --- /dev/null +++ b/src/redux/slices/registerSlice.js @@ -0,0 +1,48 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + userId: '', + email: '', + password: '', + passwordCheck: '', + nickname: '', + errors: { + email: { message: '아이디를 입력해주세요.', isError: false }, + password: { message: '8~16자리의 비밀번호를 입력해주세요.', isError: false }, + passwordCheck: { message: '확인을 위하여 위와 동일하게 입력해주세요.', isError: false }, + nickname: { message: '', isError: false }, + }, +}; + +const registerSlice = createSlice({ + name: 'signup', + initialState, + reducers: { + setEmail: (state, action) => { + state.email = action.payload; + }, + setPassword: (state, action) => { + state.password = action.payload; + }, + setPasswordCheck: (state, action) => { + state.passwordCheck = action.payload; + }, + setNickname: (state, action) => { + state.nickname = action.payload; + }, + setErrors: (state, action) => { + state.errors = action.payload; + }, + resetFields: (state, action) => { + state.nickname = ''; + state.email = ''; + state.password = ''; + state.passwordCheck = ''; + state.errors = {}; + }, + }, +}); + +export const { setEmail, setPassword, setPasswordCheck, setNickname, setErrors, resetFields } = registerSlice.actions; + +export default registerSlice.reducer; diff --git a/src/redux/slices/selectedCategorySlice.js b/src/redux/slices/selectedCategorySlice.js new file mode 100644 index 0000000..ed8c0a0 --- /dev/null +++ b/src/redux/slices/selectedCategorySlice.js @@ -0,0 +1,16 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { TOTAL_SEARCHED_OUTPUT } from '../../static/constants'; + +const initialState = { + category: TOTAL_SEARCHED_OUTPUT, +}; + +export const selectedCategorySlice = createSlice({ + name: 'selectedCategory', + initialState, + reducers: { + setCategory: (state, action) => { + state.category = action.payload; + }, + }, +}); diff --git a/src/redux/slices/signinSlice.js b/src/redux/slices/signinSlice.js new file mode 100644 index 0000000..02b89ef --- /dev/null +++ b/src/redux/slices/signinSlice.js @@ -0,0 +1,32 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + username: '', + password: '', + error: '', +}; + +const signinSlice = createSlice({ + name: 'signip', + initialState, + reducers: { + setUsername: (state, action) => { + state.username = action.payload; + }, + setPassword: (state, action) => { + state.password = action.payload; + }, + setError: (state, action) => { + state.error = action.payload; + }, + resetFields: (state, action) => { + state.username = ''; + state.password = ''; + state.errors = ''; + }, + }, +}); + +export const { setUsername, setPassword, setError, resetFields } = signinSlice.actions; + +export default signinSlice.reducer; diff --git a/src/redux/slices/userInfoChangeSlice.js b/src/redux/slices/userInfoChangeSlice.js new file mode 100644 index 0000000..3971aeb --- /dev/null +++ b/src/redux/slices/userInfoChangeSlice.js @@ -0,0 +1,86 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { UserInfoUpdate } from '../api/userInfoUpdateAPI'; + +const userInfoChangeSlice = createSlice({ + name: 'userInfoChange', + initialState: { + newPassword: '', + newPasswordCheck: '', + currentNickname: '', + newNickname: '', + errors: { + newPassword: { message: '8~16자리의 비밀번호를 입력해주세요.', isError: false }, + newPasswordCheck: { message: '', isError: false }, + newNickname: { message: '', isError: false }, + }, + updateError: null, + }, + reducers: { + setNewPassword: (state, action) => { + state.newPassword = action.payload; + }, + setNewPasswordCheck: (state, action) => { + state.newPasswordCheck = action.payload; + }, + getCurrentNickname: (state, action) => { + state.currentNickname = action.payload; + }, + setNewNickname: (state, action) => { + state.newNickname = action.payload; + }, + setErrors: (state, action) => { + state.errors = action.payload; + }, + updatePasswordStart(state) { + state.isLoading = true; + state.updateError = null; + }, + updatePasswordSuccess(state, action) { + state.isLoading = false; + state.currentPassword = action.payload; + UserInfoUpdate(state.currentPassword); + state.updateError = null; + }, + updatePasswordFailure(state, action) { + state.isLoading = false; + state.updateError = action.payload; + }, + updateNicknameStart(state) { + state.isLoading = true; + state.updateError = null; + }, + updateNicknameSuccess(state, action) { + state.isLoading = false; + state.currentNickname = action.payload; + UserInfoUpdate(state.currentNickname); + state.updateError = null; + }, + updateNicknameFailure(state, action) { + state.isLoading = false; + state.updateError = action.payload; + alert('닉네임을 입력해주세요.'); + }, + resetFields: (state) => { + state.userId = ''; + state.newPassword = ''; + state.newPasswordCheck = ''; + state.errors = {}; + }, + }, +}); + +export const { + setNewPasswordCheck, + setNewPassword, + getCurrentNickname, + setNewNickname, + setErrors, + updatePasswordStart, + updatePasswordSuccess, + updatePasswordFailure, + updateNicknameStart, + updateNicknameSuccess, + updateNicknameFailure, + resetFields, +} = userInfoChangeSlice.actions; +export default userInfoChangeSlice.reducer; diff --git a/src/redux/slices/writingSlice.js b/src/redux/slices/writingSlice.js new file mode 100644 index 0000000..4aa42b2 --- /dev/null +++ b/src/redux/slices/writingSlice.js @@ -0,0 +1,41 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + imageUrl: null, + title: '', + category: '전체', + totalAmount: 0, + maxPeople: 1, + textarea: '', +}; + +export const writingSlice = createSlice({ + name: 'writing', + initialState, + reducers: { + setImageUrl: (state, action) => { + state.imageUrl = action.payload; + localStorage.setItem('imageUrl', JSON.stringify(state.imageUrl)); + }, + setTitle: (state, action) => { + state.title = action.payload; + localStorage.setItem('title', JSON.stringify(state.title)); + }, + setCategory: (state, action) => { + state.category = action.payload; + localStorage.setItem('category', JSON.stringify(state.category)); + }, + setTotalAmount: (state, action) => { + state.totalAmount = action.payload; + localStorage.setItem('totalAmount', JSON.stringify(state.totalAmount)); + }, + setMaxPeople: (state, action) => { + state.maxPeople = action.payload; + localStorage.setItem('maxPeople', JSON.stringify(state.maxPeople)); + }, + setTextarea: (state, action) => { + state.textarea = action.payload; + localStorage.setItem('textarea', JSON.stringify(state.textarea)); + }, + }, +}); diff --git a/src/redux/store.js b/src/redux/store.js new file mode 100644 index 0000000..a4e4b33 --- /dev/null +++ b/src/redux/store.js @@ -0,0 +1,23 @@ +import { configureStore } from '@reduxjs/toolkit'; +import authSlice from './slices/authSlice'; +import registerSlice from './slices/registerSlice'; +import signinSlice from './slices/signinSlice'; +import userInfoChangeSlice from './slices/userInfoChangeSlice'; +import myPageSlice from './slices/myPageSlice'; + +import { writingSlice } from './slices/writingSlice'; +import { selectedCategorySlice } from './slices/selectedCategorySlice'; +import { friendsSlice } from './slices/friendsSlice'; + +export const store = configureStore({ + reducer: { + auth: authSlice, + register: registerSlice, + signin: signinSlice, + myPage: myPageSlice, + userInfoChange: userInfoChangeSlice, + writing: writingSlice.reducer, + selectedCategory: selectedCategorySlice.reducer, + friends: friendsSlice.reducer, + }, +}); diff --git a/src/static/constants.js b/src/static/constants.js new file mode 100644 index 0000000..f6a9157 --- /dev/null +++ b/src/static/constants.js @@ -0,0 +1,113 @@ +// PermissionPage +const PERMISSION_FIRST_TITLE = '우리동네 함께사요 이용을 위해'; +const PERMISSION_SECOND_TITLE = '앱 권한을 허용해주세요'; +const SELECT_PERMISSION = '선택 권한'; +const TEMPORARY_SRC = 'https://img.freepik.com/free-psd/portrait-of-an-adorable-golden-retriever-puppy_53876-73975.jpg'; +const NOTIFICATION = '알림'; +const NOTIFICATION_DESCRIPTION = '채팅 등 앱 알림 수신 시 필요'; +const LOCATION = '위치'; +const LOCATION_DESCRIPTION = '지역별 상품 등록, 상품 검색 시 허용'; +const CAMERA = '카메라/앨범'; +const CAMERA_DESCRIPTION = '상품 등록, 채팅에서 사진 전송 시 사용'; +const MIKE = '마이크'; +const MIKE_DESCRIPTION = '채팅에서 동영상 촬영 시 사용'; +const STORAGE = '저장소'; +const STORAGE_DESCRIPTION = '사진 편집 및 저장 시 사용'; +const PERMISSION_MESSAGE = + '해당 기능을 사용할 때 권한 허용이 필요하며, 허용되지 않아도 해당 기능 외 서비스 이용이 가능합니다.'; +const CONFIRM = '확인'; + +// RegisterLocationPage +const SEARCH_LOCATION = '내 동네 이름(동,읍,면)으로 검색'; +const NEAR_LOCATION = '근처 동네'; + +// RegisterLocationCompletePage +const MOVE_TO_HOME = '홈으로 가기'; +const REGISTER_LOCATION_COMPLETE = '위치등록 완료'; + +// SearchPage +const ENTER_INPUT = '검색어를 입력하세요'; +const TOTAL_SEARCHED_OUTPUT = '전체'; + +// WritingPage +const ADD_IMAGE = '이미지 추가'; +const ARTICLE_TITLE = '글 제목'; +const CATEGORY = '카테고리'; +const TOTAL_AMOUNT = '총 가격 (ex. 35000)'; +const MAXIMUM_PEOPLE = '희망 인원 (최대 4명)'; +const PLEASE_WRITE_TEXT = '내용을 작성해주세요'; +const DONE = '완료'; + +// CategoryListPage +const SELECT = '선택'; + +// MyPage +const MY_PAGE = '마이 페이지'; +const SETTING_LOCATION = '내 동네 설정'; +const CHANGE_INFO = '내 정보 변경'; +const LOGOUT = '로그아웃'; + +// PostsPage +const JOIN_ALERT = '현재 게시물에 참여하시겠습니까?'; +const CANCEL = '취소'; +const BEFORE = ' 전'; +const SUM = '총 금액'; +const WON = '원'; +const DIVISION = '1/N'; +const ACTUAL_PAYMENT_AMOUNT = '실제 결제 금액'; +const JOIN = '참여하기'; + +// ProfileEditPage +const EDIT = '수정하기'; + +// SearchedOutputList +const TIMEOUT = 100; +const ERROR_ALERT_MESSAGE = 'something went wrong'; + +export { + PERMISSION_FIRST_TITLE, + PERMISSION_SECOND_TITLE, + SELECT_PERMISSION, + TEMPORARY_SRC, + NOTIFICATION, + NOTIFICATION_DESCRIPTION, + LOCATION, + LOCATION_DESCRIPTION, + CAMERA, + CAMERA_DESCRIPTION, + MIKE, + MIKE_DESCRIPTION, + STORAGE, + STORAGE_DESCRIPTION, + PERMISSION_MESSAGE, + CONFIRM, + SEARCH_LOCATION, + NEAR_LOCATION, + ENTER_INPUT, + TOTAL_SEARCHED_OUTPUT, + ADD_IMAGE, + ARTICLE_TITLE, + CATEGORY, + TOTAL_AMOUNT, + MAXIMUM_PEOPLE, + PLEASE_WRITE_TEXT, + DONE, + SELECT, + MY_PAGE, + SETTING_LOCATION, + CHANGE_INFO, + LOGOUT, + JOIN_ALERT, + CANCEL, + SUM, + WON, + BEFORE, + DIVISION, + ACTUAL_PAYMENT_AMOUNT, + JOIN, + EDIT, + MOVE_TO_HOME, + REGISTER_LOCATION_COMPLETE, + TIMEOUT, + ERROR_ALERT_MESSAGE, +}; diff --git a/src/static/fonts/AppleSDGothicNeoB.ttf b/src/static/fonts/AppleSDGothicNeoB.ttf new file mode 100644 index 0000000..ebd50e2 Binary files /dev/null and b/src/static/fonts/AppleSDGothicNeoB.ttf differ diff --git a/src/static/fonts/AppleSDGothicNeoEB.ttf b/src/static/fonts/AppleSDGothicNeoEB.ttf new file mode 100644 index 0000000..d293d59 Binary files /dev/null and b/src/static/fonts/AppleSDGothicNeoEB.ttf differ diff --git a/src/static/fonts/AppleSDGothicNeoH.ttf b/src/static/fonts/AppleSDGothicNeoH.ttf new file mode 100644 index 0000000..9a438b2 Binary files /dev/null and b/src/static/fonts/AppleSDGothicNeoH.ttf differ diff --git a/src/static/fonts/AppleSDGothicNeoL.ttf b/src/static/fonts/AppleSDGothicNeoL.ttf new file mode 100644 index 0000000..ec189fb Binary files /dev/null and b/src/static/fonts/AppleSDGothicNeoL.ttf differ diff --git a/src/static/fonts/AppleSDGothicNeoM.ttf b/src/static/fonts/AppleSDGothicNeoM.ttf new file mode 100644 index 0000000..b6eed02 Binary files /dev/null and b/src/static/fonts/AppleSDGothicNeoM.ttf differ diff --git a/src/static/fonts/AppleSDGothicNeoR.ttf b/src/static/fonts/AppleSDGothicNeoR.ttf new file mode 100644 index 0000000..4ab04da Binary files /dev/null and b/src/static/fonts/AppleSDGothicNeoR.ttf differ diff --git a/src/static/fonts/AppleSDGothicNeoSB.ttf b/src/static/fonts/AppleSDGothicNeoSB.ttf new file mode 100644 index 0000000..eb8017e Binary files /dev/null and b/src/static/fonts/AppleSDGothicNeoSB.ttf differ diff --git a/src/static/fonts/AppleSDGothicNeoT.ttf b/src/static/fonts/AppleSDGothicNeoT.ttf new file mode 100644 index 0000000..9b137fd Binary files /dev/null and b/src/static/fonts/AppleSDGothicNeoT.ttf differ diff --git a/src/static/fonts/AppleSDGothicNeoUL.ttf b/src/static/fonts/AppleSDGothicNeoUL.ttf new file mode 100644 index 0000000..7576e3d Binary files /dev/null and b/src/static/fonts/AppleSDGothicNeoUL.ttf differ diff --git a/src/styles/App.css b/src/styles/App.css index b5c61c9..6781a3e 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -1,3 +1,27 @@ @tailwind base; @tailwind components; @tailwind utilities; + +html { + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background-color: #ddd; +} + +body { + width: 390px; + height: 100vh; + background: #fff; +} + +.page-wrapper { + width: 390px; + height: 844px; + position: absolute; + top: 50%; + transform: translateY(-50%); + overflow: hidden; + background: #fff; +} diff --git a/tailwind.config.js b/tailwind.config.js index 0171921..6c1b2e9 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,6 +4,17 @@ module.exports = { content: ['./src/**/*.{js,jsx,ts,tsx}'], theme: { extend: {}, + colors: { + white: '#FFFFFF', + lightGray: '#F0F0F0', + hexGray: '#F2F2F2', + gray: '#D9D9D9', + darkGray: '#A4A4A4', + deepGray: '#9D9D9D', + smokeGray: '#6B6B6B', + mainColor: '#39B54A', + orange: '#FF6B00', + }, }, plugins: [], };