diff --git a/package-lock.json b/package-lock.json
index 43e807c..e50181a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,9 +11,19 @@
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^13.5.0",
+ "@types/jest": "^27.4.1",
+ "@types/node": "^17.0.31",
+ "@types/react": "^18.0.8",
+ "@types/react-dom": "^18.0.3",
+ "@types/styled-components": "^5.1.25",
"react": "^18.1.0",
"react-dom": "^18.1.0",
+ "react-icons": "^4.3.1",
+ "react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
+ "recoil": "^0.7.2",
+ "styled-components": "^5.3.5",
+ "typescript": "^4.6.4",
"web-vitals": "^2.1.4"
}
},
@@ -1972,6 +1982,29 @@
"postcss": "^8.3"
}
},
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.2.tgz",
+ "integrity": "sha512-3QnhqeL+WW88YjYbQL5gUIkthuMw7a0NGbZ7wfFVk2kg/CK5w8w5FFa0RzWjyY1+sujN0NWbtSHH6OJmWHtJpQ==",
+ "dependencies": {
+ "@emotion/memoize": "^0.7.4"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz",
+ "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ=="
+ },
+ "node_modules/@emotion/stylis": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
+ "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
+ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
+ },
"node_modules/@eslint/eslintrc": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz",
@@ -3515,6 +3548,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",
@@ -3574,9 +3616,9 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
},
"node_modules/@types/node": {
- "version": "17.0.30",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.30.tgz",
- "integrity": "sha512-oNBIZjIqyHYP8VCNAV9uEytXVeXG2oR0w9lgAXro20eugRQfY002qr3CUl6BAe+Yf/z3CRjPdz27Pu6WWtuSRw=="
+ "version": "17.0.31",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz",
+ "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q=="
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
@@ -3674,6 +3716,16 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
},
+ "node_modules/@types/styled-components": {
+ "version": "5.1.25",
+ "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.25.tgz",
+ "integrity": "sha512-fgwl+0Pa8pdkwXRoVPP9JbqF0Ivo9llnmsm+7TCI330kbPIFd9qv1Lrhr37shf4tnxCOSu+/IgqM7uJXLWZZNQ==",
+ "dependencies": {
+ "@types/hoist-non-react-statics": "*",
+ "@types/react": "*",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/@types/testing-library__jest-dom": {
"version": "5.14.3",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz",
@@ -4702,6 +4754,26 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/babel-plugin-styled-components": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz",
+ "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.16.0",
+ "@babel/helper-module-imports": "^7.16.0",
+ "babel-plugin-syntax-jsx": "^6.18.0",
+ "lodash": "^4.17.11",
+ "picomatch": "^2.3.0"
+ },
+ "peerDependencies": {
+ "styled-components": ">= 2"
+ }
+ },
+ "node_modules/babel-plugin-syntax-jsx": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
+ "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
+ },
"node_modules/babel-plugin-transform-react-remove-prop-types": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz",
@@ -5016,6 +5088,11 @@
"node": ">= 6"
}
},
+ "node_modules/camelize": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
+ "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
+ },
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -5482,6 +5559,14 @@
"postcss": "^8.4"
}
},
+ "node_modules/css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/css-declaration-sorter": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.2.tgz",
@@ -5663,6 +5748,16 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
+ "node_modules/css-to-react-native": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
+ "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
+ "dependencies": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"node_modules/css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -7909,6 +8004,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/hamt_plus": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
+ "integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE="
+ },
"node_modules/handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@@ -7990,6 +8090,27 @@
"he": "bin/he"
}
},
+ "node_modules/history": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
+ "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.7.6"
+ }
+ },
+ "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",
@@ -13406,6 +13527,14 @@
"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.3.1",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz",
+ "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -13419,6 +13548,30 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
+ "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
+ "dependencies": {
+ "history": "^5.2.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
+ "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
+ "dependencies": {
+ "history": "^5.2.0",
+ "react-router": "6.3.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -13515,6 +13668,25 @@
"node": ">=8.10.0"
}
},
+ "node_modules/recoil": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.2.tgz",
+ "integrity": "sha512-OT4pI7FOUHcIoRtjsL5Lqq+lFFzQfir4MIbUkqyJ3nqv3WfBP1pHepyurqTsK5gw+T+I2R8+uOD28yH+Lg5o4g==",
+ "dependencies": {
+ "hamt_plus": "1.0.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/recursive-readdir": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",
@@ -14172,6 +14344,11 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -14569,6 +14746,36 @@
"webpack": "^5.0.0"
}
},
+ "node_modules/styled-components": {
+ "version": "5.3.5",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz",
+ "integrity": "sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.0.0",
+ "@babel/traverse": "^7.4.5",
+ "@emotion/is-prop-valid": "^1.1.0",
+ "@emotion/stylis": "^0.8.4",
+ "@emotion/unitless": "^0.7.4",
+ "babel-plugin-styled-components": ">= 1.12.0",
+ "css-to-react-native": "^3.0.0",
+ "hoist-non-react-statics": "^3.0.0",
+ "shallowequal": "^1.1.0",
+ "supports-color": "^5.5.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0",
+ "react-is": ">= 16.8.0"
+ }
+ },
"node_modules/stylehacks": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.0.tgz",
@@ -15131,7 +15338,6 @@
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -17516,6 +17722,29 @@
"postcss-value-parser": "^4.2.0"
}
},
+ "@emotion/is-prop-valid": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.2.tgz",
+ "integrity": "sha512-3QnhqeL+WW88YjYbQL5gUIkthuMw7a0NGbZ7wfFVk2kg/CK5w8w5FFa0RzWjyY1+sujN0NWbtSHH6OJmWHtJpQ==",
+ "requires": {
+ "@emotion/memoize": "^0.7.4"
+ }
+ },
+ "@emotion/memoize": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz",
+ "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ=="
+ },
+ "@emotion/stylis": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
+ "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
+ },
+ "@emotion/unitless": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
+ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
+ },
"@eslint/eslintrc": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz",
@@ -18620,6 +18849,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",
@@ -18679,9 +18917,9 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
},
"@types/node": {
- "version": "17.0.30",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.30.tgz",
- "integrity": "sha512-oNBIZjIqyHYP8VCNAV9uEytXVeXG2oR0w9lgAXro20eugRQfY002qr3CUl6BAe+Yf/z3CRjPdz27Pu6WWtuSRw=="
+ "version": "17.0.31",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz",
+ "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q=="
},
"@types/parse-json": {
"version": "4.0.0",
@@ -18779,6 +19017,16 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
},
+ "@types/styled-components": {
+ "version": "5.1.25",
+ "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.25.tgz",
+ "integrity": "sha512-fgwl+0Pa8pdkwXRoVPP9JbqF0Ivo9llnmsm+7TCI330kbPIFd9qv1Lrhr37shf4tnxCOSu+/IgqM7uJXLWZZNQ==",
+ "requires": {
+ "@types/hoist-non-react-statics": "*",
+ "@types/react": "*",
+ "csstype": "^3.0.2"
+ }
+ },
"@types/testing-library__jest-dom": {
"version": "5.14.3",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz",
@@ -19520,6 +19768,23 @@
"@babel/helper-define-polyfill-provider": "^0.3.1"
}
},
+ "babel-plugin-styled-components": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz",
+ "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==",
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.16.0",
+ "@babel/helper-module-imports": "^7.16.0",
+ "babel-plugin-syntax-jsx": "^6.18.0",
+ "lodash": "^4.17.11",
+ "picomatch": "^2.3.0"
+ }
+ },
+ "babel-plugin-syntax-jsx": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
+ "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
+ },
"babel-plugin-transform-react-remove-prop-types": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz",
@@ -19765,6 +20030,11 @@
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
},
+ "camelize": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
+ "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
+ },
"caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -20120,6 +20390,11 @@
"postcss-selector-parser": "^6.0.9"
}
},
+ "css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
+ },
"css-declaration-sorter": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.2.tgz",
@@ -20227,6 +20502,16 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
+ "css-to-react-native": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
+ "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
+ "requires": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -21857,6 +22142,11 @@
"duplexer": "^0.1.2"
}
},
+ "hamt_plus": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
+ "integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE="
+ },
"handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@@ -21911,6 +22201,29 @@
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
+ "history": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
+ "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
+ "requires": {
+ "@babel/runtime": "^7.7.6"
+ }
+ },
+ "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",
@@ -25692,6 +26005,12 @@
"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.3.1",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz",
+ "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==",
+ "requires": {}
+ },
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -25702,6 +26021,23 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
},
+ "react-router": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
+ "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
+ "requires": {
+ "history": "^5.2.0"
+ }
+ },
+ "react-router-dom": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
+ "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
+ "requires": {
+ "history": "^5.2.0",
+ "react-router": "6.3.0"
+ }
+ },
"react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -25775,6 +26111,14 @@
"picomatch": "^2.2.1"
}
},
+ "recoil": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.2.tgz",
+ "integrity": "sha512-OT4pI7FOUHcIoRtjsL5Lqq+lFFzQfir4MIbUkqyJ3nqv3WfBP1pHepyurqTsK5gw+T+I2R8+uOD28yH+Lg5o4g==",
+ "requires": {
+ "hamt_plus": "1.0.2"
+ }
+ },
"recursive-readdir": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",
@@ -26254,6 +26598,11 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -26553,6 +26902,23 @@
"integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==",
"requires": {}
},
+ "styled-components": {
+ "version": "5.3.5",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz",
+ "integrity": "sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==",
+ "requires": {
+ "@babel/helper-module-imports": "^7.0.0",
+ "@babel/traverse": "^7.4.5",
+ "@emotion/is-prop-valid": "^1.1.0",
+ "@emotion/stylis": "^0.8.4",
+ "@emotion/unitless": "^0.7.4",
+ "babel-plugin-styled-components": ">= 1.12.0",
+ "css-to-react-native": "^3.0.0",
+ "hoist-non-react-statics": "^3.0.0",
+ "shallowequal": "^1.1.0",
+ "supports-color": "^5.5.0"
+ }
+ },
"stylehacks": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.0.tgz",
@@ -26982,8 +27348,7 @@
"typescript": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
- "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
- "peer": true
+ "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg=="
},
"unbox-primitive": {
"version": "1.0.2",
diff --git a/package.json b/package.json
index 7f031bc..e7b1fd1 100644
--- a/package.json
+++ b/package.json
@@ -6,9 +6,19 @@
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^13.5.0",
+ "@types/jest": "^27.4.1",
+ "@types/node": "^17.0.31",
+ "@types/react": "^18.0.8",
+ "@types/react-dom": "^18.0.3",
+ "@types/styled-components": "^5.1.25",
"react": "^18.1.0",
"react-dom": "^18.1.0",
+ "react-icons": "^4.3.1",
+ "react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
+ "recoil": "^0.7.2",
+ "styled-components": "^5.3.5",
+ "typescript": "^4.6.4",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/public/images/DAY6.jpg b/public/images/DAY6.jpg
new file mode 100644
index 0000000..b696410
Binary files /dev/null and b/public/images/DAY6.jpg differ
diff --git a/public/images/backbutton.png b/public/images/backbutton.png
new file mode 100644
index 0000000..381518c
Binary files /dev/null and b/public/images/backbutton.png differ
diff --git a/public/images/colde.jpg b/public/images/colde.jpg
new file mode 100644
index 0000000..9a780c3
Binary files /dev/null and b/public/images/colde.jpg differ
diff --git a/public/images/damons year.jpg b/public/images/damons year.jpg
new file mode 100644
index 0000000..78ee21c
Binary files /dev/null and b/public/images/damons year.jpg differ
diff --git a/public/images/ducky.jpg b/public/images/ducky.jpg
new file mode 100644
index 0000000..78f445f
Binary files /dev/null and b/public/images/ducky.jpg differ
diff --git a/public/images/ghost.jpg b/public/images/ghost.jpg
new file mode 100644
index 0000000..493304b
Binary files /dev/null and b/public/images/ghost.jpg differ
diff --git a/public/images/ghost.png b/public/images/ghost.png
new file mode 100644
index 0000000..493304b
Binary files /dev/null and b/public/images/ghost.png differ
diff --git a/public/images/joan.jpg b/public/images/joan.jpg
new file mode 100644
index 0000000..d060f41
Binary files /dev/null and b/public/images/joan.jpg differ
diff --git a/public/images/keshi.jpg b/public/images/keshi.jpg
new file mode 100644
index 0000000..a5756fe
Binary files /dev/null and b/public/images/keshi.jpg differ
diff --git a/public/images/kitty.jpg b/public/images/kitty.jpg
new file mode 100644
index 0000000..0bf33c0
Binary files /dev/null and b/public/images/kitty.jpg differ
diff --git a/public/images/krkorklo.jpg b/public/images/krkorklo.jpg
new file mode 100644
index 0000000..34ecf02
Binary files /dev/null and b/public/images/krkorklo.jpg differ
diff --git a/public/images/nell.jpg b/public/images/nell.jpg
new file mode 100644
index 0000000..13e8d72
Binary files /dev/null and b/public/images/nell.jpg differ
diff --git a/public/images/send.png b/public/images/send.png
new file mode 100644
index 0000000..9dfae13
Binary files /dev/null and b/public/images/send.png differ
diff --git a/public/images/taeng_ss.jpg b/public/images/taeng_ss.jpg
new file mode 100644
index 0000000..2015aa7
Binary files /dev/null and b/public/images/taeng_ss.jpg differ
diff --git a/public/index.html b/public/index.html
index a0b8d37..6e0027d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -9,9 +9,11 @@
name="description"
content="Web site created using create-react-app"
/>
-
-
-
React App
+
+ Messenger
diff --git a/public/logo192.png b/public/logo192.png
deleted file mode 100644
index fc44b0a..0000000
Binary files a/public/logo192.png and /dev/null differ
diff --git a/public/logo512.png b/public/logo512.png
deleted file mode 100644
index a4e47a6..0000000
Binary files a/public/logo512.png and /dev/null differ
diff --git a/src/App.js b/src/App.js
deleted file mode 100644
index 772afaf..0000000
--- a/src/App.js
+++ /dev/null
@@ -1,5 +0,0 @@
-function App() {
- return 15๊ธฐ ํ์ดํ
๐
;
-}
-
-export default App;
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..5a378da
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,65 @@
+import styled, { css } from "styled-components";
+import { useRecoilState } from "recoil";
+import { resizeState } from "recoil/recoil";
+import { useEffect } from "react";
+import { Route, Routes } from "react-router-dom";
+import { ChatList, ChatFriends, ChatRoom, Setting } from "pages";
+
+const App = () => {
+ const [isMobile, setIsMobile] = useRecoilState(resizeState);
+
+ const _handleResize = () => {
+ if (window.innerWidth <= 640) {
+ setIsMobile(true);
+ } else {
+ setIsMobile(false);
+ }
+ };
+
+ useEffect(() => {
+ if (window.innerWidth <= 640) {
+ setIsMobile(true);
+ }
+
+ window.addEventListener("resize", _handleResize);
+ return () => {
+ window.removeEventListener("resize", _handleResize);
+ };
+ }, []);
+
+ return (
+
+
+
+ } />
+ } />
+ } />
+ } />
+
+
+
+ );
+};
+
+const Wrapper = styled.div`
+ height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+const Container = styled.div<{ isMobile: boolean }>`
+ border: 0.05rem solid lightgrey;
+ ${({ isMobile }) =>
+ isMobile
+ ? css`
+ height: 100%;
+ width: 100%;
+ `
+ : css`
+ height: 640px;
+ width: 360px;
+ border-radius: 0.5rem;
+ `}
+`;
+
+export default App;
diff --git a/src/components/chatFriends/ChatFriendsList.tsx b/src/components/chatFriends/ChatFriendsList.tsx
new file mode 100644
index 0000000..4d8a390
--- /dev/null
+++ b/src/components/chatFriends/ChatFriendsList.tsx
@@ -0,0 +1,55 @@
+import styled from "styled-components";
+import user from "data/user.json";
+import List from "components/layout/List";
+import Search from "components/common/Search";
+import { useState } from "react";
+
+const ChatList = () => {
+ const totalUser = user.slice(1);
+ const [showUser, setShowUser] = useState(totalUser);
+
+ const filterUser = (text: string): void => {
+ if (!text.trim()) {
+ setShowUser(totalUser);
+ } else {
+ const filtered = totalUser.filter((user) =>
+ user.name.toLowerCase().includes(text.trim().toLowerCase())
+ );
+ setShowUser(filtered);
+ }
+ };
+
+ return (
+
+
+ {showUser.map((user) => (
+
+ ))}
+
+ );
+};
+
+const Container = styled.section`
+ display: flex;
+ flex-direction: column;
+ height: 67%;
+ overflow: auto;
+ padding-top: 1rem;
+ ::-webkit-scrollbar {
+ width: 0.9rem;
+ }
+ ::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 1rem;
+ background-clip: padding-box;
+ border: 0.3rem solid transparent;
+ }
+`;
+
+export default ChatList;
diff --git a/src/components/chatList/MessageChatList.tsx b/src/components/chatList/MessageChatList.tsx
new file mode 100644
index 0000000..713e70f
--- /dev/null
+++ b/src/components/chatList/MessageChatList.tsx
@@ -0,0 +1,66 @@
+import List from "components/layout/List";
+import message from "data/message.json";
+import styled from "styled-components";
+import { useState } from "react";
+import Search from "components/common/Search";
+import { IUserType } from "interface";
+
+const MessageChatList = () => {
+ const [showMessage, setShowMessage] = useState(message);
+
+ const filterMessage = (text: string): void => {
+ if (!text.trim()) {
+ setShowMessage(message);
+ } else {
+ const filtered = message.filter(
+ (msg) =>
+ msg.user.filter((user) =>
+ user.name.toLowerCase().includes(text.trim().toLowerCase())
+ ).length !== 0
+ );
+ setShowMessage(filtered);
+ }
+ };
+ const _handleFriendsName = (friends: IUserType[]): string => {
+ return friends.length === 1
+ ? friends[0].name
+ : friends.map((user) => user.name).join(", ");
+ };
+
+ return (
+
+
+ {showMessage.map(
+ (msg) =>
+ msg.messages.length !== 0 && (
+
+ )
+ )}
+
+ );
+};
+
+const Container = styled.section`
+ display: flex;
+ flex-direction: column;
+ height: 67%;
+ overflow: auto;
+ padding-top: 1rem;
+ ::-webkit-scrollbar {
+ width: 0.9rem;
+ }
+ ::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 1rem;
+ background-clip: padding-box;
+ border: 0.3rem solid transparent;
+ }
+`;
+
+export default MessageChatList;
diff --git a/src/components/common/Alert.tsx b/src/components/common/Alert.tsx
new file mode 100644
index 0000000..f31b494
--- /dev/null
+++ b/src/components/common/Alert.tsx
@@ -0,0 +1,76 @@
+import styled from "styled-components";
+import { IAlert } from "interface";
+
+const Alert = ({ setVisibleAlert, value }: IAlert) => {
+ const _handleClick = (): void => {
+ setVisibleAlert(false);
+ };
+
+ return (
+
+
+
+
+
+ X
+
+
+ {value}
+
+
+ );
+};
+
+const Wrapper = styled.section`
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 100;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ animation: alert-show 0.2s;
+ @keyframes alert-show {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+`;
+const Background = styled.div`
+ background: #00000020;
+ width: 100%;
+ height: 100%;
+`;
+const Container = styled.section`
+ position: absolute;
+ width: 25rem;
+ height: 9rem;
+ background: #ffffff;
+ border-radius: 0.4rem;
+`;
+const AlertContent = styled.section`
+ margin: 1.8rem;
+ font-size: 1.1rem;
+`;
+const AlertButtonBox = styled.section`
+ display: flex;
+ justify-content: flex-end;
+ margin: 1.5rem;
+`;
+const AlertButton = styled.button`
+ border: none;
+ background: none;
+ font-size: 1.2rem;
+ cursor: pointer;
+ :hover {
+ opacity: 0.5;
+ transition: 0.15s;
+ }
+`;
+
+export default Alert;
diff --git a/src/components/common/Search.tsx b/src/components/common/Search.tsx
new file mode 100644
index 0000000..152f0a6
--- /dev/null
+++ b/src/components/common/Search.tsx
@@ -0,0 +1,50 @@
+import styled from "styled-components";
+import { ISearch } from "interface";
+import { AiOutlineSearch } from "react-icons/ai";
+import { useRecoilValue } from "recoil";
+import { resizeState } from "recoil/recoil";
+import useInput from "hooks/useInput";
+import { useEffect } from "react";
+
+const Search = ({ filter }: ISearch) => {
+ const { text, handleTextChange } = useInput("");
+ const isMobile = useRecoilValue(resizeState);
+
+ useEffect(() => {
+ filter(text);
+ }, [filter, text]);
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+const Container = styled.section`
+ display: flex;
+`;
+const IconBox = styled.section`
+ margin: 0.3rem 0.5rem 0 1rem;
+`;
+const SearchInput = styled.input<{ isMobile: boolean }>`
+ border: 0.05rem solid lightgrey;
+ border-radius: 0.5rem;
+ padding: 0.5rem;
+ width: ${({ isMobile }) => (isMobile ? "85%" : "17rem")};
+ height: 1rem;
+ font-size: 0.7rem;
+ :focus {
+ outline: none;
+ }
+`;
+
+export default Search;
diff --git a/src/components/layout/ChatFooter.tsx b/src/components/layout/ChatFooter.tsx
new file mode 100644
index 0000000..f39a614
--- /dev/null
+++ b/src/components/layout/ChatFooter.tsx
@@ -0,0 +1,39 @@
+import styled from "styled-components";
+import { Link } from "react-router-dom";
+import { AiOutlineUser, AiOutlineSetting } from "react-icons/ai";
+import { BiMessage } from "react-icons/bi";
+
+const ChatFooter = ({ path }: { path: string }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const Container = styled.section`
+ border-top: 0.05rem solid lightgrey;
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+`;
+const IconBox = styled(Link)<{ selected: boolean }>`
+ display: flex;
+ justify-content: center;
+ margin: 1.3rem;
+ color: ${({ selected }) => (selected ? "#1986fc" : "#000000")};
+ cursor: pointer;
+ :hover {
+ opacity: 0.7;
+ transition: 0.15s;
+ }
+`;
+
+export default ChatFooter;
diff --git a/src/components/layout/ChatHeader.tsx b/src/components/layout/ChatHeader.tsx
new file mode 100644
index 0000000..05dace3
--- /dev/null
+++ b/src/components/layout/ChatHeader.tsx
@@ -0,0 +1,22 @@
+import styled from "styled-components";
+
+const ChatHeader = ({ title }: { title: string }) => {
+ return (
+
+ {title}
+
+ );
+};
+
+const Container = styled.section`
+ height: 19%;
+ border-bottom: 0.05rem solid lightgrey;
+ display: flex;
+ align-items: center;
+`;
+const Title = styled.p`
+ font-size: 1.5rem;
+ margin: 1.5rem;
+`;
+
+export default ChatHeader;
diff --git a/src/components/layout/List.tsx b/src/components/layout/List.tsx
new file mode 100644
index 0000000..b0426ed
--- /dev/null
+++ b/src/components/layout/List.tsx
@@ -0,0 +1,58 @@
+import styled from "styled-components";
+import { Link } from "react-router-dom";
+import { IList } from "interface";
+
+const List = ({ link, img, title, subTitle }: IList) => {
+ return (
+
+
+
+
+
+ {title}
+ {subTitle}
+
+
+ );
+};
+
+const ListContent = styled.section`
+ margin: 1rem;
+`;
+const ListItem = styled(Link)`
+ display: flex;
+ height: 5rem;
+ text-decoration: none;
+ cursor: pointer;
+ :hover {
+ ${ListContent} {
+ opacity: 0.7;
+ transition: 0.15s;
+ }
+ }
+`;
+const ListTitle = styled.p`
+ margin-top: 0;
+ margin-bottom: 0.5rem;
+ color: black;
+`;
+const ListSubTitle = styled.p`
+ margin-top: 0;
+ font-size: 0.7rem;
+ display: block;
+ width: 15rem;
+ color: grey;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
+const ProfileImgBox = styled.section`
+ display: flex;
+ align-items: center;
+`;
+const ProfileImg = styled.img`
+ border-radius: 70%;
+ margin-left: 1rem;
+`;
+
+export default List;
diff --git a/src/components/messageChatRoom/MessageBallon.tsx b/src/components/messageChatRoom/MessageBallon.tsx
new file mode 100644
index 0000000..885111e
--- /dev/null
+++ b/src/components/messageChatRoom/MessageBallon.tsx
@@ -0,0 +1,79 @@
+import { IMessageType } from "interface";
+import { useRecoilValue } from "recoil";
+import { chatRoomState } from "recoil/recoil";
+import styled, { css } from "styled-components";
+
+const MessageBallon = ({ message }: { message: IMessageType }) => {
+ const { currentUser } = useRecoilValue(chatRoomState);
+ const isUser = message.user.id === currentUser.id;
+
+ return (
+
+
+
+ {message.user.name}
+ {message.text}
+
+ {message.time}
+
+ );
+};
+
+const MessageBox = styled.section<{ isUser: boolean }>`
+ display: flex;
+ flex-direction: ${({ isUser }) => isUser && "row-reverse"};
+ padding-bottom: 1rem;
+`;
+const MessageText = styled.p<{ isUser: boolean }>`
+ padding: 0.65rem;
+ font-size: 0.75rem;
+ margin: 0.4rem;
+ ${({ isUser }) =>
+ isUser
+ ? css`
+ border-radius: 0.5rem 0 0.5rem 0.5rem;
+ background-color: #1986fc;
+ color: #ffffff;
+ `
+ : css`
+ border-radius: 0 0.5rem 0.5rem 0.5rem;
+ background-color: #f1f1f3;
+ `};
+`;
+const MessageUser = styled.p<{ isUser: boolean }>`
+ font-size: 0.75rem;
+ margin: 0;
+ display: flex;
+ ${({ isUser }) =>
+ isUser
+ ? css`
+ justify-content: flex-end;
+ margin-right: 0.5rem;
+ `
+ : css`
+ margin-left: 0.5rem;
+ `}
+`;
+const MessageImg = styled.img`
+ border-radius: 70%;
+`;
+const MessageTime = styled.p<{ isUser: boolean }>`
+ font-size: 0.5rem;
+ color: lightgrey;
+ display: flex;
+ align-items: flex-end;
+ ${({ isUser }) =>
+ isUser
+ ? css`
+ margin-left: 0.5rem;
+ `
+ : css`
+ margin-right: 0.5rem;
+ `}
+`;
+
+export default MessageBallon;
diff --git a/src/components/messageChatRoom/MessageChat.tsx b/src/components/messageChatRoom/MessageChat.tsx
new file mode 100644
index 0000000..273e3c9
--- /dev/null
+++ b/src/components/messageChatRoom/MessageChat.tsx
@@ -0,0 +1,57 @@
+import styled from "styled-components";
+import MessageBallon from "./MessageBallon";
+import { useEffect, useRef } from "react";
+import { useRecoilValue } from "recoil";
+import { chatRoomState } from "recoil/recoil";
+
+const MessageChat = () => {
+ const messageWrapperRef = useRef(null);
+ const { message, currentUser } = useRecoilValue(chatRoomState);
+
+ const _scrollToBottom = (): void => {
+ if (messageWrapperRef.current) {
+ messageWrapperRef.current.scrollTop =
+ messageWrapperRef.current.scrollHeight;
+ }
+ };
+
+ useEffect(() => {
+ _scrollToBottom();
+ }, [message]);
+
+ return (
+
+ {message.map((msg) => (
+
+
+
+ ))}
+
+ );
+};
+
+const Wrapper = styled.section`
+ display: flex;
+ flex-direction: column;
+ height: 67%;
+ overflow: auto;
+ padding: 1rem 0.8rem 0 0.8rem;
+ ::-webkit-scrollbar {
+ width: 0.9rem;
+ }
+ ::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 1rem;
+ background-clip: padding-box;
+ border: 0.3rem solid transparent;
+ }
+`;
+const MessageBallonContainer = styled.section<{ isUser: boolean }>`
+ display: flex;
+ justify-content: ${({ isUser }) => isUser && "flex-end"};
+`;
+
+export default MessageChat;
diff --git a/src/components/messageInput/EmoticonPopover.tsx b/src/components/messageInput/EmoticonPopover.tsx
new file mode 100644
index 0000000..898a49d
--- /dev/null
+++ b/src/components/messageInput/EmoticonPopover.tsx
@@ -0,0 +1,53 @@
+import styled from "styled-components";
+import React from "react";
+import { IEmoticonPopover } from "interface";
+
+const emoticonList = ["โค๏ธ", "๐", "๐", "โจ", "๐"];
+
+const EmoticonPopover = ({
+ addEmoticonMessage,
+ handlePopover,
+}: IEmoticonPopover) => {
+ // ์ด๋ชจํฐ์ฝ ์ ์ก
+ const _handleClick = (e: React.MouseEvent): void => {
+ const target = e.target as HTMLElement;
+ addEmoticonMessage(target.innerText);
+ };
+
+ return (
+
+ {emoticonList.map((emo, idx) => (
+
+ {emo}
+
+ ))}
+
+ );
+};
+
+const PopoverContainer = styled.section`
+ position: absolute;
+ transform: translate(0, -120%);
+ background-color: #f1f1f3;
+ display: flex;
+ padding: 0.6rem;
+ border-radius: 0.5rem;
+ :before {
+ border-color: #f1f1f3 transparent;
+ border-style: solid;
+ border-width: 0.5rem 0.5rem 0 0.5rem;
+ content: "";
+ display: block;
+ left: 1.15rem;
+ position: absolute;
+ bottom: -0.5rem;
+ }
+`;
+const EmoticonBox = styled.section`
+ cursor: pointer;
+ & + & {
+ margin-left: 0.7rem;
+ }
+`;
+
+export default EmoticonPopover;
diff --git a/src/components/messageInput/InputEmoticon.tsx b/src/components/messageInput/InputEmoticon.tsx
new file mode 100644
index 0000000..fcb8d58
--- /dev/null
+++ b/src/components/messageInput/InputEmoticon.tsx
@@ -0,0 +1,32 @@
+import { IInputEmoticon } from "interface";
+import EmoticonPopover from "./EmoticonPopover";
+import styled from "styled-components";
+import React, { useState } from "react";
+
+const InputEmoticon = ({ addEmoticonMessage }: IInputEmoticon) => {
+ const [visiblePopover, setVisiblePopover] = useState(false);
+
+ const handlePopover = (): void => {
+ setVisiblePopover(!visiblePopover);
+ };
+
+ return (
+
+ {visiblePopover && (
+
+ )}
+ โค๏ธ
+
+ );
+};
+
+const Emoticon = styled.section`
+ width: 2rem;
+ margin-left: 1rem;
+ cursor: pointer;
+`;
+
+export default React.memo(InputEmoticon);
diff --git a/src/components/messageInput/InputMessageForm.tsx b/src/components/messageInput/InputMessageForm.tsx
new file mode 100644
index 0000000..fb651ec
--- /dev/null
+++ b/src/components/messageInput/InputMessageForm.tsx
@@ -0,0 +1,86 @@
+import useInput from "hooks/useInput";
+import React, { useState, useCallback } from "react";
+import styled from "styled-components";
+import { useRecoilValue } from "recoil";
+import { resizeState } from "recoil/recoil";
+import InputEmoticon from "./InputEmoticon";
+import { IInputMessageForm } from "interface";
+import Alert from "components/common/Alert";
+
+const InputMessageForm = ({ sendMessage }: IInputMessageForm) => {
+ const { text, handleTextChange, resetText } = useInput("");
+ const isMobile = useRecoilValue(resizeState);
+ const [visibleAlert, setVisibleAlert] = useState(false);
+
+ // ๋ฉ์์ง ์ ์ก
+ const _addInputMessage = (e: React.FormEvent): void => {
+ e.preventDefault();
+
+ if (!text.trim()) setVisibleAlert(true);
+ else {
+ sendMessage(text);
+ }
+ resetText();
+ };
+
+ // ์ด๋ชจํฐ์ฝ ์ ์ก
+ const addEmoticonMessage = useCallback(
+ (emo: string): void => {
+ sendMessage(emo);
+ resetText();
+ },
+ [sendMessage],
+ );
+
+ return (
+
+ {visibleAlert && (
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+const InputForm = styled.form`
+ border-top: 0.05rem solid lightgrey;
+ display: flex;
+ align-items: center;
+ padding-top: 1rem;
+`;
+const InputText = styled.input<{ isMobile: boolean }>`
+ border: 0.05rem solid lightgrey;
+ border-radius: 0.7rem;
+ padding: 0.7rem;
+ width: ${({ isMobile }) => (isMobile ? "78%" : "230px")};
+ height: 1rem;
+ font-size: 0.75rem;
+ :focus {
+ outline: none;
+ }
+`;
+const InputButton = styled.button`
+ background: none;
+ border: none;
+ margin-left: 0.7rem;
+ cursor: pointer;
+ padding-top: 0.5rem;
+ :hover {
+ opacity: 0.7;
+ transition: 0.15s;
+ }
+`;
+
+export default InputMessageForm;
diff --git a/src/components/messageProfile/MessageProfile.tsx b/src/components/messageProfile/MessageProfile.tsx
new file mode 100644
index 0000000..f859b97
--- /dev/null
+++ b/src/components/messageProfile/MessageProfile.tsx
@@ -0,0 +1,54 @@
+import styled from "styled-components";
+import useChatRoom from "hooks/useChatRoom";
+import { useParams } from "react-router-dom";
+import { useRecoilValue } from "recoil";
+import { chatRoomState } from "recoil/recoil";
+
+const MessageProfile = () => {
+ const { id } = useParams();
+ const { toggleUser } = useChatRoom();
+ const { currentUser } = useRecoilValue(chatRoomState);
+
+ const _handleUser = (): void => {
+ toggleUser(Number(id));
+ };
+
+ return (
+
+
+
+ {currentUser.name}
+ Typing...
+
+
+ );
+};
+
+const Profile = styled.section`
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+`;
+const ProfileImg = styled.img`
+ border-radius: 70%;
+`;
+const ProfileContent = styled.section`
+ display: flex;
+ flex-direction: column;
+ margin-left: 0.8rem;
+`;
+const ProfileName = styled.p`
+ margin: 0;
+ font-size: 0.9rem;
+`;
+const ProfileTyping = styled.p`
+ font-size: 0.7rem;
+ color: lightgrey;
+ margin-top: 0.3rem;
+`;
+
+export default MessageProfile;
diff --git a/src/components/messageProfile/MessageProfileContainer.tsx b/src/components/messageProfile/MessageProfileContainer.tsx
new file mode 100644
index 0000000..fc3292b
--- /dev/null
+++ b/src/components/messageProfile/MessageProfileContainer.tsx
@@ -0,0 +1,39 @@
+import styled from "styled-components";
+import { Link } from "react-router-dom";
+import MessageProfile from "./MessageProfile";
+
+const MessageProfileContainer = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+const Container = styled.section`
+ height: 19%;
+ border-bottom: 0.05rem solid lightgrey;
+ display: grid;
+ grid-template-columns: 1fr 8rem 1fr;
+`;
+const BackButton = styled(Link)`
+ display: flex;
+ align-items: center;
+ margin-left: 2rem;
+`;
+const BackButtonImg = styled.img`
+ cursor: pointer;
+ :hover {
+ opacity: 0.7;
+ transition: 0.15s;
+ }
+`;
+
+export default MessageProfileContainer;
diff --git a/src/components/setting/SettingList.tsx b/src/components/setting/SettingList.tsx
new file mode 100644
index 0000000..ec0bedd
--- /dev/null
+++ b/src/components/setting/SettingList.tsx
@@ -0,0 +1,43 @@
+import styled from "styled-components";
+
+const settings = ["Profile", "Notifications", "General", "Privacy", "Help"];
+
+const SettingList = () => {
+ return (
+
+ {settings.map((set, idx) => (
+ {set}
+ ))}
+
+ );
+};
+
+const Container = styled.section`
+ display: flex;
+ flex-direction: column;
+ height: 67%;
+ overflow: auto;
+ padding-top: 1rem;
+ ::-webkit-scrollbar {
+ width: 0.9rem;
+ }
+ ::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 1rem;
+ background-clip: padding-box;
+ border: 0.3rem solid transparent;
+ }
+`;
+const ListItem = styled.section`
+ display: flex;
+ cursor: pointer;
+ text-decoration: none;
+ margin-left: 2.5rem;
+ padding-top: 2rem;
+ :hover {
+ opacity: 0.7;
+ transition: 0.15s;
+ }
+`;
+
+export default SettingList;
diff --git a/src/data/message.json b/src/data/message.json
new file mode 100644
index 0000000..19ede5b
--- /dev/null
+++ b/src/data/message.json
@@ -0,0 +1,441 @@
+[
+ {
+ "id": 1001,
+ "user": [
+ {
+ "id": 9,
+ "name": "DAY6",
+ "statusMessage": "ํ ํ์ด์ง๊ฐ ๋ ์ ์๊ฒ"
+ },
+ {
+ "id": 10,
+ "name": "ghost",
+ "statusMessage": "์ ๋ น์ ์ง"
+ },
+ {
+ "id": 11,
+ "name": "kitty",
+ "statusMessage": "Meow Meow"
+ }
+ ],
+ "messages": [
+ {
+ "id": 1,
+ "text": "์์งํ ๋งํ ๊ฒ ๋ง์ด ๊ธฐ๋ค๋ ค ์์ด ๋๋ ๊ทธ๋ฌ์ ๊ฑฐ๋ผ ๋ฏฟ์ด",
+ "user": {
+ "id": 10,
+ "name": "ghost",
+ "statusMessage": "์ ๋ น์ ์ง"
+ },
+ "time": "15:08"
+ },
+ {
+ "id": 2,
+ "text": "์ค๋์ด ์ค๊ธธ ๋งค์ผ๊ฐ์ด ๋ฌ๋ ฅ์ ๋ณด๋ฉด์",
+ "user": {
+ "id": 10,
+ "name": "ghost",
+ "statusMessage": "์ ๋ น์ ์ง"
+ },
+ "time": "15:09"
+ },
+ {
+ "id": 3,
+ "text": "์์งํ ๋์๊ฒ๋ ์ง๊ธ ์ด ์๊ฐ์ ๊ฟ๋ง ๊ฐ์ ๋์ ํจ๊ป๋ผ",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "17:08"
+ },
+ {
+ "id": 4,
+ "text": "์ค๋์ ์ํด ๊ฝค ๋ง์ ๊ฑธ ์ค๋นํด ๋ดค์ด",
+ "user": {
+ "id": 11,
+ "name": "kitty",
+ "statusMessage": "Meow Meow"
+ },
+ "time": "17:08"
+ },
+ {
+ "id": 5,
+ "text": "์๋ฆ๋ค์ด ์ฒญ์ถ์ ํ ์ฅ ํจ๊ป ์จ๋ด๋ ค ๊ฐ์",
+ "user": {
+ "id": 9,
+ "name": "DAY6",
+ "statusMessage": "ํ ํ์ด์ง๊ฐ ๋ ์ ์๊ฒ"
+ },
+ "time": "17:09"
+ },
+ {
+ "id": 6,
+ "text": "๋์์ ์ถ์ต๋ค๋ก ๊ฐ๋ ์ฑ์ธ๋",
+ "user": {
+ "id": 9,
+ "name": "DAY6",
+ "statusMessage": "ํ ํ์ด์ง๊ฐ ๋ ์ ์๊ฒ"
+ },
+ "time": "18:10"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "user": [
+ {
+ "id": 2,
+ "name": "ducky",
+ "statusMessage": ""
+ }
+ ],
+ "messages": [
+ {
+ "id": 1,
+ "text": "15๊ธฐ",
+ "user": {
+ "id": 2,
+ "name": "ducky",
+ "statusMessage": "๊ฝฅ"
+ },
+ "time": "11:20"
+ },
+ {
+ "id": 2,
+ "text": "ํ๋ก ํธ",
+ "user": {
+ "id": 2,
+ "name": "ducky",
+ "statusMessage": "๊ฝฅ"
+ },
+ "time": "11:21"
+ },
+ {
+ "id": 3,
+ "text": "์งฑ์ด์งโก",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "11:41"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "user": [
+ {
+ "id": 3,
+ "name": "damons year",
+ "statusMessage": "yours"
+ }
+ ],
+ "messages": [
+ {
+ "id": 1,
+ "text": "๋ด๊ฐ ์์ ์ก์๊ฒ ๋๋ ํ์ ๋นผ๋ ๋ผ",
+ "user": {
+ "id": 3,
+ "name": "damons year",
+ "statusMessage": "yours"
+ },
+ "time": "12:20"
+ },
+ {
+ "id": 2,
+ "text": "๊ทธ์ ๋ณต์ฌ๊ฝ ํ ๊ฑฐ๋ฆด ๊ฑท์",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "12:30"
+ },
+ {
+ "id": 3,
+ "text": "๋์ ๋ง์์ด ๋
น์ ์ฐ๋ฆฌ ๋ฐค์ ํฉ์น๋ฉด",
+ "user": {
+ "id": 3,
+ "name": "damons year",
+ "statusMessage": "yours"
+ },
+ "time": "12:31"
+ },
+ {
+ "id": 4,
+ "text": "๋ฌด๋์ง ๋ฌ์ ์ธ์๋์",
+ "user": {
+ "id": 3,
+ "name": "damons year",
+ "statusMessage": "yours"
+ },
+ "time": "12:31"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "user": [
+ {
+ "id": 4,
+ "name": "colde",
+ "statusMessage": "offonoff"
+ }
+ ],
+ "messages": [
+ {
+ "id": 1,
+ "text": "๋๊ฐ ๋ด๊ฒ ํ๋ ๋ง์ ๋ฒ์ด ๋ผ",
+ "user": {
+ "id": 4,
+ "name": "colde",
+ "statusMessage": "offonoff"
+ },
+ "time": "05:40"
+ },
+ {
+ "id": 2,
+ "text": "๋ชจ๋ ์ง์ผ๋ด์ผ๋ง ๋ ๋์ ๋ง์์ ์ด ์๊ฐ ์๊ธฐ์",
+ "user": {
+ "id": 4,
+ "name": "colde",
+ "statusMessage": "offonoff"
+ },
+ "time": "05:50"
+ },
+ {
+ "id": 3,
+ "text": "๋๋ก ์ธํด ํผ์ด๋ ๊ฝ์ด ๋ด ๋ง์์ ์๋ค ์๊ฐ ์์ง",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "08:12"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "user": [
+ {
+ "id": 5,
+ "name": "keshi",
+ "statusMessage": "right here"
+ }
+ ],
+ "messages": [
+ {
+ "id": 1,
+ "text": "If you need me, I'll be here",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "22:03"
+ },
+ {
+ "id": 2,
+ "text": "Said I'm here",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "22:11"
+ },
+ {
+ "id": 3,
+ "text": "you do not know who to be",
+ "user": {
+ "id": 5,
+ "name": "keshi",
+ "statusMessage": "right here"
+ },
+ "time": "23:20"
+ },
+ {
+ "id": 4,
+ "text": "Then go on and come home to me",
+ "user": {
+ "id": 5,
+ "name": "keshi",
+ "statusMessage": "right here"
+ },
+ "time": "23:20"
+ }
+ ]
+ },
+ {
+ "id": 6,
+ "user": [
+ {
+ "id": 6,
+ "name": "joan",
+ "statusMessage": "good vibe๐"
+ }
+ ],
+ "messages": [
+ {
+ "id": 1,
+ "text": "You took my breath away",
+ "user": {
+ "id": 6,
+ "name": "joan",
+ "statusMessage": "good vibe๐"
+ },
+ "time": "15:45"
+ },
+ {
+ "id": 2,
+ "text": "I just can't believe my eyes",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "15:46"
+ },
+ {
+ "id": 3,
+ "text": "Now you're here, life has never felt so good",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "15:46"
+ }
+ ]
+ },
+ {
+ "id": 7,
+ "user": [
+ {
+ "id": 7,
+ "name": "nell",
+ "statusMessage": ""
+ }
+ ],
+ "messages": [
+ {
+ "id": 1,
+ "text": "๊ฐ๋์ ์์ง๋ ๊ทธ๋ด ์ ์์ ๊ฑฐ๋ผ๊ณ ์๊ฐ์ด ๋ค ๋๊ฐ ์์ด",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "13:36"
+ },
+ {
+ "id": 2,
+ "text": "ํ์ง๋ง ์ด๋ด ๋ค์ ๋๋ ค์์ด ์์๊ณ ๋ง์์ด ๋ฌด๊ฑฐ์์ ธ",
+ "user": {
+ "id": 7,
+ "name": "nell",
+ "statusMessage": ""
+ },
+ "time": "14:46"
+ },
+ {
+ "id": 3,
+ "text": "์ด๋์ ๋ ์
๊ฐ์ ํ์จ์ด ๋งบํ",
+ "user": {
+ "id": 7,
+ "name": "nell",
+ "statusMessage": ""
+ },
+ "time": "14:47"
+ },
+ {
+ "id": 4,
+ "text": "์๊ฐ์ ๋ ์ด๋ฅธ์ด ๋๊ฒ ํ์ง๋ง ๊ฐํด์ง๊ฒ ํ์ง๋ ์์ ๊ฒ ๊ฐ์",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "15:00"
+ }
+ ]
+ },
+ {
+ "id": 8,
+ "user": [
+ {
+ "id": 8,
+ "name": "taeng_ss",
+ "statusMessage": "์ ๋ง ๋๋ฅผ ์ฌ๋ํ์๊น"
+ }
+ ],
+ "messages": [
+ {
+ "id": 1,
+ "text": "์๋ก๋ฅผ ๊ทธ๋ฆฌ์ํ๊ณ ์๋ก๋ฅผ ์ง๊ฒจ์ํ์ง",
+ "user": {
+ "id": 8,
+ "name": "taeng_ss",
+ "statusMessage": "์ ๋ง ๋๋ฅผ ์ฌ๋ํ์๊น"
+ },
+ "time": "15:08"
+ },
+ {
+ "id": 2,
+ "text": "๊ทธ ๊ธด ๋ฎ๊ณผ ๋ฐค๋ค์ด ๋ก์ ๋
น์ฌ๊ธฐ ์ ์",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "15:09"
+ },
+ {
+ "id": 3,
+ "text": "์ฐ๋ฆฌ ๋ค์ ๋ฐ์ง์ด์",
+ "user": {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ "time": "17:08"
+ }
+ ]
+ },
+ {
+ "id": 9,
+ "user": [
+ {
+ "id": 9,
+ "name": "ghost",
+ "statusMessage": ""
+ }
+ ],
+ "messages": []
+ },
+ {
+ "id": 10,
+ "user": [
+ {
+ "id": 10,
+ "name": "kitty",
+ "statusMessage": ""
+ }
+ ],
+ "messages": []
+ },
+ {
+ "id": 11,
+ "user": [
+ {
+ "id": 11,
+ "name": "DAY6",
+ "statusMessage": ""
+ }
+ ],
+ "messages": []
+ }
+]
diff --git a/src/data/user.json b/src/data/user.json
new file mode 100644
index 0000000..04aa508
--- /dev/null
+++ b/src/data/user.json
@@ -0,0 +1,57 @@
+[
+ {
+ "id": 1,
+ "name": "krkorklo",
+ "statusMessage": "๐"
+ },
+ {
+ "id": 2,
+ "name": "ducky",
+ "statusMessage": "๊ฝฅ"
+ },
+ {
+ "id": 3,
+ "name": "damons year",
+ "statusMessage": "yours"
+ },
+ {
+ "id": 4,
+ "name": "colde",
+ "statusMessage": "offonoff"
+ },
+ {
+ "id": 5,
+ "name": "keshi",
+ "statusMessage": "right here"
+ },
+ {
+ "id": 6,
+ "name": "joan",
+ "statusMessage": "good vibe๐"
+ },
+ {
+ "id": 7,
+ "name": "nell",
+ "statusMessage": ""
+ },
+ {
+ "id": 8,
+ "name": "taeng_ss",
+ "statusMessage": "์ ๋ง ๋๋ฅผ ์ฌ๋ํ์๊น"
+ },
+ {
+ "id": 9,
+ "name": "DAY6",
+ "statusMessage": "ํ ํ์ด์ง๊ฐ ๋ ์ ์๊ฒ"
+ },
+ {
+ "id": 10,
+ "name": "ghost",
+ "statusMessage": "์ ๋ น์ ์ง"
+ },
+ {
+ "id": 11,
+ "name": "kitty",
+ "statusMessage": "Meow Meow"
+ }
+]
diff --git a/src/hooks/useChatRoom.tsx b/src/hooks/useChatRoom.tsx
new file mode 100644
index 0000000..be61f8c
--- /dev/null
+++ b/src/hooks/useChatRoom.tsx
@@ -0,0 +1,59 @@
+import { chatRoomState, messageState, userState } from "recoil/recoil";
+import { useRecoilState, useRecoilValue } from "recoil";
+
+const useChatRoom = () => {
+ const messageList = useRecoilValue(messageState);
+ const userStore = useRecoilValue(userState);
+ const [chatState, setChatState] = useRecoilState(chatRoomState);
+
+ // ๋ฉ์์ง ์ถ๊ฐ
+ const addMessage = (text: string): void => {
+ const curTime =
+ String(new Date().getHours()).padStart(2, "0") +
+ ":" +
+ String(new Date().getMinutes()).padStart(2, "0");
+ const messageObj = {
+ id: new Date().valueOf(),
+ user: chatState.currentUser,
+ time: curTime,
+ text: text,
+ };
+ setChatState({ ...chatState, message: [...chatState.message, messageObj] });
+ };
+
+ // user ํ ๊ธ ํจ์
+ const toggleUser = (id: number): void => {
+ let toggleIdx = 0;
+ if (id <= 1000 && chatState.currentUser.id === userStore.mainUser.id) {
+ toggleIdx = userStore.users.findIndex((user) => user.id === id);
+ } else if (id > 1000) {
+ // ๋จ์ฒด chat์ ๊ฒฝ์ฐ
+ let users =
+ messageList[messageList.findIndex((msg) => msg.id === id)].user;
+ toggleIdx = users.findIndex(
+ (user) => user.id === chatState.currentUser.id
+ );
+ if (toggleIdx === users.length - 1) {
+ toggleIdx = 0;
+ } else {
+ toggleIdx = userStore.users.findIndex(
+ (user) => user.id === users[toggleIdx + 1].id
+ );
+ }
+ }
+ setChatState({ ...chatState, currentUser: userStore.users[toggleIdx] });
+ };
+
+ // ํ์ฌ ๋ฉ์์ง ์ธํ
+ const handleChatRoom = (id: number): void => {
+ const toggleIndex = messageList.findIndex((msg) => msg.id === id);
+ setChatState({
+ currentUser: userStore.mainUser,
+ message: messageList[toggleIndex].messages,
+ });
+ };
+
+ return { addMessage, handleChatRoom, toggleUser };
+};
+
+export default useChatRoom;
diff --git a/src/hooks/useInput.tsx b/src/hooks/useInput.tsx
new file mode 100644
index 0000000..7d33657
--- /dev/null
+++ b/src/hooks/useInput.tsx
@@ -0,0 +1,19 @@
+import { useState } from "react";
+
+const useInput = (initialValue: string) => {
+ const [text, setText] = useState(initialValue);
+
+ // input text ๋ณ๊ฒฝ
+ const handleTextChange = (e: React.ChangeEvent): void => {
+ setText(e.target.value);
+ };
+
+ // ์
๋ ฅ๊ฐ ์ด๊ธฐํ
+ const resetText = (): void => {
+ setText("");
+ };
+
+ return { text, handleTextChange, resetText };
+};
+
+export default useInput;
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index 593edf1..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import App from './App';
-
-const root = ReactDOM.createRoot(document.getElementById('root'));
-root.render(
-
-
-
-);
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..e7720b8
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,18 @@
+import { RecoilRoot } from "recoil";
+import ReactDOM from "react-dom/client";
+import GlobalStyle from "style/GlobalStyle";
+import App from "./App";
+import { BrowserRouter } from "react-router-dom";
+
+const root = ReactDOM.createRoot(
+ document.getElementById("root") as HTMLInputElement,
+);
+
+root.render(
+
+
+
+
+
+ ,
+);
diff --git a/src/interface/alert.tsx b/src/interface/alert.tsx
new file mode 100644
index 0000000..cc8f014
--- /dev/null
+++ b/src/interface/alert.tsx
@@ -0,0 +1,4 @@
+export interface IAlert {
+ setVisibleAlert: React.Dispatch;
+ value: string;
+}
diff --git a/src/interface/chat.tsx b/src/interface/chat.tsx
new file mode 100644
index 0000000..0343de3
--- /dev/null
+++ b/src/interface/chat.tsx
@@ -0,0 +1,24 @@
+import { IUserType } from "./user";
+import { IMessageType } from "./message";
+
+export interface IChatListItem {
+ id: number;
+ user: IUserType[];
+ messages: IMessageType[];
+}
+
+export interface IChatRoomState {
+ message: IMessageType[];
+ currentUser: IUserType;
+}
+
+export interface IList {
+ link: number;
+ img: string;
+ title: string;
+ subTitle: string;
+}
+
+export interface ISearch {
+ filter: (text: string) => void;
+}
diff --git a/src/interface/index.tsx b/src/interface/index.tsx
new file mode 100644
index 0000000..06dfa5a
--- /dev/null
+++ b/src/interface/index.tsx
@@ -0,0 +1,23 @@
+import { IUserType, IUserState } from "./user";
+import {
+ IInputMessageForm,
+ IMessageType,
+ IInputEmoticon,
+ IEmoticonPopover,
+} from "./message";
+import { IAlert } from "./alert";
+import { IChatListItem, IList, IChatRoomState, ISearch } from "./chat";
+
+export type {
+ IUserType,
+ IUserState,
+ IInputEmoticon,
+ IEmoticonPopover,
+ IAlert,
+ IInputMessageForm,
+ IMessageType,
+ IChatListItem,
+ IList,
+ IChatRoomState,
+ ISearch,
+};
diff --git a/src/interface/message.tsx b/src/interface/message.tsx
new file mode 100644
index 0000000..9e53a34
--- /dev/null
+++ b/src/interface/message.tsx
@@ -0,0 +1,20 @@
+import { IUserType } from "./user";
+
+export interface IMessageType {
+ id: number;
+ text: string;
+ user: IUserType;
+ time: string;
+}
+
+export interface IInputMessageForm {
+ sendMessage: (text: string) => void;
+}
+
+export interface IInputEmoticon {
+ addEmoticonMessage: (emo: string) => void;
+}
+
+export interface IEmoticonPopover extends IInputEmoticon {
+ handlePopover: () => void;
+}
diff --git a/src/interface/user.tsx b/src/interface/user.tsx
new file mode 100644
index 0000000..fc1802e
--- /dev/null
+++ b/src/interface/user.tsx
@@ -0,0 +1,10 @@
+export interface IUserType {
+ id: number;
+ name: string;
+ statusMessage: string;
+}
+
+export interface IUserState {
+ users: IUserType[];
+ mainUser: IUserType;
+}
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 9dfc1c0..0000000
--- a/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/pages/ChatFriends.tsx b/src/pages/ChatFriends.tsx
new file mode 100644
index 0000000..74a99bb
--- /dev/null
+++ b/src/pages/ChatFriends.tsx
@@ -0,0 +1,15 @@
+import ChatFooter from "components/layout/ChatFooter";
+import ChatHeader from "components/layout/ChatHeader";
+import ChatFriendsList from "components/chatFriends/ChatFriendsList";
+
+const ChatFriends = () => {
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default ChatFriends;
diff --git a/src/pages/ChatList.tsx b/src/pages/ChatList.tsx
new file mode 100644
index 0000000..d78ee84
--- /dev/null
+++ b/src/pages/ChatList.tsx
@@ -0,0 +1,15 @@
+import ChatFooter from "components/layout/ChatFooter";
+import ChatHeader from "components/layout/ChatHeader";
+import MessageChatList from "components/chatList/MessageChatList";
+
+const ChatList = () => {
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default ChatList;
diff --git a/src/pages/ChatRoom.tsx b/src/pages/ChatRoom.tsx
new file mode 100644
index 0000000..24ee63f
--- /dev/null
+++ b/src/pages/ChatRoom.tsx
@@ -0,0 +1,25 @@
+import useChatRoom from "hooks/useChatRoom";
+import MessageChat from "components/messageChatRoom/MessageChat";
+import InputMessageForm from "components/messageInput/InputMessageForm";
+import MessageProfileContainer from "components/messageProfile/MessageProfileContainer";
+import { useEffect } from "react";
+import { useParams } from "react-router-dom";
+
+const ChatRoom = () => {
+ const { id } = useParams();
+ const { addMessage, handleChatRoom } = useChatRoom();
+
+ useEffect(() => {
+ handleChatRoom(Number(id));
+ }, []);
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default ChatRoom;
diff --git a/src/pages/Setting.tsx b/src/pages/Setting.tsx
new file mode 100644
index 0000000..22e243c
--- /dev/null
+++ b/src/pages/Setting.tsx
@@ -0,0 +1,15 @@
+import ChatFooter from "components/layout/ChatFooter";
+import ChatHeader from "components/layout/ChatHeader";
+import SettingList from "components/setting/SettingList";
+
+const Setting = () => {
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default Setting;
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
new file mode 100644
index 0000000..86b4bf6
--- /dev/null
+++ b/src/pages/index.tsx
@@ -0,0 +1,6 @@
+import ChatList from "./ChatList";
+import ChatRoom from "./ChatRoom";
+import ChatFriends from "./ChatFriends";
+import Setting from "./Setting";
+
+export { ChatList, ChatRoom, ChatFriends, Setting };
diff --git a/src/recoil/recoil.tsx b/src/recoil/recoil.tsx
new file mode 100644
index 0000000..fa43502
--- /dev/null
+++ b/src/recoil/recoil.tsx
@@ -0,0 +1,30 @@
+import messages from "data/message.json";
+import users from "data/user.json";
+import { IChatListItem, IChatRoomState, IUserState } from "interface";
+import { atom } from "recoil";
+
+export const messageState = atom({
+ key: "messages",
+ default: messages,
+});
+
+export const userState = atom({
+ key: "user",
+ default: {
+ users: users,
+ mainUser: users[0],
+ },
+});
+
+export const chatRoomState = atom({
+ key: "chatRoom",
+ default: {
+ message: [],
+ currentUser: { id: 0, name: "", statusMessage: "" },
+ },
+});
+
+export const resizeState = atom({
+ key: "resize",
+ default: false,
+});
diff --git a/src/style/GlobalStyle.tsx b/src/style/GlobalStyle.tsx
new file mode 100644
index 0000000..3010938
--- /dev/null
+++ b/src/style/GlobalStyle.tsx
@@ -0,0 +1,10 @@
+import { createGlobalStyle } from "styled-components";
+
+const GlobalStyle = createGlobalStyle`
+ body {
+ margin: 0;
+ font-family: "Noto Sans KR", sans-serif;
+}
+`;
+
+export default GlobalStyle;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..5493aa7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "baseUrl": "./src",
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "types": ["react/next", "react-dom/next"]
+ },
+ "include": ["src"]
+}