diff --git a/.gitignore b/.gitignore index 4d29575d..e85f4eb5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +node_modules \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..8b0bc4ef --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} \ No newline at end of file diff --git a/README copy.md b/README copy.md deleted file mode 100644 index 82b92ada..00000000 --- a/README copy.md +++ /dev/null @@ -1,66 +0,0 @@ -# 서론 - -안녕하세요 🙌🏻 19기 프론트 운영진 배성준입니다. 이번 미션에서는 드디어 투두리스트에서 벗어나 새로운 프로젝트인 **messenger** 만들기를 진행합니다. - -이번주는 특별히 **디자이너와의 협업**으로 진행되는 미션입니다. 디자이너분께서 열심히 리디자인 한 메신저 프로젝트를 여러분들께서 구현해주시면 됩니다. - -동시에, 이번주부터는 새로 **TypeScript**를 적용해보려고 합니다. - -프로젝트의 규모가 커지게 될 수록, 컴포넌트가 가지는 props의 종류 또한 다양해지게 됩니다. 무지성 코딩을 하다보면 가끔 이 props의 구성과 이름, 어떤 타입이 들어가야 하는지 헷갈리기 마련이죠. 보통 그럴 때 다시 컴포넌트 정의 부분으로 돌아가 props의 구성을 보고 오곤 합니다. - -하지만 이럴 때, typescript를 적용한다면 컴포넌트의 구성과 그 이름, 심지어 타입까지 한번에 자동완성으로 편리하게 관리할 수 있고, 생산성이 증대되겠죠. - -또한, **React Hooks**에 조금 더 익숙해지는 것을 목표로 합니다. 여러 Hook들이 있지만 그 중에서도 `useState`, `useEffect`, `useRef`를 중점적으로 사용해 보는 미션인데요, React를 사용하면서 굉장히 자주 쓰이는 Hook들이기 때문에 이 부분을 집중적으로 공부해 보아요! - -그럼 이번 미션도 파이팅입니다!!🎉 - -# 미션 - -## Key Questions - -- JavaScript를 사용할때에 비해 TypeScript를 사용할 때의 장점은 무엇인가요? -- 디자이너로부터 전달받은 피그마 링크 혹은, 피그마 캡처본 -- 컴포넌트를 분리한 기준은 무엇인가요? -- 디자인 시스템을 적용하면서 느낀 점은 무엇인가요? -- 디자이너와 소통하며 느낀점은 무엇인가요? - -## 미션 목표 - -- TypeScript를 사용해봅시다. -- useState로 컴포넌트의 상태를 관리합니다. -- useEffect와 useRef의 사용법을 이해합니다. -- styled-components를 통한 CSS-in-JS 및 CSS Preprocessor의 사용법에 익숙해집니다. - -## 기한 - -2024년 3월 29일 금요일 - -## 필수 구현 기능 - -- 피그마를 보고 [결과화면](https://3th-fb-messenger.netlify.app)과 같이 구현합니다. -- 디자인 시스템을 구축합니다. -- 채팅방 상단의 프로필을 클릭하면 사용자를 변경할 수 있습니다. -- 메세지를 보내면 채팅방 하단으로 스크롤을 이동시킵니다. -- 메세지에 유저 정보(프로필 사진, 이름)를 표시합니다. -- user와 message 데이터를 json 파일에 저장합니다. -- UI는 **반응형을 제외**하고 피그마파일을 따라서 진행합니다. - -### 추가 구현 기능 - -- 더블 클릭 하면 감정표현을 추가합니다. -- 그 외 추가하고 싶은 기능이 있다면 마음껏 추가해 주세요! - -참고로 이번 과제는 다음주까지 이어지는 과제이므로 **확장성**을 충분히 고려해 주세요. 참고로 **4주차 과제에서는 유저 및 기능 추가와 Routing을** 진행합니다. 이를 위해 [recoil](https://recoiljs.org/ko/)이나 [redux](https://ko.redux.js.org/introduction/getting-started/)를 이용한 상태관리를 미리 해보시는 것을 추천합니다!! 모두 공식문서 많이 읽어보시고 자신만의 상태관리 조합도 찾아보면 재밌을 거에요 XD - -## 링크 및 참고자료 - -- [React docs - Hook](https://ko.reactjs.org/docs/hooks-intro.html) -- [React의 Hooks 완벽 정복하기](https://velog.io/@velopert/react-hooks#1-usestate) -- [useEffect 완벽 가이드](https://overreacted.io/ko/a-complete-guide-to-useeffect/) -- [코딩 컨벤션](https://ui.toast.com/fe-guide/ko_CODING-CONVENTION) -- [타입스크립트 핸드북](https://joshua1988.github.io/ts/intro.html) -- [리액트 프로젝트에서 타입스크립트 사용하기 (시리즈)](https://velog.io/@velopert/series/react-with-typescript) -- [디자인 시스템 구축기](https://yozm.wishket.com/magazine/detail/1830/) -- [[영상] : 컴포넌트에 대한 이해](https://www.youtube.com/watch?v=21eiJc90ggo) -- [Styled Component로 디자인 시스템 구축하기](https://zaat.dev/blog/building-a-design-system-in-react-with-styled-components/) -- [ts 절대경로 설정하기](https://tesseractjh.tistory.com/232) diff --git a/package-lock.json b/package-lock.json index c27bbe4e..3a78dee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,23 @@ "@types/node": "^16.18.91", "@types/react": "^18.2.69", "@types/react-dom": "^18.2.22", + "animate.css": "^4.1.1", + "eslint-plugin-react": "^7.36.1", "react": "^18.2.0", + "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", + "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", + "recoil": "^0.7.7", + "recoil-persist": "^5.1.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "prettier-plugin-tailwindcss": "^0.6.6", + "tailwindcss": "^3.4.13" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -3338,6 +3350,15 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", + "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4851,6 +4872,12 @@ "ajv": "^6.9.1" } }, + "node_modules/animate.css": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", + "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==", + "license": "MIT" + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -5072,27 +5099,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, "node_modules/array.prototype.tosorted": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", - "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { @@ -5145,9 +5165,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "funding": [ { "type": "opencollective", @@ -5162,12 +5182,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -5634,9 +5655,9 @@ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "funding": [ { "type": "opencollective", @@ -5651,11 +5672,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -5762,9 +5784,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001600", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", - "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", + "version": "1.0.30001663", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz", + "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==", "funding": [ { "type": "opencollective", @@ -5778,7 +5800,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -7018,9 +7041,10 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.715", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz", - "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==" + "version": "1.5.27", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz", + "integrity": "sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==", + "license": "ISC" }, "node_modules/emittery": { "version": "0.8.1", @@ -7091,9 +7115,10 @@ } }, "node_modules/es-abstract": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz", - "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "arraybuffer.prototype.slice": "^1.0.3", @@ -7134,11 +7159,11 @@ "safe-regex-test": "^1.0.3", "string.prototype.trim": "^1.2.9", "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.7", + "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.2", "typed-array-byte-length": "^1.0.1", "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.5", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", "which-typed-array": "^1.1.15" }, @@ -7193,13 +7218,14 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.0.18", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz", - "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==", + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", + "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", @@ -7569,34 +7595,35 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.34.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", - "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", + "version": "7.36.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz", + "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==", + "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlast": "^1.2.4", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", - "array.prototype.tosorted": "^1.1.3", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.17", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7", - "object.hasown": "^1.1.3", - "object.values": "^1.1.7", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.10" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { @@ -8861,6 +8888,12 @@ "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": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==", + "license": "MIT" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -12619,9 +12652,10 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -12804,22 +12838,6 @@ "node": ">= 0.4" } }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dependencies": { - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", @@ -13092,9 +13110,10 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -13258,9 +13277,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -13275,10 +13294,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -14464,6 +14484,102 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.6.tgz", + "integrity": "sha512-OPva5S7WAsPLEsOuOWXATi13QrCKACCiIonFgIR6V4lYv4QLp++UXVhZSzRbZxXGimkQtQT86CC6fQqTOybGng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig-melody": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig-melody": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -14834,6 +14950,19 @@ "node": ">=8" } }, + "node_modules/react-device-detect": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.2.3.tgz", + "integrity": "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==", + "license": "MIT", + "dependencies": { + "ua-parser-js": "^1.0.33" + }, + "peerDependencies": { + "react": ">= 0.14.0", + "react-dom": ">= 0.14.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -14864,6 +14993,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", + "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", + "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2", + "react-router": "6.26.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -14968,6 +15129,35 @@ "node": ">=8.10.0" } }, + "node_modules/recoil": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", + "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", + "license": "MIT", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/recoil-persist": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/recoil-persist/-/recoil-persist-5.1.0.tgz", + "integrity": "sha512-sew4k3uBVJjRWKCSFuBw07Y1p1pBOb0UxLJPxn4G2bX/9xNj+r2xlqYy/BRfyofR/ANfqBU04MIvulppU4ZC0w==", + "license": "MIT", + "peerDependencies": { + "recoil": "^0.7.2" + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -15805,9 +15995,10 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -16117,6 +16308,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -16491,9 +16692,10 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", + "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -16503,7 +16705,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -16947,6 +17149,32 @@ "node": ">=4.2.0" } }, + "node_modules/ua-parser-js": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz", + "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -17044,9 +17272,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "funding": [ { "type": "opencollective", @@ -17061,9 +17289,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/package.json b/package.json index ea335d36..937a3ef5 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,15 @@ "@types/node": "^16.18.91", "@types/react": "^18.2.69", "@types/react-dom": "^18.2.22", + "animate.css": "^4.1.1", + "eslint-plugin-react": "^7.36.1", "react": "^18.2.0", + "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", + "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", + "recoil": "^0.7.7", + "recoil-persist": "^5.1.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, @@ -25,8 +31,13 @@ "eslintConfig": { "extends": [ "react-app", - "react-app/jest" - ] + "react-app/jest", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "warn" + } }, "browserslist": { "production": [ @@ -39,5 +50,11 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "prettier-plugin-tailwindcss": "^0.6.6", + "tailwindcss": "^3.4.13" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/favicon-messenger.png b/public/favicon-messenger.png new file mode 100644 index 00000000..02dbed7f Binary files /dev/null and b/public/favicon-messenger.png differ diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index a11777cc..00000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/index.html b/public/index.html index aa069f27..03cbafd7 100644 --- a/public/index.html +++ b/public/index.html @@ -1,43 +1,12 @@ - + - + - - - - - - - React App + Direct Message -
- diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a3..00000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a65..00000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 080d6c77..00000000 --- a/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index e9e57dc4..00000000 --- a/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e053..00000000 --- a/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/App.tsx b/src/App.tsx index 5381007b..110c400c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,76 @@ +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { RecoilRoot } from "recoil"; +import { BrowserView, MobileView } from "react-device-detect"; + +// common +import { ReactComponent as HomeIndicator } from "./assets/images/home_indicator.svg"; +import StatusBar from "./components/common/StatusBar"; + +// pages +import MyProfilePage from "./pages/MyProfilePage"; +import FollowListPage from "./pages/FollowListPage"; +import ChatListPage from "./pages/ChatListPage"; +import ChatRoomPage from "./pages/ChatRoomPage"; + +//hooks +import useHandleHeight from "./hooks/useHandleHeight"; + function App() { + const height = useHandleHeight(); + return ( -
-

20기 프론트엔드 파이팅!!! 디자인과 사이좋게 지내요~~~

-
+ <> + + {/*브라우저 뷰 (상하단 바 포함)*/} + +
+ +
+ + + }> + } + > + }> + } + > + + +
+ +
+
+ {/*모바일 뷰*/} + +
+ + + }> + } + > + }> + } + > + + +
+
+
+ ); } diff --git a/src/assets/fonts/SF-Pro-Text-Bold.otf b/src/assets/fonts/SF-Pro-Text-Bold.otf new file mode 100644 index 00000000..93a89484 Binary files /dev/null and b/src/assets/fonts/SF-Pro-Text-Bold.otf differ diff --git a/src/assets/fonts/SF-Pro-Text-Regular.otf b/src/assets/fonts/SF-Pro-Text-Regular.otf new file mode 100644 index 00000000..e6301485 Binary files /dev/null and b/src/assets/fonts/SF-Pro-Text-Regular.otf differ diff --git a/src/assets/fonts/SF-Pro.ttf b/src/assets/fonts/SF-Pro.ttf new file mode 100644 index 00000000..e051a82c Binary files /dev/null and b/src/assets/fonts/SF-Pro.ttf differ diff --git a/src/assets/icons/arrow.svg b/src/assets/icons/arrow.svg new file mode 100644 index 00000000..ba5193d3 --- /dev/null +++ b/src/assets/icons/arrow.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/back.svg b/src/assets/icons/back.svg new file mode 100644 index 00000000..60505d7a --- /dev/null +++ b/src/assets/icons/back.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/camera.svg b/src/assets/icons/camera.svg new file mode 100644 index 00000000..ca033379 --- /dev/null +++ b/src/assets/icons/camera.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/icons/camera_gray.svg b/src/assets/icons/camera_gray.svg new file mode 100644 index 00000000..32275952 --- /dev/null +++ b/src/assets/icons/camera_gray.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/edit.svg b/src/assets/icons/edit.svg new file mode 100644 index 00000000..7e8d3519 --- /dev/null +++ b/src/assets/icons/edit.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/ellipse.svg b/src/assets/icons/ellipse.svg new file mode 100644 index 00000000..d4791cd9 --- /dev/null +++ b/src/assets/icons/ellipse.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/emotion_add.svg b/src/assets/icons/emotion_add.svg new file mode 100644 index 00000000..75620e13 --- /dev/null +++ b/src/assets/icons/emotion_add.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/emotion_angry.svg b/src/assets/icons/emotion_angry.svg new file mode 100644 index 00000000..ce228170 --- /dev/null +++ b/src/assets/icons/emotion_angry.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/emotion_heart.svg b/src/assets/icons/emotion_heart.svg new file mode 100644 index 00000000..6172d3c6 --- /dev/null +++ b/src/assets/icons/emotion_heart.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/emotion_joy.svg b/src/assets/icons/emotion_joy.svg new file mode 100644 index 00000000..1e51ace3 --- /dev/null +++ b/src/assets/icons/emotion_joy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/emotion_surprise.svg b/src/assets/icons/emotion_surprise.svg new file mode 100644 index 00000000..37c1ed79 --- /dev/null +++ b/src/assets/icons/emotion_surprise.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/emotion_tear.svg b/src/assets/icons/emotion_tear.svg new file mode 100644 index 00000000..0330d2d7 --- /dev/null +++ b/src/assets/icons/emotion_tear.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/emotion_thumbup.svg b/src/assets/icons/emotion_thumbup.svg new file mode 100644 index 00000000..7674981c --- /dev/null +++ b/src/assets/icons/emotion_thumbup.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/gif.svg b/src/assets/icons/gif.svg new file mode 100644 index 00000000..396bd82a --- /dev/null +++ b/src/assets/icons/gif.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/grid.svg b/src/assets/icons/grid.svg new file mode 100644 index 00000000..33ae68e5 --- /dev/null +++ b/src/assets/icons/grid.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icons/grid2.svg b/src/assets/icons/grid2.svg new file mode 100644 index 00000000..cf7128c8 --- /dev/null +++ b/src/assets/icons/grid2.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icons/home.svg b/src/assets/icons/home.svg new file mode 100644 index 00000000..19403d8e --- /dev/null +++ b/src/assets/icons/home.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/info.svg b/src/assets/icons/info.svg new file mode 100644 index 00000000..46d57a00 --- /dev/null +++ b/src/assets/icons/info.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/icons/menu.svg b/src/assets/icons/menu.svg new file mode 100644 index 00000000..6fd0181b --- /dev/null +++ b/src/assets/icons/menu.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/message.svg b/src/assets/icons/message.svg new file mode 100644 index 00000000..b53fcaf8 --- /dev/null +++ b/src/assets/icons/message.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/message_active.svg b/src/assets/icons/message_active.svg new file mode 100644 index 00000000..91204399 --- /dev/null +++ b/src/assets/icons/message_active.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/mic.svg b/src/assets/icons/mic.svg new file mode 100644 index 00000000..6a20a594 --- /dev/null +++ b/src/assets/icons/mic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/more.svg b/src/assets/icons/more.svg new file mode 100644 index 00000000..2dab7e10 --- /dev/null +++ b/src/assets/icons/more.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/phone.svg b/src/assets/icons/phone.svg new file mode 100644 index 00000000..71e458a6 --- /dev/null +++ b/src/assets/icons/phone.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/picture.svg b/src/assets/icons/picture.svg new file mode 100644 index 00000000..ea0f4e6d --- /dev/null +++ b/src/assets/icons/picture.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/pin.svg b/src/assets/icons/pin.svg new file mode 100644 index 00000000..87797ffd --- /dev/null +++ b/src/assets/icons/pin.svg @@ -0,0 +1,20 @@ + + + + + + + diff --git a/src/assets/icons/pin2.svg b/src/assets/icons/pin2.svg new file mode 100644 index 00000000..08a4d63e --- /dev/null +++ b/src/assets/icons/pin2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/plus.svg b/src/assets/icons/plus.svg new file mode 100644 index 00000000..54f98272 --- /dev/null +++ b/src/assets/icons/plus.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/profile.svg b/src/assets/icons/profile.svg new file mode 100644 index 00000000..c17b88a5 --- /dev/null +++ b/src/assets/icons/profile.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/profile_active.svg b/src/assets/icons/profile_active.svg new file mode 100644 index 00000000..e03558b2 --- /dev/null +++ b/src/assets/icons/profile_active.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg new file mode 100644 index 00000000..efac557a --- /dev/null +++ b/src/assets/icons/search.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/send_button.svg b/src/assets/icons/send_button.svg new file mode 100644 index 00000000..97621081 --- /dev/null +++ b/src/assets/icons/send_button.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/tagged.svg b/src/assets/icons/tagged.svg new file mode 100644 index 00000000..f3096eb7 --- /dev/null +++ b/src/assets/icons/tagged.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/tagged2.svg b/src/assets/icons/tagged2.svg new file mode 100644 index 00000000..50905d63 --- /dev/null +++ b/src/assets/icons/tagged2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/videocam.svg b/src/assets/icons/videocam.svg new file mode 100644 index 00000000..f777b1e0 --- /dev/null +++ b/src/assets/icons/videocam.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/ceos_logo.svg b/src/assets/images/ceos_logo.svg new file mode 100644 index 00000000..56aed933 --- /dev/null +++ b/src/assets/images/ceos_logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/content1.png b/src/assets/images/content1.png new file mode 100644 index 00000000..64838fe6 Binary files /dev/null and b/src/assets/images/content1.png differ diff --git a/src/assets/images/content2.png b/src/assets/images/content2.png new file mode 100644 index 00000000..647f3d2c Binary files /dev/null and b/src/assets/images/content2.png differ diff --git a/src/assets/images/content3.png b/src/assets/images/content3.png new file mode 100644 index 00000000..c6f69355 Binary files /dev/null and b/src/assets/images/content3.png differ diff --git a/src/assets/images/ewha_logo.svg b/src/assets/images/ewha_logo.svg new file mode 100644 index 00000000..c4325231 --- /dev/null +++ b/src/assets/images/ewha_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/images/home_indicator.svg b/src/assets/images/home_indicator.svg new file mode 100644 index 00000000..ecb5158d --- /dev/null +++ b/src/assets/images/home_indicator.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/images/hongik_logo.svg b/src/assets/images/hongik_logo.svg new file mode 100644 index 00000000..968e0197 --- /dev/null +++ b/src/assets/images/hongik_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/images/newJeans_logo.svg b/src/assets/images/newJeans_logo.svg new file mode 100644 index 00000000..1fdf7fee --- /dev/null +++ b/src/assets/images/newJeans_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/images/none_tagged.png b/src/assets/images/none_tagged.png new file mode 100644 index 00000000..8103d7aa Binary files /dev/null and b/src/assets/images/none_tagged.png differ diff --git a/src/assets/images/profile_img.svg b/src/assets/images/profile_img.svg new file mode 100644 index 00000000..59a2592e --- /dev/null +++ b/src/assets/images/profile_img.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/images/profile_img2.svg b/src/assets/images/profile_img2.svg new file mode 100644 index 00000000..bf902d9a --- /dev/null +++ b/src/assets/images/profile_img2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/ChatList/ChatListContents.tsx b/src/components/ChatList/ChatListContents.tsx new file mode 100644 index 00000000..a674e635 --- /dev/null +++ b/src/components/ChatList/ChatListContents.tsx @@ -0,0 +1,99 @@ +// components +import ChatListItem from "./ChatListItem"; + +// icons +import { ReactComponent as SearchIcon } from "../../assets/icons/search.svg"; + +// reoil +import { useRecoilValue } from "recoil"; +import { chattingState, userState } from "../../recoil/atom"; +import { useEffect, useState } from "react"; + +import userData from "../../data/UserData.json"; + +const ChatListContents = () => { + const chattings = useRecoilValue(chattingState); + const user = useRecoilValue(userState); + const myId = user.me.id; + + // 검색 기능 + const [searchInput, setSearchInput] = useState(""); + const handleInput = (e: React.ChangeEvent) => { + setSearchInput(e.target.value); + }; + + // 상단 고정 기능 + const [pinnedChatIds, setPinnedChatIds] = useState(() => { + const storedPinnedChatIds = localStorage.getItem("pinnedChatIds"); + return storedPinnedChatIds ? JSON.parse(storedPinnedChatIds) : []; + }); + + useEffect(() => { + // pinnedChatIds 변경 시 로컬 스토리지에 저장 + localStorage.setItem("pinnedChatIds", JSON.stringify(pinnedChatIds)); + }, [pinnedChatIds]); + + const handlePinToggle = (id: number | undefined) => { + if (id !== undefined) { + setPinnedChatIds( + (prevIds) => + prevIds.includes(id) + ? prevIds.filter((chatId) => chatId !== id) // 이미 고정된 경우 해제 + : [id, ...prevIds], // 새로운 고정 항목은 가장 상단으로 추가 + ); + } + }; + + const filteredFollowers = chattings + .filter((chat) => + chat.users.some((userId) => { + if (userId !== myId) { + const otherUser = userData.users.find((user) => user.id === userId); // 대화가 존재하는 사용자와의 채팅 필터 + return otherUser?.userName.toLocaleLowerCase().includes(searchInput); // 검색어에 부합하는 사용자와의 채팅 필터 + } + return false; + }), + ) + .sort((a, b) => { + // 고정된 항목 상단으로 + 최신 항목이 더 위에 오도록 정렬 + const aIdx = pinnedChatIds.indexOf(a.id!); + const bIdx = pinnedChatIds.indexOf(b.id!); + if (aIdx === -1 && bIdx === -1) return 0; // 모두 핀 되지 않은 경우 -> 순서 변경 안함 + if (aIdx === -1) return 1; // a 핀x, b 핀o + if (bIdx === -1) return -1; // a 핀o, b 핀x + return aIdx - bIdx; // 둘다 핀 된 경우 -> 배열 순대로 + }); + + return ( +
+
+ + +
+
+ Messages + Requests +
+
+ {filteredFollowers.map( + (chatting) => + chatting.id !== undefined && ( // id가 undefined가 아닌 항목만 렌더링 + handlePinToggle(chatting.id)} + /> + ), + )} +
+
+ ); +}; + +export default ChatListContents; diff --git a/src/components/ChatList/ChatListItem.tsx b/src/components/ChatList/ChatListItem.tsx new file mode 100644 index 00000000..be7acdd4 --- /dev/null +++ b/src/components/ChatList/ChatListItem.tsx @@ -0,0 +1,105 @@ +import { useNavigate } from "react-router-dom"; + +// icons +import { ReactComponent as CameraIcon } from "../../assets/icons/camera_gray.svg"; +import { ReactComponent as PinnedIcon } from "../../assets/icons/pin.svg"; +import { ReactComponent as PinIcon } from "../../assets/icons/pin2.svg"; + +// data & recoil +import userData from "../../data/UserData.json"; +import { chattingInterface } from "../../types/interface"; +import useHandleTime from "../../hooks/useHandleTime"; +import { useState } from "react"; + +interface ChatListItemProps { + chatting: chattingInterface; + isPinned: boolean; + handleTogglePin: () => void; +} + +const ChatListItem: React.FC = ({ + chatting, + isPinned, + handleTogglePin, +}) => { + const followerId = chatting.users.filter((id) => id !== 0)[0]; + const follower = userData.users.filter((user) => user.id === followerId)[0]; + + const lastMessageContent = chatting.chatList[chatting.chatList.length - 1]; + const lastMessage = lastMessageContent?.message.startsWith("data:image/") + ? "이미지가 전송됨" // 이미지 파일일 경우 경로 대신 대체 텍스트 + : lastMessageContent.message; + + // + const { lastModified } = useHandleTime(lastMessageContent); + + const navigate = useNavigate(); + const gotoChatRoom = () => { + navigate(`/chatroom/${followerId}`); + }; + + const [isSlided, setIsSlided] = useState(false); + const clickCamera = () => { + setIsSlided(!isSlided); + }; + + const clickPin = () => { + handleTogglePin(); + // console.log("Pinned!"); // 콘솔 대신 핀 기능 구현 + setIsSlided(false); + }; + + return ( +
+
+
+ + + gotoChatRoom()} + className="flex flex-grow cursor-pointer flex-col" + > +
+ + {follower.userName} + + {isPinned && } +
+ +
+ + {lastMessage} + + + {lastModified} + +
+
+ +
+
+
+ {isSlided && ( + + + + )} +
+ ); +}; + +export default ChatListItem; diff --git a/src/components/ChatList/TopBar.tsx b/src/components/ChatList/TopBar.tsx new file mode 100644 index 00000000..9b590a45 --- /dev/null +++ b/src/components/ChatList/TopBar.tsx @@ -0,0 +1,29 @@ +import { useNavigate } from "react-router-dom"; +// icons +import { ReactComponent as BackIcon } from "../../assets/icons/back.svg"; +import { ReactComponent as ArrowIcon } from "../../assets/icons/arrow.svg"; +import { ReactComponent as EditIcon } from "../../assets/icons/edit.svg"; + +const TopBar = () => { + const navigate = useNavigate(); + + return ( + <> + + + + navigate("/")} + /> + jngynjng + + + + + + + ); +}; + +export default TopBar; diff --git a/src/components/ChatRoom/Chattings.tsx b/src/components/ChatRoom/Chattings.tsx new file mode 100644 index 00000000..66c1a75a --- /dev/null +++ b/src/components/ChatRoom/Chattings.tsx @@ -0,0 +1,98 @@ +// components +import InputBox from "./InputBox"; +import MyChat from "./MyChat"; +import ReceivedChat from "./ReceivedChat"; + +// recoil +import { useRecoilValue } from "recoil"; +import { selectedEmotionsState, selectedMessageState } from "../../recoil/atom"; + +// hooks +import useAutoScroll from "../../hooks/useAutoScroll"; +import useChatSend from "../../hooks/useChatSend"; +import useEmotionBox from "../../hooks/useEmotionBox"; + +const Chattings = () => { + const { users, currentChatting, sendChat } = useChatSend(); + + // 하단으로 자동 스크롤 + const scrollRef = useAutoScroll([currentChatting.chatList]); + + // 감정 남기기 + const { handleLongPress, emotionBoxRef } = useEmotionBox(scrollRef); + const selectedMessage = useRecoilValue(selectedMessageState); + const selectedEmotions = useRecoilValue(selectedEmotionsState); + + return ( +
+
+ {currentChatting.chatList.length > 0 ? ( + // 이전 대화내역이 있을 때 + currentChatting.chatList.map((chat, index) => + chat.sender === users.me.id ? ( + + ) : ( + handleLongPress(index, e)} + isSelected={selectedMessage === index} + selectedEmotion={selectedEmotions[index] || null} + emotionBoxRef={emotionBoxRef} + /> + ), + ) + ) : ( + // 이전 대화내역이 없을 때 +
+ + +
+ {users.other.userName} +
+
{users.other.userId}
+
735 followers 174 posts
+
+ hongik_university also follows +
+ + + Inquire + + + View profile + + +
+ )} +
+ +
+ ); +}; +export default Chattings; diff --git a/src/components/ChatRoom/EmotionBox.tsx b/src/components/ChatRoom/EmotionBox.tsx new file mode 100644 index 00000000..5db1a579 --- /dev/null +++ b/src/components/ChatRoom/EmotionBox.tsx @@ -0,0 +1,44 @@ +import heart from "../../assets/icons/emotion_heart.svg"; +import joy from "../../assets/icons/emotion_joy.svg"; +import surprise from "../../assets/icons/emotion_surprise.svg"; +import tear from "../../assets/icons/emotion_tear.svg"; +import angry from "../../assets/icons/emotion_angry.svg"; +import thumbup from "../../assets/icons/emotion_thumbup.svg"; +import plus from "../../assets/icons/emotion_add.svg"; + +interface EmotionBoxProps { + onSelectEmotion: (emotionId: number) => void; // 선택된 감정을 처리하는 함수 prop +} + +const EmotionBox: React.FC = ({ onSelectEmotion }) => { + const emotionList = [ + { id: 1, src: heart }, + { id: 2, src: joy }, + { id: 3, src: surprise }, + { id: 4, src: tear }, + { id: 5, src: angry }, + { id: 6, src: thumbup }, + { id: 7, src: plus }, + ]; + return ( + <> +
+
+ Tap and hold to super react +
+ + {emotionList.map((item) => ( + onSelectEmotion(item.id)} + className="cursor-pointer" + /> + ))} + +
+ + ); +}; + +export default EmotionBox; diff --git a/src/components/ChatRoom/EmotionRemained.tsx b/src/components/ChatRoom/EmotionRemained.tsx new file mode 100644 index 00000000..10cc5d77 --- /dev/null +++ b/src/components/ChatRoom/EmotionRemained.tsx @@ -0,0 +1,39 @@ +import heart from "../../assets/icons/emotion_heart.svg"; +import joy from "../../assets/icons/emotion_joy.svg"; +import surprise from "../../assets/icons/emotion_surprise.svg"; +import tear from "../../assets/icons/emotion_tear.svg"; +import angry from "../../assets/icons/emotion_angry.svg"; +import thumbup from "../../assets/icons/emotion_thumbup.svg"; +import plus from "../../assets/icons/emotion_add.svg"; + +interface EmotionRemainedProps { + emotionId: number; + isMine: boolean; +} + +const EmotionRemained: React.FC = ({ + emotionId, + isMine, +}) => { + const emotionList = [ + { id: 1, src: heart }, + { id: 2, src: joy }, + { id: 3, src: surprise }, + { id: 4, src: tear }, + { id: 5, src: angry }, + { id: 6, src: thumbup }, + { id: 7, src: plus }, + ]; + + const emotionIcon = emotionList.find((emotion) => emotion.id === emotionId); + + return ( +
+ emotion +
+ ); +}; + +export default EmotionRemained; diff --git a/src/components/ChatRoom/InputBox.tsx b/src/components/ChatRoom/InputBox.tsx new file mode 100644 index 00000000..e8e71c2e --- /dev/null +++ b/src/components/ChatRoom/InputBox.tsx @@ -0,0 +1,114 @@ +import { useRef, useState } from "react"; + +// icons +import { ReactComponent as CameraIcon } from "../../assets/icons/camera.svg"; +import { ReactComponent as MicIcon } from "../../assets/icons/mic.svg"; +import { ReactComponent as PictureIcon } from "../../assets/icons/picture.svg"; +import { ReactComponent as GIFIcon } from "../../assets/icons/gif.svg"; +import { ReactComponent as SendButton } from "../../assets/icons/send_button.svg"; + +interface InputBoxProps { + sendChat: (message: string | File, type: "text" | "image") => void; +} + +const InputBox: React.FC = ({ sendChat }) => { + const [inputText, setInputText] = useState(""); // 입력한 텍스트 + const heightRef = useRef(null); // 입력창 높이 지정 + const fileInputRef = useRef(null); + + // 입력 내용 반영 + const handleChange = (e: React.ChangeEvent) => { + setInputText(e.target.value); + }; + + // 입력 내용 전송 + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (inputText.trim() !== "") { + sendChat(inputText, "text"); + setInputText(""); // 입력창 내용 초기화 + if (heightRef.current) { + heightRef.current.style.height = "auto"; // 전송 후 입력창 높이 초기화 + } + } + }; + + // 이미지 업로드 + const handleImageUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + // setUploadedImage(file); + sendChat(file, "image"); + console.log("image ", file); + } + }; + + // PictureIcon 클릭 시 파일 업로드 창 열기 + const handlePictureIconClick = () => { + fileInputRef.current?.click(); + console.log("click"); + }; + + // 입력 내용 엔터키로 전송 (shift + Enter로 줄바꿈 가능) + const handleEnterSubmit = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + // 입력창 높이 자동 조절 + const handleInputHeight = () => { + if (heightRef.current) { + heightRef.current.style.height = "auto"; // 기본 높이 + heightRef.current.style.height = heightRef.current.scrollHeight + "px"; // 줄바꿈 시 변화 + } + }; + + return ( +
+ +
+